跳表
算法概述
【跳表】(Skip List)是一种随机化的数据结构,它使用多层链表实现,支持快速的查找、插入和删除操作,平均时间复杂度为O(log n)。跳表是有序链表的一种优化版本,通过随机建立"快速通道",使得搜索过程可以跳过许多元素,从而达到对数级别的时间复杂度。
跳表的核心思想是:通过在有序链表的基础上添加多级索引,形成一种"多层链表"结构,从而在保持链表插入删除操作简单性的同时,也获得了类似二分查找的高效搜索能力。
基本结构
跳表由多层有序链表组成:
- 最底层(Level 0)是一个包含所有元素的有序链表
- 上层(Level 1, 2, 3, ...)是下层链表的子集,作为索引加速查找
- 每一层的元素个数大约是下一层的1/2(通过随机决定)
跳表的节点结构
struct SkipListNode {
int key; // 节点值
int value; // 关联值(如果需要)
vector<SkipListNode*> forward; // 指向各层的下一个节点
SkipListNode(int k, int v, int level) : key(k), value(v) {
forward.resize(level + 1, nullptr);
}
};
跳表的基本操作
跳表构造与初始化
class SkipList {
private:
int maxLevel; // 最大层数
float probability; // 上升到下一层的概率
SkipListNode* header; // 头节点
int level; // 当前最高层数
public:
SkipList(int maxLevel = 16, float p = 0.5) {
this->maxLevel = maxLevel;
probability = p;
level = 0;
// 头节点不存储实际元素,仅作为入口
header = new SkipListNode(-1, -1, maxLevel);
}
// 其他方法...
};
随机层数生成
跳表的一个关键操作是决定新插入节点的层数,这通常使用随机化方法:
int SkipList::randomLevel() {
int lvl = 0;
// 以概率p(通常为0.5)向上生成层数,直到达到最大层数或随机数不满足条件
while (rand() < probability * RAND_MAX && lvl < maxLevel) {
lvl++;
}
return lvl;
}
查找操作
跳表的查找从最高层开始,当遇到大于目标的节点时下降一层,否则在当前层向前移动:
SkipListNode* SkipList::search(int key) {
SkipListNode* current = header;
// 从最高层开始查找
for (int i = level; i >= 0; i--) {
// 在当前层向前移动,直到下一个节点大于等于目标
while (current->forward[i] && current->forward[i]->key < key) {
current = current->forward[i];
}
}
// 已经到达最底层,检查下一个节点是否为目标
current = current->forward[0];
// 如果找到目标节点,返回它
if (current && current->key == key) {
return current;
}
// 未找到目标节点
return nullptr;
}
插入操作
插入操作需要先找到合适的位置,然后随机决定新节点的层数,并更新相应的指针:
void SkipList::insert(int key, int value) {
// 记录每一层的前驱节点
vector<SkipListNode*> update(maxLevel + 1, nullptr);
SkipListNode* current = header;
// 从最高层开始查找插入位置的前驱节点
for (int i = level; i >= 0; i--) {
while (current->forward[i] && current->forward[i]->key < key) {
current = current->forward[i];
}
update[i] = current;
}
// 移至最底层的下一个节点
current = current->forward[0];
// 键已存在,更新值
if (current && current->key == key) {
current->value = value;
return;
}
// 生成随机层数
int newLevel = randomLevel();
// 如果新层数高于当前最高层,更新header的forward指针
if (newLevel > level) {
for (int i = level + 1; i <= newLevel; i++) {
update[i] = header;
}
level = newLevel;
}
// 创建新节点
SkipListNode* newNode = new SkipListNode(key, value, newLevel);
// 插入新节点,更新指针
for (int i = 0; i <= newLevel; i++) {
newNode->forward[i] = update[i]->forward[i];
update[i]->forward[i] = newNode;
}
}
删除操作
删除操作先找到目标节点,然后更新所有指向它的指针:
bool SkipList::remove(int key) {
// 记录每一层的前驱节点
vector<SkipListNode*> update(maxLevel + 1, nullptr);
SkipListNode* current = header;
// 从最高层开始查找删除位置的前驱节点
for (int i = level; i >= 0; i--) {
while (current->forward[i] && current->forward[i]->key < key) {
current = current->forward[i];
}
update[i] = current;
}
// 移至最底层的下一个节点
current = current->forward[0];
// 键不存在,无法删除
if (!current || current->key != key) {
return false;
}
// 更新指针,跳过要删除的节点
for (int i = 0; i <= level; i++) {
if (update[i]->forward[i] != current) {
break;
}
update[i]->forward[i] = current->forward[i];
}
// 删除节点
delete current;
// 更新跳表的最高层数(如果需要)
while (level > 0 && header->forward[level] == nullptr) {
level--;
}
return true;
}
跳表的复杂度分析
时间复杂度
- 查找:平均O(log n),最坏O(n)
- 插入:平均O(log n),最坏O(n)
- 删除:平均O(log n),最坏O(n)
空间复杂度
跳表的空间复杂度为O(n),但常数因子比平衡树大,因为每个节点需要存储指向不同层的指针。平均而言,一个n个元素的跳表需要约2n个指针。
与平衡树的比较
特性 | 跳表 | 平衡树(如AVL/红黑树) |
---|---|---|
查找时间 | O(log n)平均 | O(log n)最坏 |
插入时间 | O(log n)平均 | O(log n)最坏 |
删除时间 | O(log n)平均 | O(log n)最坏 |
实现复杂度 | 简单 | 复杂 |
平衡维护 | 随机化,无需显式平衡 | 需要旋转等复杂操作 |
并发性能 | 优秀(局部修改) | 一般(可能需要全局重平衡) |
内存使用 | 较高 | 适中 |
跳表在ACM竞赛中的应用
在ACM竞赛中,跳表主要用于以下场景:
- 有序数据集合维护:当需要一个支持高效查找、插入和删除的有序集合时
- 排名查询:可以扩展跳表支持查找第k小元素或确定元素排名
- 区间操作:跳表的有序特性使其适合处理区间查询和修改
- 替代平衡树:当红黑树实现复杂度过高时,跳表是一个简单的替代选择
跳表的优化与扩展
1. 跳表的内存优化
在ACM竞赛环境中,可以使用数组池代替动态内存分配来提高效率:
// 节点池优化
const int MAXN = 100005;
SkipListNode nodePool[MAXN];
int nodeCount = 0;
SkipListNode* createNode(int key, int value, int level) {
nodePool[nodeCount].key = key;
nodePool[nodeCount].value = value;
nodePool[nodeCount].forward.resize(level + 1, nullptr);
return &nodePool[nodeCount++];
}
2. 支持排名查询的跳表
可以在节点中添加span字段来记录跨度,用于支持排名查询:
struct RankedSkipListNode {
int key;
int value;
vector<RankedSkipListNode*> forward;
vector<int> span; // 在每一层,到forward[i]的跨度
RankedSkipListNode(int k, int v, int level) : key(k), value(v) {
forward.resize(level + 1, nullptr);
span.resize(level + 1, 0);
}
};
// 查找排名为rank的元素
RankedSkipListNode* findByRank(int rank) {
if (rank <= 0) return nullptr;
RankedSkipListNode* current = header;
int accumulated = 0;
for (int i = level; i >= 0; i--) {
while (current->forward[i] && accumulated + current->span[i] < rank) {
accumulated += current->span[i];
current = current->forward[i];
}
}
// 现在accumulated + current->span[0]应该等于rank
if (current->forward[0]) {
return current->forward[0];
}
return nullptr; // rank超出范围
}
3. 支持区间查询的跳表
跳表的有序特性使其自然支持区间查询:
// 查询区间[minKey, maxKey]内的所有元素
vector<SkipListNode*> rangeQuery(int minKey, int maxKey) {
vector<SkipListNode*> result;
// 找到大于等于minKey的第一个节点
SkipListNode* current = header;
for (int i = level; i >= 0; i--) {
while (current->forward[i] && current->forward[i]->key < minKey) {
current = current->forward[i];
}
}
// 移至第一个符合条件的节点
current = current->forward[0];
// 收集区间内的所有节点
while (current && current->key <= maxKey) {
result.push_back(current);
current = current->forward[0];
}
return result;
}
跳表的典型应用例题
例题:多重集合维护
问题描述: 实现一个数据结构支持以下操作:
- 插入一个数x
- 删除一个数x(如果有多个,只删除一个)
- 查询数x的排名(即集合中严格小于x的元素个数+1)
- 查询排名为k的数
- 查询x的前驱(小于x的最大元素)
- 查询x的后继(大于x的最小元素)
解决思路: 这个问题可以使用跳表解决,实现如下:
class MultiskipList {
private:
// 基础跳表实现...
// 查询元素排名
int getRank(int x) {
SkipListNode* current = header;
int rank = 0;
for (int i = level; i >= 0; i--) {
while (current->forward[i] && current->forward[i]->key < x) {
rank += current->span[i];
current = current->forward[i];
}
}
return rank + 1; // +1因为排名从1开始
}
// 查询第k小的元素
SkipListNode* getElementByRank(int k) {
if (k <= 0) return nullptr;
SkipListNode* current = header;
int accumulated = 0;
for (int i = level; i >= 0; i--) {
while (current->forward[i] && accumulated + current->span[i] < k) {
accumulated += current->span[i];
current = current->forward[i];
}
}
if (current->forward[0]) {
return current->forward[0];
}
return nullptr;
}
// 查询前驱
SkipListNode* getPredecessor(int x) {
SkipListNode* current = header;
SkipListNode* predecessor = nullptr;
for (int i = level; i >= 0; i--) {
while (current->forward[i] && current->forward[i]->key < x) {
current = current->forward[i];
}
if (current != header) {
predecessor = current;
}
}
return predecessor;
}
// 查询后继
SkipListNode* getSuccessor(int x) {
SkipListNode* current = header;
for (int i = level; i >= 0; i--) {
while (current->forward[i] && current->forward[i]->key <= x) {
current = current->forward[i];
}
}
if (current->forward[0]) {
return current->forward[0];
}
return nullptr;
}
};
跳表的实现技巧
1. 边界处理
在跳表的所有操作中,需要仔细处理边界情况:
- 空跳表
- 目标键不存在
- 操作第一个或最后一个元素
2. 随机化策略
跳表的性能很大程度上依赖于随机层数生成策略,常见方法有:
- 固定概率(如0.5或0.25)
- 基于节点数量动态调整概率
- 限制最大层数(通常取log₂n或更小)
// 一种常用的层数生成策略
int randomLevel() {
int level = 0;
while (rand() < RAND_MAX / 4 && level < maxLevel) { // 概率0.25
level++;
}
return level;
}
3. 避免使用STL
在ACM竞赛中,为了最大化性能,可以避免使用STL vector,而是使用固定大小的数组:
struct OptimizedNode {
int key;
int value;
OptimizedNode* forward[MAX_LEVEL + 1];
int span[MAX_LEVEL + 1];
OptimizedNode(int k, int v) : key(k), value(v) {
memset(forward, 0, sizeof(forward));
memset(span, 0, sizeof(span));
}
};
总结
跳表是一种随机化的数据结构,它提供了与平衡树相近的性能,但实现更为简单。通过多层链表结构和随机化技术,跳表在保持链表操作简单性的同时,实现了对数级别的查找时间复杂度。
跳表的主要优势在于:
- 实现简单,代码量小于平衡树
- 插入和删除操作较为局部化,不需要全局重平衡
- 空间利用效率相对较高
- 随机化特性使其在大多数情况下表现良好
在ACM竞赛中,当需要一个有序数据结构但不想实现复杂的平衡树时,跳表是一个非常好的选择。同时,跳表也可以扩展以支持排名查询、区间操作等高级功能,适用于各种复杂的问题场景。