Skip to content

跳表(Skip List)实现详解

一、是什么?

跳表是一种概率型数据结构,用于在有序元素集合中实现高效的查找、插入和删除操作。它由多层链表组成,其中:

  • 底层(第0层)包含所有元素的有序链表
  • 上层链表是下层的"快速通道",包含部分元素的子集
  • 每个元素被随机分配到不同层级的链表中

二、解决什么问题

  1. 有序链表的查找效率问题:普通有序链表的查找时间复杂度为O(n)
  2. 平衡树的复杂度问题:平衡树(如红黑树)实现复杂,维护成本高
  3. 高效操作需求:需要支持O(log n)时间复杂度的查找、插入和删除操作
  4. 实现简单性:相比平衡树,跳表实现更简单,尤其适合并发环境

三、核心实现机制

  1. 节点结构
java
class SkipListNode {
    int value;
    SkipListNode[] forward; // 指向各层的下一个节点
    
    SkipListNode(int value, int level) {
        this.value = value;
        this.forward = new SkipListNode[level + 1];
    }
}
  1. 核心操作流程
  1. 关键算法步骤

    1. 随机层数生成:使用概率函数决定节点高度
    java
    private int randomLevel() {
        int level = 0;
        while (Math.random() < P_FACTOR && level < MAX_LEVEL) {
            level++;
        }
        return level;
    }
    1. 查找操作
    • 从最高层开始向右遍历
    • 当右侧节点值大于目标值时向下移动一层
    • 重复直到第0层找到目标节点
    1. 插入操作
    • 查找插入位置并记录搜索路径
    • 随机生成新节点层数
    • 创建节点并更新各层指针
    1. 删除操作
    • 查找目标节点并记录搜索路径
    • 更新所有相关层的指针
    • 释放节点内存

四、应用场景

  1. 内存数据库索引:如Redis的有序集合(zset)
  2. 高效有序集合:Java的ConcurrentSkipListSetConcurrentSkipListMap
  3. 范围查询系统:需要快速查找某个范围内的元素
  4. 替代平衡树:当需要简单实现且不严格要求平衡时
  5. 并发数据结构:跳表天然适合实现无锁并发数据结构

五、Java示例实现

java
import java.util.Random;

public class SkipList {
    private static final int MAX_LEVEL = 16;
    private static final double P_FACTOR = 0.5;
    private final SkipListNode head = new SkipListNode(Integer.MIN_VALUE, MAX_LEVEL);
    private int currentLevel = 0;
    private final Random random = new Random();
    
    static class SkipListNode {
        int value;
        SkipListNode[] forward;
        
        SkipListNode(int value, int level) {
            this.value = value;
            this.forward = new SkipListNode[level + 1];
        }
    }
    
    public boolean search(int target) {
        SkipListNode current = head;
        for (int i = currentLevel; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].value < target) {
                current = current.forward[i];
            }
        }
        current = current.forward[0];
        return current != null && current.value == target;
    }
    
    public void add(int num) {
        SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
        SkipListNode current = head;
        
        for (int i = currentLevel; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].value < num) {
                current = current.forward[i];
            }
            update[i] = current;
        }
        
        int newLevel = randomLevel();
        if (newLevel > currentLevel) {
            for (int i = currentLevel + 1; i <= newLevel; i++) {
                update[i] = head;
            }
            currentLevel = newLevel;
        }
        
        SkipListNode newNode = new SkipListNode(num, newLevel);
        for (int i = 0; i <= newLevel; i++) {
            newNode.forward[i] = update[i].forward[i];
            update[i].forward[i] = newNode;
        }
    }
    
    public boolean erase(int num) {
        SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
        SkipListNode current = head;
        
        for (int i = currentLevel; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].value < num) {
                current = current.forward[i];
            }
            update[i] = current;
        }
        
        current = current.forward[0];
        if (current == null || current.value != num) {
            return false;
        }
        
        for (int i = 0; i <= currentLevel; i++) {
            if (update[i].forward[i] != current) {
                break;
            }
            update[i].forward[i] = current.forward[i];
        }
        
        while (currentLevel > 0 && head.forward[currentLevel] == null) {
            currentLevel--;
        }
        return true;
    }
    
    private int randomLevel() {
        int level = 0;
        while (random.nextDouble() < P_FACTOR && level < MAX_LEVEL) {
            level++;
        }
        return level;
    }
}

六、重要注意事项

  1. 空间复杂度:平均O(n),最坏O(n log n) - 比普通链表多约50%空间
  2. 时间复杂度
    • 查找/插入/删除:平均O(log n),最坏O(n)
    • 优于普通链表(O(n)),接近平衡树(O(log n))
  3. 随机性影响:性能依赖于随机层数生成,可能偶尔出现不平衡情况
  4. 层数限制:设置最大层数(MAX_LEVEL)防止内存过度使用
  5. 并发安全:标准库中的ConcurrentSkipListMap使用CAS操作实现线程安全
  6. 概率因子:P_FACTOR(通常0.5)影响层级分布和性能平衡

七、与平衡树的区别

特性跳表平衡树(红黑树)
实现复杂度简单(约200行代码)复杂(约500+行代码)
插入/删除只需局部修改指针需要旋转和重新着色
范围查询更高效(顺序访问)需要额外操作
内存占用略高(多层级指针)较低(每个节点两个指针)
并发实现更简单(使用CAS操作)较复杂
随机性依赖概率分布确定性算法

八、总结

  1. 跳表通过多层链表结构随机层级分配实现了高效的有序集合操作
  2. 解决了有序链表查询效率低的问题,时间复杂度达到平均O(log n)
  3. Java标准库中的ConcurrentSkipListMap/Set基于跳表实现,适合并发场景
  4. 相比平衡树,跳表实现更简单但空间开销略大
  5. 关键设计点包括:随机层数生成、多层指针维护和更新路径记录
  6. 在内存数据库、高效有序集合和并发编程中有广泛应用

跳表通过巧妙的概率设计和多层结构,在简单实现和高效性能之间取得了良好平衡,是每个Java开发者值得掌握的进阶数据结构。