
链表是一种线性数据结构,优势在于插入删除的时间复杂度为 O(1),但查找的时间复杂度为 O(n)。即使是有序链表,也需要逐个遍历节点,效率低下。
跳表的设计灵感来源于「多级索引」:在有序链表的基础上,建立若干层索引,每一层索引都是下一层索引的子集。最底层是原始链表,上层索引通过「跳跃」的方式快速定位目标节点,从而将查找时间复杂度降低到 O(log n)。
举个例子: 原始有序链表:1 → 3 → 5 → 7 → 9 → 11 第一层索引:1 → 5 → 9 第二层索引:1 → 9
查找节点 7 时,先在第二层索引找到 9(大于 7),然后回退到第一层索引的 5,最后在原始链表中从 5 遍历到 7,只需 3 步,而非遍历整个链表。
跳表的每个节点包含两部分:
/**
* 跳表节点
* @author ken
*/
@Data
public class SkipListNode<T> {
/**
* 节点值
*/
private T value;
/**
* 各层指针数组
*/
private SkipListNode<T>[] next;
@SuppressWarnings("unchecked")
public SkipListNode(T value, int level) {
this.value = value;
this.next = new SkipListNode[level];
}
}
跳表的层级是动态生成的,新节点插入时通过「随机算法」决定其层数(层数越高,概率越低),以保证跳表的平衡性。常见的随机算法是:
/**
* 跳表实现
* @author ken
*/
@Slf4j
publicclass SkipList<T extends Comparable<T>> {
/**
* 最大层数
*/
privatestaticfinalint MAX_LEVEL = 16;
/**
* 随机层数概率因子(50%)
*/
privatestaticfinaldouble P = 0.5;
/**
* 头节点
*/
private SkipListNode<T> head;
/**
* 当前跳表层数
*/
privateint currentLevel;
public SkipList() {
this.currentLevel = 1;
this.head = new SkipListNode<>(null, MAX_LEVEL);
}
}
/**
* 随机生成节点层数
* @return 层数
*/
private int randomLevel() {
int level = 1;
while (Math.random() < P && level < MAX_LEVEL) {
level++;
}
return level;
}
/**
* 查找节点
* @param value 目标值
* @return 目标节点(null 表示不存在)
*/
public SkipListNode<T> find(T value) {
if (ObjectUtils.isEmpty(value)) {
returnnull;
}
SkipListNode<T> current = head;
// 从最高层开始查找,逐层下降
for (int i = currentLevel - 1; i >= 0; i--) {
while (current.getNext()[i] != null && current.getNext()[i].getValue().compareTo(value) < 0) {
current = current.getNext()[i];
}
}
// 检查底层是否存在目标节点
if (current.getNext()[0] != null && current.getNext()[0].getValue().equals(value)) {
return current.getNext()[0];
}
returnnull;
}
/**
* 插入节点
* @param value 节点值
*/
public void insert(T value) {
if (ObjectUtils.isEmpty(value)) {
return;
}
// 记录每层需要更新的节点
SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL];
SkipListNode<T> current = head;
// 从高层到低层查找插入位置
for (int i = currentLevel - 1; i >= 0; i--) {
while (current.getNext()[i] != null && current.getNext()[i].getValue().compareTo(value) < 0) {
current = current.getNext()[i];
}
update[i] = current;
}
// 生成新节点层数
int newLevel = randomLevel();
// 如果新层数超过当前层数,更新头节点的指针
if (newLevel > currentLevel) {
for (int i = currentLevel; i < newLevel; i++) {
update[i] = head;
}
currentLevel = newLevel;
}
// 创建新节点并插入
SkipListNode<T> newNode = new SkipListNode<>(value, newLevel);
for (int i = 0; i < newLevel; i++) {
newNode.getNext()[i] = update[i].getNext()[i];
update[i].getNext()[i] = newNode;
}
log.info("插入节点 {},层数 {}", value, newLevel);
}
/**
* 删除节点
* @param value 节点值
*/
public void delete(T value) {
if (ObjectUtils.isEmpty(value)) {
return;
}
// 记录每层需要更新的节点
SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL];
SkipListNode<T> current = head;
// 从高层到低层查找删除位置
for (int i = currentLevel - 1; i >= 0; i--) {
while (current.getNext()[i] != null && current.getNext()[i].getValue().compareTo(value) < 0) {
current = current.getNext()[i];
}
update[i] = current;
}
// 检查是否存在目标节点
current = current.getNext()[0];
if (current == null || !current.getValue().equals(value)) {
log.info("节点 {} 不存在", value);
return;
}
// 删除节点(更新各层指针)
for (int i = 0; i < currentLevel; i++) {
if (update[i].getNext()[i] != current) {
break;
}
update[i].getNext()[i] = current.getNext()[i];
}
// 更新当前跳表层数(如果最高层无节点)
while (currentLevel > 1 && head.getNext()[currentLevel - 1] == null) {
currentLevel--;
}
log.info("删除节点 {}", value);
}
/**
* 跳表测试类
* @author ken
*/
publicclass SkipListTest {
public static void main(String[] args) {
SkipList<Integer> skipList = new SkipList<>();
// 插入节点
skipList.insert(3);
skipList.insert(1);
skipList.insert(5);
skipList.insert(7);
skipList.insert(9);
// 查找节点
SkipListNode<Integer> node = skipList.find(5);
System.out.println("查找节点 5:" + (node != null ? node.getValue() : "不存在"));
// 删除节点
skipList.delete(7);
node = skipList.find(7);
System.out.println("查找节点 7:" + (node != null ? node.getValue() : "不存在"));
}
}
运行结果:
插入节点 3,层数 1
插入节点 1,层数 2
插入节点 5,层数 1
插入节点 7,层数 3
插入节点 9,层数 1
查找节点 5:5
删除节点 7
查找节点 7:不存在
Redis 的有序集合(ZSet)底层采用跳表(+哈希表)实现,而非红黑树,原因如下:
红黑树的范围查询需要中序遍历,时间复杂度为 O(log n + k)(k 为结果数量);而跳表只需找到起始节点,然后沿底层链表遍历即可,实现更简单,效率更高。
红黑树需要维护颜色翻转、旋转等复杂规则,代码量庞大;跳表的逻辑更直观,插入、删除操作仅需调整指针,易于实现和调试。
红黑树的插入删除涉及全局结构调整,并发场景下需加锁;跳表的节点修改仅影响局部指针,可通过乐观锁或分段锁优化并发性能。
红黑树每个节点需存储颜色、父节点指针等信息,空间开销固定;跳表通过动态层数控制空间开销,可根据实际场景调整最大层数。
ZooKeeper 的数据模型是树形结构,每个节点(ZNode)维护着一个「子节点列表」。为了高效查找子节点,ZooKeeper 采用跳表存储子节点的名称(有序),支持快速的插入、删除和范围查询。
例如,当客户端调用 getChildren 时,ZooKeeper 可通过跳表的底层链表直接返回有序的子节点列表,无需额外排序。
跳表的成功,本质是「简单即高效」的设计哲学的胜利。它没有复杂的数学证明,仅通过「分层索引」和「随机平衡」,就实现了媲美红黑树的性能,同时兼具易实现、易扩展的优势。
Redis 和 ZooKeeper 对跳表的选择,正是这种工程思维的体现——用最简单的结构解决最核心的问题,这也是技术设计的终极智慧。