什么是跳表
1. 什么是跳表?
直观理解
想象一下你在查阅一本很厚的词典:
-
最笨的方法:从第一页开始一页一页翻找 → O(n)
-
聪明的方法:先看目录,找到大概范围,再细找 → 这就是跳表的思想
跳表就是一种支持快速查找的有序链表,通过建立多级索引来加速查找过程。
2. 跳表的基本结构
2.1 普通的有序链表
text
1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15
查找时间复杂度:O(n)
2.2 添加第一级索引
text
第一级索引:1 ------> 5 ------> 9 ------> 13| | | | 底层链表: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15
2.3 添加第二级索引
text
第二级索引:1 ---------------> 9| | 第一级索引:1 ------> 5 ------> 9 ------> 13| | | | 底层链表: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15
3. 跳表的核心思想
3.1 空间换时间
-
额外空间:存储多级索引
-
时间收益:查找效率从 O(n) 提升到 O(log n)
3.2 概率平衡
跳表不像平衡树那样需要严格的旋转操作,而是通过随机化来决定节点的高度。
4. 跳表的详细实现
4.1 节点结构
cpp
struct SkipListNode {int value;vector<SkipListNode*> next; // 每层的下一个指针vector<SkipListNode*> prev; // 双向跳表需要,单向可以省略SkipListNode(int val, int level) : value(val), next(level, nullptr) {}
};
4.2 跳表类框架
cpp
class SkipList {
private:SkipListNode* head; // 头节点,有最大层数int maxLevel; // 最大层数int currentLevel; // 当前有效层数float probability; // 晋升概率,通常为0.5// 随机生成节点层数int randomLevel() {int level = 1;while ((rand() / (float)RAND_MAX) < probability && level < maxLevel) {level++;}return level;}public:SkipList(int maxLvl = 16, float p = 0.5) : maxLevel(maxLvl), probability(p), currentLevel(1) {head = new SkipListNode(INT_MIN, maxLevel); // 头节点值为最小值}// 查找、插入、删除等方法...
};
5. 核心操作详解
5.1 查找操作
查找值为 target 的节点:
cpp
bool search(int target) {SkipListNode* current = head;// 从最高层开始查找for (int i = currentLevel - 1; i >= 0; i--) {// 在当前层向前移动,直到下一个节点值大于等于targetwhile (current->next[i] != nullptr && current->next[i]->value < target) {current = current->next[i];}}// 现在current是底层中小于target的最大节点current = current->next[0];return current != nullptr && current->value == target;
}
查找过程示例(查找11):
text
第2层:1 ---------------> 9 (11>9,跳到9)↓ 第1层:9 ------> 13 (11<13,下降)↓ 第0层:9 -> 11 (找到!)
5.2 插入操作
cpp
void insert(int value) {vector<SkipListNode*> update(maxLevel, nullptr);SkipListNode* current = head;// 1. 找到每层中待插入位置的前驱节点for (int i = currentLevel - 1; i >= 0; i--) {while (current->next[i] != nullptr && current->next[i]->value < value) {current = current->next[i];}update[i] = current; // 记录每层的前驱节点}// 2. 随机决定新节点的层数int newLevel = randomLevel();if (newLevel > currentLevel) {for (int i = currentLevel; i < newLevel; i++) {update[i] = head;}currentLevel = newLevel;}// 3. 创建新节点并插入到各层SkipListNode* newNode = new SkipListNode(value, newLevel);for (int i = 0; i < newLevel; i++) {newNode->next[i] = update[i]->next[i];update[i]->next[i] = newNode;}
}
插入过程图示(插入8,随机到2层):
text
插入前: 第1层:1 ------> 5 ------> 9| | | 第0层:1 -> 3 -> 5 -> 7 -> 9插入后: 第1层:1 ------> 5 -> 8 -> 9| | | | 第0层:1 -> 3 -> 5 -> 7 -> 8 -> 9
5.3 删除操作
cpp
bool remove(int value) {vector<SkipListNode*> update(maxLevel, nullptr);SkipListNode* current = head;// 1. 找到包含该节点的各层前驱for (int i = currentLevel - 1; i >= 0; i--) {while (current->next[i] != nullptr && current->next[i]->value < value) {current = current->next[i];}update[i] = current;}// 2. 检查是否存在该节点current = current->next[0];if (current == nullptr || current->value != value) {return false;}// 3. 从各层中删除该节点for (int i = 0; i < currentLevel; i++) {if (update[i]->next[i] != current) {break;}update[i]->next[i] = current->next[i];}delete current;// 4. 更新当前层数(如果最高层变空)while (currentLevel > 1 && head->next[currentLevel - 1] == nullptr) {currentLevel--;}return true;
}
6. 时间复杂度分析
6.1 查找时间复杂度
-
最坏情况:O(n) - 所有节点都在同一层
-
平均情况:O(log n) - 通过多级索引加速
数学推导:
-
第0层:n个节点
-
第1层:约n/2个节点
-
第2层:约n/4个节点
-
...
-
第k层:约n/2^k个节点
-
当n/2^k = 1时,k = log₂n
6.2 空间复杂度
-
额外空间:O(n) - 索引节点总数约为 n + n/2 + n/4 + ... ≈ 2n
7. 跳表 vs 平衡树
| 特性 | 跳表 | 平衡树(如红黑树) |
|---|---|---|
| 实现难度 | 简单 | 复杂 |
| 查找性能 | O(log n) | O(log n) |
| 插入性能 | O(log n) | O(log n) |
| 删除性能 | O(log n) | O(log n) |
| 范围查询 | 容易 | 相对复杂 |
| 内存使用 | 较多 | 较少 |
| 并发控制 | 相对容易 | 复杂 |
8. 实际应用场景
8.1 Redis中的有序集合
cpp
// Redis使用跳表实现ZSET
typedef struct zset {dict *dict; // 哈希表,用于O(1)查找zskiplist *zsl; // 跳表,用于范围查询和排序
} zset;
8.2 LevelDB/RocksDB
用于内存中的MemTable实现,支持快速的范围查询。
8.3 Lucene
在倒排索引中使用跳表进行文档ID的存储和查询。
9. 完整代码示例
cpp
#include <iostream>
#include <vector>
#include <climits>
#include <cstdlib>
#include <ctime>using namespace std;struct SkipListNode {int value;vector<SkipListNode*> next;SkipListNode(int val, int level) : value(val), next(level, nullptr) {}
};class SkipList {
private:SkipListNode* head;int maxLevel;int currentLevel;float probability;public:SkipList(int maxLvl = 16, float p = 0.5) : maxLevel(maxLvl), probability(p), currentLevel(1) {srand(time(0));head = new SkipListNode(INT_MIN, maxLevel);}int randomLevel() {int level = 1;while ((rand() / (float)RAND_MAX) < probability && level < maxLevel) {level++;}return level;}void insert(int value) {vector<SkipListNode*> update(maxLevel, nullptr);SkipListNode* current = head;for (int i = currentLevel - 1; i >= 0; i--) {while (current->next[i] != nullptr && current->next[i]->value < value) {current = current->next[i];}update[i] = current;}int newLevel = randomLevel();if (newLevel > currentLevel) {for (int i = currentLevel; i < newLevel; i++) {update[i] = head;}currentLevel = newLevel;}SkipListNode* newNode = new SkipListNode(value, newLevel);for (int i = 0; i < newLevel; i++) {newNode->next[i] = update[i]->next[i];update[i]->next[i] = newNode;}cout << "插入 " << value << ",层数: " << newLevel << endl;}bool search(int target) {SkipListNode* current = head;for (int i = currentLevel - 1; i >= 0; i--) {while (current->next[i] != nullptr && current->next[i]->value < target) {current = current->next[i];}}current = current->next[0];bool found = (current != nullptr && current->value == target);cout << "查找 " << target << ": " << (found ? "找到" : "未找到") << endl;return found;}void display() {cout << "\n跳表结构:" << endl;for (int i = currentLevel - 1; i >= 0; i--) {SkipListNode* node = head->next[i];cout << "第" << i << "层: ";while (node != nullptr) {cout << node->value << " -> ";node = node->next[i];}cout << "NULL" << endl;}}
};// 测试示例
int main() {SkipList skiplist;skiplist.insert(3);skiplist.insert(6);skiplist.insert(7);skiplist.insert(9);skiplist.insert(12);skiplist.insert(19);skiplist.insert(17);skiplist.display();skiplist.search(6);skiplist.search(15);return 0;
}
10. 总结
跳表的核心优势:
-
实现简单:比平衡树容易理解和实现
-
性能优秀:平均O(log n)的查找、插入、删除
-
支持范围查询:天然支持有序遍历
-
并发友好:更容易实现线程安全版本
跳表通过"空间换时间"和"概率平衡"的思想,在保持链表简单性的同时,获得了接近平衡树的性能,是现代系统中广泛使用的重要数据结构。
