
🔥keyipatience:个人主页
🎬作者简介:C/C++后端开发学习者
🌟专栏传送门:《c++》《linux》《c++高阶数据结构》《c++数据结构与算法》
⭐️patience is key in life

基础背景
普通有序单向链表 :查找需要逐个遍历,时间复杂度O(n)。 跳表(skiplist)是在有序链表基础上,多层索引链表实现的结构。
William Pugh 的原始优化思路
- 第一层索引:每 2 个底层节点,向上层提一个节点建立指针。上层链表节点数只有底层一半,查找比较次数减半。
- 多层迭代:在上一层链表继续隔二取一,继续向上建立更高层索引,层层叠加。
- 理想情况 :每层节点数是下层的一半,查找类似二分查找,时间复杂度 O(log n)

致命缺陷:插入 / 删除节点会打破严格 2:1 的上下层比例;要维持这个比例,需要改动后面大量节点,复杂度退化成O(n)
跳表 SkipList 效率保证原理
不再强制维持 2:1 固定比例!
插入新节点时,随机生成这个节点的层数 。 插入、删除操作只需要修改当前节点相关的指针,不需要改动其他节点的层数,操作简单高效。
随机层数生成算法 randomLevel()
randomLevel()
lvl := 1
while random() < p and lvl < MaxLevel do
lvl := lvl + 1
return lvl
逻辑:
- 新节点初始层数 lvl=1(必然至少在底层链表)
- 循环:每次以概率
p尝试再往上增加一层 ;达到最大层数maxLevel就停止 - Redis 跳表参数p=1/4,maxLevel=32:规律:层数越高,生成概率越小
- 层数恰好 =1:概率 (1-p)
- 层数恰好 =2:概率 (p(1-p))
- 层数恰好 =3:概率 (p^2(1-p))
- 层数恰好 =k:概率 (p^(k-1)(1-p))
节点平均层数(平均指针数量)推导--就是数学期望的计算
因此,一个节点的平均层数(也即包含的平均指针数目),计算如下,概率*层数:
平均层数 = 1×(1-p)+2p (1-p)+3p²(1-p)+4p³(1-p)+... = (1-p) × ∑(k=1 到∞) k・p^(k-1) = 1/(1-p)
代入计算:p=1/2,平均层数 = 1/(1-1/2)=2,每个节点平均 2 个指针 ,p=1/4,平均层数 = 1/(1-1/4)≈1.33,Redis 采用这个,节省内存
如何保证查找,插入,删除平均 O (log n)
1.概率上理解**,高层节点稀疏,低层节点稠密。** 每层节点期望数量是下一层的 p 倍。比如 p=1/4:第 1 层 n 个,第 2 层期望 np,第 3 层 n p²...... 高层索引数量快速衰减。这个概率分布等效于理想跳表的 2:1 分层,只是用随机代替强制固定间隔。
2.推导
纵向(逐层下沉):
跳表插入节点时,每往上一层的成功概率是 p:
第 0 层:n 个节点
第 1 层索引:期望节点数 n・p
第 2 层索引:期望节点数 n・p²
第 k 层索引:期望节点数 n・p^k
当高层节点数量降到 1 个 的时候,就是跳表最高层(再减少都没有了呀): n・p^k ≈ 1
变形: p^k = 1/n ,也就是 n = 1/(p^k)
两边取对数: log (1/p)n = k
k 就是跳表的期望总层数。查找的时候,我们从最高层一层一层往下走,最多需要下降 k 层。
横向(同层向右):
在任意一层查找时,向右比较的次数的数学期望是常数1/p,和数据总量 n 无关。
以 Redis 的 p=1/4 举例: 每 4 个底层节点里,平均只有 1 个节点会向上晋升到上一层索引。 也就是说,在当前索引层,两个相邻索引节点之间,平均隔着 4 个底层节点。
当我们在这一层向右查找:
- 当前节点的值 < 目标值:继续向右走
- 当前节点的值 ≥ 目标值:停止,下沉到下一层
在这一层,平均只需要向右比较 4 次(也就是 (1/p)),就一定会触发下沉。 不管总共有 1000 个还是 100 万个节点,同一层的平均向右比较次数永远是 4 次,不会随着 n 变大而增加,这就是「常数」。
总结:
- 横向(同层向右):期望比较次数 = 1/p,属于 (O(1))
- 纵向(逐层下沉):期望层数O (log (1/p) n),属于 (O(log n))
查找总步数 = 每层横向比较次数 × 总层数=O(1)xO(log n)
所以查找整体时间复杂度 (O(log n))。最坏O(n)几乎遇不到
插入,删除时间复杂度
1.插入分为三步:
- 查找插入位置:期望 (O(log n))
- 随机生成新节点的层数
- 在生成层数对应的每一层链表执行插入(链表插入是常数 (O(1)))
总耗时:(O(log n)+O(1)=O(log n)) 最坏:(O(n))
2.删除和插入几乎一样:
- 查找目标节点:期望 \(O(\log n)\)
- 在该节点存在的每一层链表执行删除(链表删除 \(O(1)\))
总耗时:(O(log n)\) 最坏:(O(n))
一步步实现跳表代码
1.struct SkipListNode
struct SkiplistNode
{
int val; // 节点存储的值
vector<SkiplistNode*> next; // next[i] 代表第 i 层的后继节点指针
// 构造函数:val为值,level代表这个节点拥有多少层指针
SkiplistNode(int v, int level)
: val(v), next(level, nullptr)
{}
};
2.class Skiplist
class Skiplist
{
typedef SkiplistNode Node;
public:
Skiplist(){}
~Skiplist(){}
bool search(int target){}
vector<Node*> findprev(int num)
void add(int num)
bool erase(int num)
int randomLevel()
private:
Node* head; // 跳表头节点
const int maxLevel = 32; // 允许的最大层数
const double p = 0.25; // 向上晋升一层的概率
};
3.Skiplsit(){}构造函数
SkipList()
{
// 头节点:不存有效数据,val=-1,初始只有1层,,尽量不要初始化为maxlevel,要加快head从上往下下沉速度
head = new Node(-1, 1);
}
4.~Skiplsit(){}析构函数
~SkipList()
{
Node* cur = head;
while (cur!=nullptr)
{
Node* nextnode = cur->next[0];
delete cur;//先把1个节点从下到上删除
cur = nextnode;//去下一个节点
}
}
5.bool search查询某个元素在不在Skipist
bool search(int target)
{
Node* cur = head;
int curlevel = head->next.size() - 1;
while (curlevel >= 0)
{
// 当前层的下一个节点存在,并且值 < target:继续向右走
if (cur->next[curlevel] != nullptr && cur->next[curlevel]->_val < target)
{
cur = cur->next[curlevel];
}
//下一个节点为空 或者 值 > target:向右找不到,下沉到下一层
else if (cur->next[curlevel] == nullptr || cur->next[curlevel]->_val > target)
{
curlevel--;
}
// 找到相等节点
else
{
return true;
}
}
// 全部层找完,没有target
return false;
}
6.vector<Node*> findprev(int num),记录num的前驱
vector<Node*>findprev(int num)
{
Node* cur = head;
int curlevel = head->next.size() - 1;
// 初始化前驱数组,所有层默认前驱是头节点
vector<Node*>prev(curlevel + 1, head);//头节点一共curlevel+1层
while (curlevel >= 0)
{
// 当前层后继节点存在,并且后继值 < num:向右移动
if (cur->next[curlevel] != nullptr && cur->next[curlevel]->_val < num)
{
cur = cur->next[curlevel];
}
// 后继为空或者后继值 >= num:不能继续右走,记录当前节点为本层前驱,下沉
//!!!是下沉的时候更新前驱
else
{
prev[curlevel] = cur;
curlevel--;
} }
return prev;
}

逐轮循环跑一遍(num=17):
curLevel=3:cur=head,head->next 3=6,6<17 → cur=6; 现在 6->next 3 是 nullptr,不满足右走条件。prev[3]=6,curLevel=2。curLevel=2:cur=6,6->next 2=25,25>17,不能右走。prev[2]=6,curLevel=1。curLevel=1:cur=6,6->next 1=9,9<17 → cur=9; 9->next 1=25,25>17,不能右走。prev[1]=9,curLevel=0。curLevel=0:cur=9,9->next 0=12,12<17 → cur=12; 12->next 0=19,19>17,不能右走。prev[0]=12,curLevel=-1,循环结束。
返回结果:prev[0]=12,prev[1]=9,prev[2]=6,prev[3]=6
7. void add(int num)
void add(int num)
{
//1.找到num的每一层的前驱节点
vector<Node*>prev = findprev(num);
// 2. 随机生成新节点的层数
int newnodelevel = randomLevel();
//3.创建新节点
Node* newnode = new Node(num, newnodelevel);
// 如果新节点层数 > head层数:扩容头节点head,并且补充高层前驱为head
if (newnodelevel > head->next.size())
{
head->next.resize(newnodelevel, nullptr);
prev.resize(newnodelevel, head);
}
// 4. 逐层完成链表插入:新节点指向后继,前驱指向新节点
for (int i = 0; i < newnodelevel; i++)
{
newnode->next[i] = prev[i]->next[i];
prev[i]->next[i] = newnode;
}
}

调用 add(17) 插入,用上面得到的 prev 数组,newnodelevel=2;
循环 i 从 0 到 2(所以只会用到prev0,prev1):
- i=0(level0 底层):
newNode->next[0]=prev[0]->next[0] =12->next[0]=19;prev[0]->next[0]=newNode→12 指向 17 - i=1(level1):
newNode->next[1]=prev[1]->next[1]=9->next[1]=25;prev[1]->next[1]=newNode→9 指向 17
8. bool erase(int num)
bool erase(int num)
{
vector<Node*>prev = findprev(num);
//底层链表的后继不是num,说明不存在该数字!!,要从上往下一直判断到底层
//删除,插入必须到第0层,找的时候<=特别是==都还要往下走,一直走到第0层,才能记录所有的前驱,插入和删除的时候才好连接前后指针
if (prev[0]->next[0] == nullptr || prev[0]->next[0]->_val != num)
{
return false;
}
Node* delnode = prev[0]->next[0];
// 逐层断开待删除节点的链接
for (int i = 0; i < delnode->next.size(); i++)
{
prev[i]->next[i] = delnode->next[i];
}
delete delnode;
// 删除节点后,清理头节点高层空指针,压缩跳表最大层数,加快head从上往下下沉速度,nullptr没有用
int headmaxlevel = head->next.size() - 1;
while (headmaxlevel >= 0 && head->next[headmaxlevel] == nullptr)
{
headmaxlevel--;
}
head->next.resize(headmaxlevel + 1);
return true;
}

prev[0]->next[0]是 17,存在节点,可以删除。- delnode=17,delnode 层数是 2(0,1)
- i=0:
prev[0]->next[0]=delnode->next[0],12 的后继改成 19 - i=1:
prev[1]->next[1]=delnode->next[1],9 的后继改成 25
- i=0:
- delete 释放 17 节点,检查高层,把头节点空层压缩。 删除完成,跳表回到图上半部分原始状态。
9.int randomLevel()随机生成新节点层数
int randomLevel()
{
int level = 1;
// rand()/RAND_MAX ∈ [0,1],小于等于p就继续增加层数
while (rand() / RAND_MAX <= p && level < maxLevel)
{
level++;
}
return level;
}
完整代码:
#include<iostream>
#include<vector>
using namespace std;
struct SkipListNode
{
int _val;
vector<SkipListNode*> next;// next[i] 代表第 i 层的后继节点指针
SkipListNode(int v, int level)
:_val(v)
,next(level,nullptr)
{}
};
class SkipList
{
typedef SkipListNode Node;
public:
SkipList()
{
// 头节点:不存有效数据,val=-1,初始只有1层,,尽量不要初始化为maxlevel,要加快head从上往下下沉速度
head = new Node(-1, 1);
}
~SkipList()
{
Node* cur = head;
while (cur!=nullptr)
{
Node* nextnode = cur->next[0];
delete cur;//先把1个节点从下到上删除
cur = nextnode;//去下一个节点
}
}
bool search(int target)
{
Node* cur = head;
int curlevel = head->next.size() - 1;
while (curlevel >= 0)
{
// 当前层的下一个节点存在,并且值 < target:继续向右走
if (cur->next[curlevel] != nullptr && cur->next[curlevel]->_val < target)
{
cur = cur->next[curlevel];
}
//下一个节点为空 或者 值 > target:向右找不到,下沉到下一层
else if (cur->next[curlevel] == nullptr || cur->next[curlevel]->_val > target)
{
curlevel--;
}
// 找到相等节点
else
{
return true;
}
}
// 全部层找完,没有target
return false;
}
vector<Node*>findprev(int num)
{
Node* cur = head;
int curlevel = head->next.size() - 1;
// 初始化前驱数组,所有层默认前驱是头节点
vector<Node*>prev(curlevel + 1, head);//头节点一共curlevel+1层
while (curlevel >= 0)
{
// 当前层后继节点存在,并且后继值 < num:向右移动
if (cur->next[curlevel] != nullptr && cur->next[curlevel]->_val < num)
{
cur = cur->next[curlevel];
}
// 后继为空或者后继值 >= num:不能继续右走,记录当前节点为本层前驱,下沉
//!!!是下沉的时候更新前驱
else
{
prev[curlevel] = cur;
curlevel--;
} }
return prev;
}
void add(int num)
{
//1.找到num的每一层的前驱节点
vector<Node*>prev = findprev(num);
// 2. 随机生成新节点的层数
int newnodelevel = randomLevel();
//3.创建新节点
Node* newnode = new Node(num, newnodelevel);
// 如果新节点层数 > head层数:扩容头节点head,并且补充高层前驱为head
if (newnodelevel > head->next.size())
{
head->next.resize(newnodelevel, nullptr);
prev.resize(newnodelevel, head);
}
// 4. 逐层完成链表插入:新节点指向后继,前驱指向新节点
for (int i = 0; i < newnodelevel; i++)
{
newnode->next[i] = prev[i]->next[i];
prev[i]->next[i] = newnode;
}
}
bool erase(int num)
{
vector<Node*>prev = findprev(num);
//底层链表的后继不是num,说明不存在该数字!!,要从上往下一直判断到底层
//删除,插入必须到第0层,找的时候<=特别是==都还要往下走,一直走到第0层,才能记录所有的前驱,插入和删除的时候才好连接前后指针
if (prev[0]->next[0] == nullptr || prev[0]->next[0]->_val != num)
{
return false;
}
Node* delnode = prev[0]->next[0];
// 逐层断开待删除节点的链接
for (int i = 0; i < delnode->next.size(); i++)
{
prev[i]->next[i] = delnode->next[i];
}
delete delnode;
// 删除节点后,清理头节点高层空指针,压缩跳表最大层数,加快head从上往下下沉速度,nullptr没有用
int headmaxlevel = head->next.size() - 1;
while (headmaxlevel >= 0 && head->next[headmaxlevel] == nullptr)
{
headmaxlevel--;
}
head->next.resize(headmaxlevel + 1);
return true;
}
int randomLevel()
{
int level = 1;
// rand()/RAND_MAX ∈ [0,1],小于等于p就继续增加层数
while (rand() / RAND_MAX <= p && level < maxLevel)
{
level++;
}
return level;
}
private:
Node* head;
const int maxLevel = 32; // 允许的最大层数
const double p = 0.25; // 向上晋升一层的概率
};