跳表(Skip List)是一种由William Pugh于1990年提出的概率性数据结构,它通过在有序链表之上构建多层索引,实现了与平衡二叉搜索树相当的平均时间复杂度——查找、插入、删除均为O(log n)。跳表的核心思想简单却优雅:以概率p决定节点是否晋升到更高层,从而形成金字塔式的多层结构,使得搜索时可以”跳跃”前进,避免逐一遍历。
核心原理与概率分析
跳表的基础是一个有序链表。在此基础上,每个节点有概率p(通常取0.5)拥有一条指向更高层的指针。最底层包含所有元素,向上逐层节点数量呈指数级递减。理想情况下,第k层约有n·p^k个节点。
搜索过程从顶层头节点开始,向右比较直到遇到大于目标值的节点,然后下降到下一层继续搜索,如此反复直到最底层。由于每层可以”跳过”大量节点,期望搜索步数为O(log n)。
Java完整实现
import java.util.Random;
/**
* 跳表实现 - 概率性多层索引数据结构
* 支持有序集合的快速查找、插入、删除操作
* @param <T> 元素类型,需实现Comparable接口
*/
public class SkipList<T extends Comparable<T>> {
/** 最大层数上限,防止极端情况下层数过高 */
private static final int MAX_LEVEL = 16;
/** 节点晋升到更高层的概率,通常取0.5 */
private static final double P = 0.5;
/** 随机数生成器,用于决定节点层数 */
private final Random random;
/** 当前跳表的实际最大层数 */
private int level;
/** 头节点,不存储实际数据,仅作为多层索引起点 */
private final Node<T> head;
/** 当前跳表中元素总数 */
private int size;
/**
* 跳表节点内部类
* 每个节点包含数据值和指向各层下一个节点的指针数组
*/
private static class Node<T> {
T value;
/** forward[i]表示第i层的下一个节点 */
Node<T>[] forward;
@SuppressWarnings("unchecked")
Node(T value, int level) {
this.value = value;
// 数组大小为level+1,索引0对应最底层
this.forward = new Node[level + 1];
}
}
public SkipList() {
this.random = new Random();
// 头节点初始化为最大层数,方便后续扩展
this.head = new Node<>(null, MAX_LEVEL);
this.level = 0;
this.size = 0;
}
/**
* 通过随机抛硬币决定新节点的层数
* 以概率P递增层数,直到概率判定失败或达到最大层数
* @return 新节点应拥有的层数(从0开始计数)
*/
private int randomLevel() {
int lvl = 0;
// 当随机数小于P且未达到最大层数时,继续晋升
while (random.nextDouble() < P && lvl < MAX_LEVEL - 1) {
lvl++;
}
return lvl;
}
/**
* 查找目标值是否在跳表中
* 时间复杂度:平均O(log n),最坏O(n)
* @param target 待查找的目标值
* @return 存在返回true,否则false
*/
public boolean contains(T target) {
Node<T> current = head;
// 从当前最高层开始,逐层向下搜索
for (int i = level; i >= 0; i--) {
// 在当前层向右移动,直到下一个节点值大于等于目标值
while (current.forward[i] != null &&
current.forward[i].value.compareTo(target) < 0) {
current = current.forward[i];
}
}
// 到达最底层,检查下一个节点是否等于目标值
current = current.forward[0];
return current != null && current.value.compareTo(target) == 0;
}
/**
* 向跳表中插入新元素
* 若元素已存在则不重复插入
* 时间复杂度:平均O(log n)
* @param value 待插入的值
* @return 插入成功返回true,已存在返回false
*/
public boolean insert(T value) {
// update数组记录各层中待插入位置的前驱节点
@SuppressWarnings("unchecked")
Node<T>[] update = new Node[MAX_LEVEL];
Node<T> current = head;
// 第一阶段:查找各层插入位置的前驱节点
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null &&
current.forward[i].value.compareTo(value) < 0) {
current = current.forward[i];
}
update[i] = current;
}
// 检查最底层下一个节点是否已存在该值
current = current.forward[0];
if (current != null && current.value.compareTo(value) == 0) {
return false; // 元素已存在,不重复插入
}
// 第二阶段:生成随机层数并插入新节点
int newLevel = randomLevel();
// 若新节点层数超过当前跳表层数,更新头节点对应层的前驱
if (newLevel > level) {
for (int i = level + 1; i <= newLevel; i++) {
update[i] = head;
}
level = newLevel;
}
Node<T> newNode = new Node<>(value, newLevel);
// 在各层中插入新节点:调整指针连接
for (int i = 0; i <= newLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
size++;
return true;
}
/**
* 从跳表中删除指定元素
* 时间复杂度:平均O(log n)
* @param value 待删除的值
* @return 删除成功返回true,不存在返回false
*/
public boolean delete(T value) {
@SuppressWarnings("unchecked")
Node<T>[] update = new Node[MAX_LEVEL];
Node<T> current = head;
// 查找待删除节点在各层的前驱节点
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null &&
current.forward[i].value.compareTo(value) < 0) {
current = current.forward[i];
}
update[i] = current;
}
// 定位到待删除节点
current = current.forward[0];
if (current == null || current.value.compareTo(value) != 0) {
return false; // 元素不存在
}
// 从各层中移除该节点
for (int i = 0; i <= level; i++) {
if (update[i].forward[i] != current) {
break; // 更高层不存在该节点
}
update[i].forward[i] = current.forward[i];
}
// 若最高层变为空,则降低当前层数
while (level > 0 && head.forward[level] == null) {
level--;
}
size--;
return true;
}
/**
* 获取跳表中元素总数
*/
public int size() {
return size;
}
/**
* 打印跳表结构,用于调试和可视化
*/
public void display() {
System.out.println("===== Skip List Structure =====");
for (int i = level; i >= 0; i--) {
Node<T> node = head.forward[i];
System.out.print("Level " + i + ": head");
while (node != null) {
System.out.print(" -> " + node.value);
node = node.forward[i];
}
System.out.println(" -> null");
}
System.out.println("===============================");
}
/**
* 主方法:演示跳表的基本操作
*/
public static void main(String[] args) {
SkipList<Integer> skipList = new SkipList<>();
int[] data = {3, 6, 7, 9, 12, 19, 17, 26, 21, 25};
System.out.println("插入元素: " + java.util.Arrays.toString(data));
for (int num : data) {
skipList.insert(num);
}
skipList.display();
System.out.println("\n查找测试:");
System.out.println("contains(12): " + skipList.contains(12));
System.out.println("contains(15): " + skipList.contains(15));
System.out.println("\n删除元素 12");
skipList.delete(12);
System.out.println("contains(12) after delete: " + skipList.contains(12));
skipList.display();
System.out.println("\n当前元素总数: " + skipList.size());
}
}
复杂度分析
| 操作 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 查找 | O(log n) | O(n) | – |
| 插入 | O(log n) | O(n) | O(n) |
| 删除 | O(log n) | O(n) | – |
空间复杂度为O(n),因为每个节点期望拥有 1/(1-p) 个指针。当p=0.5时,期望指针数为2,即额外空间开销约为2n。
应用场景
跳表在工业界有广泛应用:Redis的有序集合(Sorted Set)底层使用跳表实现;LevelDB和RocksDB的MemTable也采用跳表作为内存数据结构。相较于红黑树,跳表的实现更为简洁,且支持高效的区间查询和并发操作。