引言
在计算机科学中,数据结构的选择对于程序的性能至关重要。对于需要高效查找、插入和删除操作的场景,跳跃表(SkipList)是一种非常实用的数据结构。本文将详细介绍跳跃表的原理、实现和应用场景。
什么是跳表
下图是一个简单的有序单链表,假设我们需要查找10这个元素,只能从头部开始逐个遍历,无法随机访问,查找路径:1、2、3、4、5、6、7、8、9、10。这样的查找效率很低,平均时间复杂度很高O(n)。那有没有办法提高链表的查找速度呢?

如下图,我们从每5个元素抽出一个元素构建一个新的链表,新链表指向了原链表。这样我们查找数据的时候先从上层链表开始查找,那么查找10的路径为1、6、7、8、9、10。查询范围一下子缩小了一半

两层链表就把查询范围减少了一半,如果加多几层,岂不更快 ?
增加多一层链表后,查询路径为1、6、9、10,比两层更快。可以看出跳表是通过空间换时间的方式加快查询性能

跳表是一种基于有序链表 的随机化数据结构,通过多级索引 实现类似二分查找的效率。其核心思想是:用空间换时间,允许快速查询、插入和删除操作。跳表的平均时间复杂度为 O(log n),与平衡二叉树相当,但实现更简单
跳表的实现
插入操作
如下图,如果我们想把4这个元素插入到跳表中,那应该怎么操作呢?
插入元素和查找元素一样,先从最上层开始查找,找到底层原始链表应该插入的位置,跳表的原始链表需要保持有序。整个过程类似查找4原始一样,查找路径1、1、3、3。因为4大于3,小于5,4应该插入再3和5之间。

如果一直往原始链表插入数据,不更新索引(往上抽多层链表),就可能出现两个索引节点之间数据非常多,极端情况下,跳表就退化为单链表了。这种情况怎么解 ?那就要我们插入数据的时候,索引节点也要相对应的增加或者重建。

那怎么去维护这个索引呢 ?相对容易理解的方法就是每次新增节点把之前的索引删掉,再全部重新构建。这种效率是极低的,有没有更加高效的方法 ?
假设我们希望跳表每一层抽取50%元素作为上一层索引,理想的情况下第二层索引就是每隔一个元素抽取一个,第三层在第二层的基础上每隔一个元素抽取一个,以此类推。如下图所示

为了提高索引的维护效率,如果我们在原始链表中随机选择n/2 个元素作为一级索引,以此类推是否也可以呢? 虽然随机选择的元素作为索引可能是不均匀的,但是如果元素基数足够大,且抽取是随机的,那么索引整体就是均匀的,对于查询效率影响不大。
所以我们可以维护这么一个索引:随机选择n/2元素作为一级索引、n/4作为二级索引、n/8作为三级索引,以此类推,直到最顶层索引。下面就用代码来展示怎么实现
在每次插入新元素的时候,随机让元素有1/2概率在一级索引、1/4概率在二级索引、1/8概率在三级索引... 这样就能满足以上的条件。我们先实现一个随机选择索引层的方法
Java
//最大层数
private static final int MAX_LEVEL = 16;
//随机概率
private static final double P = 0.5;
private Random random = new Random();
public int randomLevel() {
int level = 1;
while (random.nextDouble() < P && level < MAX_LEVEL) {
level++;
}
return level;
}
假设我们规定最大索引层为16,有1/2的概率作为一层索引。因为 random.nextDouble() < P 每次有1/2的概率,所以level=1就是1/2, level=2 就是 1/2*1/2=1/4,以此类推。这方法符合我们的要求
Java
private static class Node<K, V> {
K key;
V value;
Node<K, V>[] forward; //指向不同层级的下一个节点
public Node(K key, V value, int level) {
this.key = key;
this.value = value;
this.forward = new Node[level];
}
}
Java
//插入
public void add(K key, V value) {
// 创建一个数组来记录每一层插入节点的前驱节点
Node<K, V>[] update = new Node[MAX_LEVEL];
// 从头节点开始
Node<K, V> current = head;
// 从最高层开始向下逐层查找插入位置
for (int i = level - 1; i >= 0; i--) {
// 沿着当前层的链表移动,直到找到最后一个小于目标键的节点
while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
current = current.forward[i];
}
// 记录当前层的前驱节点
update[i] = current;
}
// 检查目标键是否已存在
if (current.forward[0] != null && current.forward[0].key.compareTo(key) == 0) {
// 如果存在,更新其值
current.forward[0].value = value;
return;
}
// 随机确定新节点的层级
int randomLevel = randomLevel();
// 如果新节点的层级超过了当前最高层级,则更新前驱节点数组
if (randomLevel > level) {
for (int i = level; i < randomLevel; i++) {
update[i] = head;
}
// 更新当前最高层级
level = randomLevel;
}
// 创建新节点
Node<K, V> newNode = new Node<>(key, value, randomLevel);
// 更新每一层的指针,将新节点插入到跳跃表中
for (int i = 0; i < randomLevel; i++) {
// 新节点的指针指向下一个节点
newNode.forward[i] = update[i].forward[i];
// 前驱节点的指针指向新节点
update[i].forward[i] = newNode;
}
}
查找
Java
public V get(K key) {
// 从头节点开始
Node<K, V> current = head;
// 从最高层开始向下逐层查找
for (int i = level - 1; i >= 0; i--) {
// 沿着当前层的链表移动,直到找到最后一个小于目标键的节点
while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
current = current.forward[i];
}
}
// 这时current已经在最底层原始链表,检查目标键是否存在于最底层
if (current.forward[0] != null && current.forward[0].key.compareTo(key) == 0) {
// 如果存在,返回对应的值
return current.forward[0].value;
}
// 如果不存在,返回 null
return null;
}
删除
Java
public void remove(K key) {
// 创建一个数组来记录每一层的前驱节点
Node<K, V>[] update = new Node[MAX_LEVEL];
// 从头节点开始
Node<K, V> current = head;
// 从最高层开始向下逐层查找目标键的前驱节点
for (int i = level; i >= 0; i--) {
// 沿着当前层的链表移动,直到找到最后一个小于目标键的节点
while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
current = current.forward[i];
}
// 记录当前层的前驱节点
update[i] = current;
}
// 遍历每一层,检查目标键是否存在并更新指针
for (int i = 0; i < level; i++) {
// 检查当前层的前驱节点的下一个节点是否为目标键
if (update[i].forward[i] != null && update[i].forward[i].key.compareTo(key) == 0) {
// 获取要删除的节点
Node<K, V> deleteNode = update[i].forward[i];
// 更新前驱节点的指针,跳过删除的节点
update[i].forward[i] = deleteNode.forward[i];
// 断开删除节点的指针
deleteNode.forward[i] = null;
}
}
// 更新当前最高层级
// 如果最高层的指针为空,则减少层级
while (level > 0 && head.forward[level] == null) {
level--;
}
}
完整代码
Java
package com.test.lsmtree;
import java.util.Random;
/**
* @author Yang WenJie
* @date 2025/4/7 下午5:23
*/
public class SkipList<K extends Comparable,V> {
//最大层数
private static final int MAX_LEVEL = 16;
//随机概率
private static final double P = 0.5;
private Random random;
private Node head;
private int level;
public SkipList() {
this.head = new Node(null, null, MAX_LEVEL);
this.level = 0;
this.random = new Random();
}
public int randomLevel() {
int level = 1;
while (random.nextDouble() < P && level < MAX_LEVEL) {
level++;
}
return level;
}
public void add(K key, V value) {
Node<K,V>[] update = new Node[MAX_LEVEL];
Node<K,V> current = head;
for (int i = level - 1; i >= 0; i--) {
while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
current = current.forward[i];
}
update[i] = current;
}
if (current.forward[0] != null && current.forward[0].key.compareTo(key) == 0) {
current.forward[0].value = value;
return;
}
int randomLevel = randomLevel();
if (randomLevel > level) {
for (int i = level; i < randomLevel; i++) {
update[i] = head;
}
level = randomLevel;
}
Node<K,V> newNode = new Node<>(key, value, randomLevel);
for (int i = 0; i < randomLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
}
public V get(K key) {
Node<K,V> current = head;
for (int i = level - 1; i >= 0; i--) {
while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
current = current.forward[i];
}
}
if (current.forward[0] != null && current.forward[0].key.compareTo(key) == 0) {
return current.forward[0].value;
}
return null;
}
public void remove(K key) {
Node<K,V>[] update = new Node[MAX_LEVEL];
Node<K,V> current = head;
for (int i = level; i >=0; i--) {
while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
current = current.forward[i];
}
update[i] = current;
}
for (int i = 0; i < level; i++) {
if (update[i].forward[i] != null && update[i].forward[i].key.compareTo(key) == 0) {
Node<K, V> deleteNode = update[i].forward[i];
update[i].forward[i] = deleteNode.forward[i];
deleteNode.forward[i] = null; // 断开引用
}
}
while (level > 0 && head.forward[level] == null) {
level--;
}
}
public void printAllLevels() {
System.out.println("Skip List Levels:"+ level);
for (int i = level - 1; i >=0 ; i--) {
Node<K,V> current = head.forward[i];
System.out.print("Level " + i + ": ");
while (current != null) {
System.out.print(current.key + " ");
current = current.forward[i];
}
System.out.println();
}
}
private static class Node<K,V> {
K key;
V value;
Node<K,V>[] forward;
public Node(K key, V value, int level) {
this.key = key;
this.value = value;
this.forward = new Node[level];
}
}
public static void main(String[] args) {
SkipList<Integer, String> skipList = new SkipList<>();
skipList.add(1, "one");
skipList.add(2, "two");
skipList.add(3, "three");
skipList.add(4, "four");
skipList.add(5, "five");
skipList.add(6, "six");
skipList.add(7, "seven");
skipList.add(8, "eight");
skipList.add(9, "nine");
skipList.add(2, "two updated"); // 更新键为2的值
skipList.printAllLevels(); // 打印每一层的内容
System.out.println(skipList.get(1)); // 输出: one
System.out.println(skipList.get(2)); // 输出: two
System.out.println(skipList.get(3)); // 输出: three
skipList.remove(4);
System.out.println(skipList.get(4)); // 输出: null
}
}
跳表的应用
跳跃表在数据库和缓存系统中被广泛使用,特别是在需要高效查找和动态更新的场景中。例如:
- LevelDB:Google 的 LevelDB 使用跳跃表作为内存表(MemTable)的实现,用于存储最近插入的数据。跳跃表的高效插入和查找能力使其成为理想的内存存储结构。
- Redis:Redis 的有序集合(Sorted Set)使用跳跃表作为底层数据结构,支持高效的范围查询和动态更新。
本文由博客一文多发平台 OpenWrite 发布!