跳表(Skip List)是一种随机化的数据结构,基于并联的有序链表,实现了类似平衡树的高效搜索性能,但更简单易实现。本文将深入介绍跳表的原理、特性及其实现方法。
普通有序链表的查找需要O(n)时间复杂度,跳表通过建立多层索引来加速查找:
Level 3: -∞ -----------------------> ∞
Level 2: -∞ ----------> 30 --------> ∞
Level 1: -∞ ----> 20 --> 30 -------> ∞
Level 0: -∞ -> 10 -> 20 -> 30 -> 40 -> ∞
这种结构使得查找过程类似于二分查找,能够跳过许多不必要的节点。
每个跳表节点包含以下成员:
typedef struct skiplistNode {
// 节点值
void *obj;
// 分值,用于排序
double score;
// 后向指针
struct skiplistNode *backward;
// 层级结构
struct skiplistLevel {
// 前向指针
struct skiplistNode *forward;
// 跨度
unsigned long span;
} level[];
} skiplistNode;
跳表的查找从最高层开始,逐层向下:
查找23的过程:
1. 从最高层Level 3的-∞开始
2. 向右移动,发现没有节点,降至Level 2
3. 在Level 2发现30 > 23,降至Level 1
4. 在Level 1找到20,向右移动,遇到30 > 23,降至Level 0
5. 在Level 0,从20向右移动,找到30 > 23
6. 由于未找到23,且已到达最底层,确认23不在跳表中
查找的时间复杂度为O(log n)。
插入节点时,需要:
- 确定插入位置(类似查找)
- 随机生成节点高度
- 更新前向、后向指针及跨度
def randomLevel():
level = 1
# SKIPLIST_P = 0.25
while random() < SKIPLIST_P and level < MAX_LEVEL:
level += 1
return level
随机层数的设计保证了跳表期望空间复杂度为O(n)。
删除节点时,需要:
- 找到目标节点
- 更新相关指针和跨度
- 释放节点内存
操作 | 平均时间复杂度 | 最坏时间复杂度 |
---|
查找 | O(log n) | O(n) |
插入 | O(log n) | O(n) |
删除 | O(log n) | O(n) |
空间复杂度 | O(n) | O(n log n) |
优势:
- 实现简单,代码量小
- 插入/删除只需修改相邻节点指针,不需要复杂的旋转操作
- 更适合并发环境(修改局部性好)
- 平均性能与平衡树相当
劣势:
- 空间占用稍大
- 范围查询不如B+树高效
- 随机化可能导致性能波动
Redis的有序集合(Zset)主要由跳表和哈希表组成:
- 跳表:保证元素有序性,支持范围操作
- 哈希表:提供O(1)的成员查找
ZADD leaderboard 89 "张三"
ZADD leaderboard 78 "李四"
ZADD leaderboard 95 "王五"
// 获取分数前两名
ZREVRANGE leaderboard 0 1
// 结果: 1) "王五" 2) "张三"
Google的LevelDB使用跳表作为内存表(MemTable)的实现,用于存储最近写入的键值对。
package main
import (
"fmt"
"math/rand"
)
const (
MaxLevel = 16 // 最大层数
Probability = 0.25 // 层级提升概率
)
// 跳表节点
type Node struct {
key int // 键
value interface{} // 值
forward []*Node // 前向指针
}
// 跳表
type SkipList struct {
header *Node // 头节点
level int // 当前最大层数
length int // 元素个数
}
// 创建跳表
func NewSkipList() *SkipList {
return &SkipList{
header: &Node{forward: make([]*Node, MaxLevel)},
level: 1,
}
}
// 随机层数
func randomLevel() int {
level := 1
for rand.Float64() < Probability && level < MaxLevel {
level++
}
return level
}
// 查找元素
func (sl *SkipList) Search(key int) interface{} {
x := sl.header
// 从最高层开始查找
for i := sl.level - 1; i >= 0; i-- {
// 找到第i层小于目标key的最大节点
for x.forward[i] != nil && x.forward[i].key < key {
x = x.forward[i]
}
}
// 检查是否找到
x = x.forward[0]
if x != nil && x.key == key {
return x.value
}
return nil
}
// 插入元素
func (sl *SkipList) Insert(key int, value interface{}) {
update := make([]*Node, MaxLevel)
x := sl.header
// 找到每一层的插入位置
for i := sl.level - 1; i >= 0; i-- {
for x.forward[i] != nil && x.forward[i].key < key {
x = x.forward[i]
}
update[i] = x
}
// 到达第0层,检查key是否已存在
x = x.forward[0]
if x != nil && x.key == key {
x.value = value // 更新已有值
return
}
// 获取随机层数
level := randomLevel()
if level > sl.level {
for i := sl.level; i < level; i++ {
update[i] = sl.header
}
sl.level = level
}
// 创建新节点
newNode := &Node{
key: key,
value: value,
forward: make([]*Node, level),
}
// 更新前向指针
for i := 0; i < level; i++ {
newNode.forward[i] = update[i].forward[i]
update[i].forward[i] = newNode
}
sl.length++
}
跳表是一种兼具简单性和高效性的数据结构,其随机化的特性使其在期望上能达到与平衡树相当的性能,同时具有实现简单、易于维护的优势。跳表在Redis、LevelDB等系统中的成功应用,证明了其在实际工程中的价值。
对于需要高效有序数据结构且希望避免平衡树复杂性的场景,跳表是一个优秀的选择。它不仅提供了O(log n)的查找效率,还支持高效的插入和删除操作,使其成为构建高性能系统的有力工具。