跳表详细解析

🔥keyipatience:个人主页

🎬作者简介:C/C++后端开发学习者

🌟专栏传送门:《c++》《linux》《c++高阶数据结构》《c++数据结构与算法》

⭐️patience is key in life

基础背景

普通有序单向链表 :查找需要逐个遍历,时间复杂度O(n)。 跳表(skiplist)是在有序链表基础上,多层索引链表实现的结构。

William Pugh 的原始优化思路

  1. 第一层索引:每 2 个底层节点,向上层提一个节点建立指针。上层链表节点数只有底层一半,查找比较次数减半。
  2. 多层迭代:在上一层链表继续隔二取一,继续向上建立更高层索引,层层叠加。
  3. 理想情况 :每层节点数是下层的一半,查找类似二分查找,时间复杂度 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

逻辑:

  1. 新节点初始层数 lvl=1(必然至少在底层链表)
  2. 循环:每次以概率 p 尝试再往上增加一层 ;达到最大层数maxLevel就停止
  3. 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. 横向(同层向右):期望比较次数 = 1/p,属于 (O(1))
  2. 纵向(逐层下沉):期望层数O (log (1/p) n),属于 (O(log n))

查找总步数 = 每层横向比较次数 × 总层数=O(1)xO(log n)

所以查找整体时间复杂度 (O(log n))。最坏O(n)几乎遇不到

插入,删除时间复杂度

1.插入分为三步:

  1. 查找插入位置:期望 (O(log n))
  2. 随机生成新节点的层数
  3. 在生成层数对应的每一层链表执行插入(链表插入是常数 (O(1)))

总耗时:(O(log n)+O(1)=O(log n)) 最坏:(O(n))

2.删除和插入几乎一样:

  1. 查找目标节点:期望 \(O(\log n)\)
  2. 在该节点存在的每一层链表执行删除(链表删除 \(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):

  1. curLevel=3:cur=head,head->next 3=6,6<17 → cur=6; 现在 6->next 3 是 nullptr,不满足右走条件。prev[3]=6,curLevel=2。
  2. curLevel=2:cur=6,6->next 2=25,25>17,不能右走。prev[2]=6,curLevel=1。
  3. curLevel=1:cur=6,6->next 1=9,9<17 → cur=9; 9->next 1=25,25>17,不能右走。prev[1]=9,curLevel=0。
  4. 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;
}
  1. prev[0]->next[0] 是 17,存在节点,可以删除。
  2. 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
  3. 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;   // 向上晋升一层的概率
};
相关推荐
喆星时瑜1 小时前
R语言基础语法入门
开发语言·r语言
汉克老师1 小时前
GESP2026年9月认证C++一级( 第一部分选择题(8~15题)精讲
c++·gesp·小学生·学c++编程
想吃火锅10051 小时前
【leetcode】189.轮转数组js
开发语言·javascript·leetcode
郝学胜-神的一滴1 小时前
AI 编程智能体 02:AI智能体到底是什么
开发语言·人工智能·python·程序人生·pycharm
念何架构之路1 小时前
go-grpc服务端调用
开发语言·后端·golang
小小、码农1 小时前
〖Linux文件系统〗:彻底打通文件 IO 全链路
linux·开发语言·数据结构·c++·系统
大侠归来2 小时前
cJSON 源码解析:parse_string 函数深入剖析
linux·网络·算法
弹简特2 小时前
【Java项目-企悦抽】13-奖品管理模块-奖品列表和创建奖品的实现
java·开发语言·网络·springboot
Rosanci2 小时前
从零到合入主线:我的 DeepSeek Harness 开源贡献实战手记
开发语言·前端·开源