数据结构:跳表
- 跳表
- 实现
- 类架构
- 构造函数
- 析构函数
- 查找
- 插入
- 删除
- 总代码
跳表
在传统的链表中,不论单链表还是双链表,查询时都要O(N)
的时间复杂度,就算是一个有序链表,由于无法像数组一样定址,无法进行二分查找。
为此,有人提出了增加链表的指针域,使其可以进行二分查找:
假设对于一个有序链表,每相邻的两个节点,升高一层,增加一个指针,让指针指向下一个升高的节点,也就是下面的链表。
如果要查找21
,那么从3
出发,从高层往底层查找。先在第二层查找,21 > 7
,所以目标节点在7
后面,21 > 12
、21 > 19
以此类推。直到查询到25
,发现21 < 25
,说明要查找的系欸但在[19, 25]
之间,下降一层。随后去19
的第一层的后一个节点查找,21 == 21
找到目标元素。
这个查询过程中,跳过了6 9 17
这几个只低层节点,快速的定位目标节点的范围,其实就是一个二分思想。
以此类推,还可以让每4
个节点再升高一层:
甚至每8
个节点再升高一层,也就是每到隔
2
n
2^{n}
2n的节点层数增加一层。这样就是一个完全的二分查找了,查询时间复杂度为
O
(
l
o
g
N
)
O(logN)
O(logN)。
但是这样的结构有很大的问题,那就是不好删除节点。如果要插入或删除某个节点,此时后续每个节点的位置都变化了,那么节点对应的层数也要进行变化。而层数变化之后,又要维护错综复杂的指针关系,因此这种严格的二分结构没有被推广。
为了避免这种问题,设计者做了一个大胆的处理,不再严格规定每个位置的节点的层数,而是插入节点时随机出一个层数。这样插入与删除都不用管其它节点是多少层,自己的层数完全随机。
图中红色路径是查找21
,绿色路径是在查找26
。在查找过程中,会跳过很多节点,因此称为skipList 跳表
。
刚刚提到,跳表每个节点的层数都是随机的,这会不会太随便了,会不会导致效率很低?
其实跳表的随机是有固定的算法的,如下:
int getRandomLevel()
{
int level = 1;
while (rand() < RAND_MAX * p)
level++;
return level;
}
跳表的初始层数为1
,随后进入一个循环判断,每次循环有p
的概率增加一层,如果本次没有增加层,那么终止循环。
比如说第一次循环:
- 有
p
的概率变成2
层,并进入下一轮循环判定 - 有
1 - p
的概率不增加层,此时跳出循环,最终层数就是1
如果可以进入第二次循环:
- 有
p
的概率变成3
层,并进入下一轮循环判定 - 有
1 - p
的概率不增加层,此时跳出循环,最终层数就是2
以此类推。另外的,跳表还要给出一个最高层数的限制,如果层数到达最高层数,不再进行下一轮判定,这一步在上述代码中没有体现。
由此可得:
- 层数为
1
的概率为 1 − p 1 - p 1−p - 层数为
2
的概率为 p × ( 1 − p ) p\times(1-p) p×(1−p),第一轮加层概率为 p,第二轮不加层概率为 (p - 1) - 层数为
3
的概率为 p 2 × ( 1 − p ) p^{2}\times(1-p) p2×(1−p),前两轮加层概率为 p 2 p^{2} p2,第三轮不加层概率为 (p - 1) - 以此类推…
- 层数为
n
的概率为 p n − 1 × ( 1 − p ) p^{n-1}\times(1-p) pn−1×(1−p),前n-1
轮加层概率为 p n − 1 p^{n-1} pn−1,第n
轮不加层概率为 (p - 1)
计算可得,跳表的平均层数为:
1 1 − p \frac{1}{1-p} 1−p1
在Redis
中,p = 0.25
,最大层数不超过32
层,此时平均每个节点的层数是4/3
层。
实现
类架构
- 节点类
template <typename T>
struct SkipListNode
{
T _val;
vector<SkipListNode*> _nextNodes;
SkipListNode(T val, int level)
: _val(val)
, _nextNodes(level, nullptr)
{}
};
val
:当前节点存储的值_nextNodes
:动态数组,存储指向下一个节点的指针,每一层的下一个节点可能不同
- 跳表类
template <typename T>
class SkipList
{
using Node = SkipListNode<T>;
private:
Node* _head; // 头节点
int _maxLevel; // 最大层高
double _p; // 概率
int _curLevel; // 当前最大层高
int _size; // 节点个数
};
_head
:指向头节点的指针_maxLevel
:单个节点的最大层数_p
:单轮循环,节点增加层的概率_curLevel
:当前所有节点中的最大层高_size
:跳表的元素个数
构造函数
构造函数如下:
SkipList(int maxLevel = 32, double p = 0.25)
: _maxLevel(maxLevel)
, _p(p)
, _curLevel(0)
, _size(0)
{
srand((unsigned int)time(nullptr));
_head = new Node(T(), _maxLevel);
}
在构造函数中,允许用户自定义概率和最大层数,默认情况maxLevel = 32
,p = 0.25
。在初始化列表中完成成员变量的初始化。
由于后续要计算随机数,在构造函数中srand((unsigned int)time(nullptr))
将时间作为随机数种子。
随后构造一个头节点,这个节点作为哨兵不存储实际数据。头节点的层数指定为最大层数。
析构函数
析构函数如下:
~SkipList()
{
Node* del = _head;
Node* tmp = nullptr;
while (del)
{
tmp = del->_nextNodes[0];
delete del;
del = tmp;
}
}
析构函数需要从头到尾进行释放节点,此时直接遍历链表,并且只遍历第一层_nextNodes[0]
。因为所有节点都有第一层,所以遍历第一层一定可以释放掉所有节点。
查找
基于跳表的两大特性:有序、分层。其实查找过程中,每次行动只有以下两种选择:
- 在同层向右移动
- 向下一层移动
比如查找21
的过程:
红色路径就是21
的查找路径。
- 从头节点第
4
层出发,查看同层的下一个节点6
,21 >= 6
说明要找的节点在6
之后,向右移动 - 从
6
的第4
层出发,查看同层的下一个节点NULL
,说明当前层没有下一个节点了,只能向下移动 - 从
6
的第3
层出发,查看同层的下一个节点25
,21 < 25
说明要找的节点在25
之前,向下移动 - 从
6
的第2
层出发,查看同层的下一个节点9
,21 >= 9
说明要找的节点在9
之后,向右移动 - 从
9
的第2
层出发,查看同层的下一个节点17
,21 >= 17
说明要找的节点在17
之后,向右移动 - 从
17
的第2
层出发,查看同层的下一个节点25
,21 < 25
说明要找的节点在25
之前,向下移动 - 从
17
的第1
层出发,查看同层的下一个节点19
,21 >= 19
说明要找的节点在19
之后,向右移动 - 从
19
的第1
层出发,查看同层的下一个节点21
,21 >= 21
说明要找的节点在21
之后,向右移动 - 从
21
的第1
层出发,查看同层的下一个节点25
,21 < 25
说明要找的节点在25
之前,向下移动 - 到达第
0
层,说明已经查找完毕,当前所在节点21 == 21
,成功找到节点
假设要查找20
,前7步相同,后两步:
- 从
19
的第1
层出发,查看同层的下一个节点21
,20 <=21
说明要找的节点在21
之前,向下移动 - 到达第
0
层,说明已经查找完毕,当前所在节点19 != 20
,说明节点20
不存在
可以总结出查找逻辑:
- 同层下一个节点为空指针,向下移动一层
- 目标值大于等于同层下一个节点的值,移动至同层的下一个节点
- 目标值小于同层下一个节点的值,向下移动一层
- 当到达第0层,判断节点值是否是目标值
代码:
bool find(T val)
{
int level = _curLevel - 1; // 获得当前的最高层
Node* cur = _head; // 记录当前节点
while (level >= 0) // 当 level 到达第0层,查找完毕
{
if (cur->_nextNodes[level] != nullptr && val >= cur->_nextNodes[level]->_val)
cur = cur->_nextNodes[level]; // 向同层的下一个节点移动
else
level--; // 向下移动一层
}
return cur != nullptr && cur->_val == val;
}
首先要注意的是,_nextNodes
是一个数组,_nextNodes[0]
代表的是第一层,也就是说下标和层数之间有一个错位。所以最开始level = _curLevel - 1
要进行减一操作。
在while(level >= 0)
同理,level = 0
时,是在第一层而不是第零层,继续循环。
cur->_nextNodes[level]
代表同层的下一个节点,因为每次比较,都要和同层的下一个节点比较。比较之前,先判断下一个节点是否为空,防止访问空指针。
最后跳出循环时,查看cur
存储的值是否是目标值,如果是,那么该元素存在,反之则不存在。
插入
如图:
当插入一个新节点时,该节点的前后指针都要改变,也就是图中固定红色指针。那么在找到插入位置后,要把插入位置每一层的前一个节点记录下来。
查找每一层的前一个节点:
vector<Node*> findPrev(T val)
{
vector<Node*> prevNodes(_maxLevel, _head); // 前一个节点的数组
int level = _curLevel - 1; // 从当前最高层出发
Node* cur = _head;
while (level >= 0)
{
if (cur->_nextNodes[level] && val > cur->_nextNodes[level]->_val)
{
cur = cur->_nextNodes[level]; // 向右移动
}
else
{
prevNodes[level] = cur; // 记录该层的前一个节点为cur
level--; // 向下移动
}
}
return prevNodes;
}
该函数接受一个val
,返回val
的每一层的前一个节点。 prevNodes(_maxLevel, _head)
用于存储返回值,元素初始化为_head
,即每个节点默认的前一个节点为_head
,如果后续新节点的层数超出了当前最大层数,此时前一个节点为_head
。
level = _curLevel - 1
随后从当前的最高层出发,查找插入位置。
查找每一层前一个节点时,只要层数下降,说明当前节点就是该层的前一个节点。
比如插入15
,从head
出发,到达节点6
的第四层,此时第四层下一个节点为NULL
,向下移动,此时15
在第四层的前一个节点为6
。
从节点6
的第三层出发,第三层下一个节点为25
,25 > 15
向下移动,此时15
在第三层的前一个节点为6
。
后续同理,在第二层的前一个节点为9
,第一层的前一个节点为12
。
在上述代码中,有一个小细节,那就是val > cur->_nextNodes[level]->_val)
,在find
中,此处是大于等于,而这里改成了大于。因为要找前一个节点,在下一个节点值等于目标值的时候,要向下移动,而不是向后移动。
插入代码:
bool insert(const T& val)
{
vector<Node*> prevNodes = findPrev(val);
if (prevNodes[0]->_nextNodes[0] != nullptr
&& prevNodes[0]->_nextNodes[0]->_val == val)
return false; // 已存在
int level = getRandomLevel(); // 获取随机层数
_curLevel = max(_curLevel, level); // 更新最大层数
Node* node = new Node(val, level); // 创建新节点
for (int i = 0; i < level; i++)
{
node->_nextNodes[i] = prevNodes[i]->_nextNodes[i];
prevNodes[i]->_nextNodes[i] = node;
}
_size++;
return true;
}
插入之前,先判断目标值是否已经存在,由于prevNodes
存储的是每一层的前一个节点,所以prevNodes[0]->_nextNodes[0]
才是要插入的位置,此时如果prevNodes[0]->_nextNodes[0]->_val == val
,说明要插入的值原先就存在,直接返回false
。
随后通过getRandomLevel
函数获取一个随机层数。如果随机的层数超过了当前最大层,更新当前的最大层数。
然后通过val
和随机层数level
创建一个节点。从新节点的最高层往下,node->_nextNodes[i] = prevNodes[i]->_nextNodes[i]
将新节点的下一个节点,指向原先前一个节点的下一个节点。prevNodes[i]->_nextNodes[i] = node
将前一个节点的下一个节点,设为新节点。这和链表的插入是一样的,只是因为有一个分层的概念,所以这里比较抽象。
其实就是:
node->next = prev->next;
prev->next = node;
这样就很好理解了。
完成前后节点的指针关系维护,插入就完成了,最后_size++
表示总节点数增加。
删除
理解了插入,删除也很简单了,如图:
同理,删除节点,只需要注意维护每一层的前后指针关系即可。
删除代码:
bool erase(const T& val)
{
vector<Node*> prevNodes = findPrev(val);
if (prevNodes[0]->_nextNodes[0] == nullptr
|| prevNodes[0]->_nextNodes[0]->_val != val)
return false; // 不存在
Node* del = prevNodes[0]->_nextNodes[0]; // 记录被删除的节点
for (int i = 0; i < del->_nextNodes.size(); i++)
prevNodes[i]->_nextNodes[i] = del->_nextNodes[i]; // 维护指针关系
// 更新最大层数
delete del;
_size--;
return true;
}
首先判断被删除的节点是否存在,这个if
判断条件和插入是刚好相反的。如果目标值不存在,就不删除,返回false
。
随后Node* del = prevNodes[0]->_nextNodes[0]
记录要删除的节点。这里使用prevNodes[0]
是因为不确定要删除的节点是多少层,但是它至少有1
层,所以拿第一层的下一个节点一定可以找到要删除的节点。
随后一个for
循环维护指针关系,相当于以下代码:
prev->next = del->next;
最后delete del
删除节点,最后_size--
表示目标节点少一个。
但是这还有一个问题,那就是如果删除节点之后,层高降低了:
此处6
作为唯一一个四层的节点,删除后整个跳表的高度都降低为了3
,所以在删除操作的最后,还少了一步检测当前的最大层高。也就是以上代码的// 更新最大层数
位置。
更新层高:
if (del->_nextNodes.size() == _curLevel) // 被删除的节点,层高和当前的最大层高一致
{
for (int i = 0; i < _maxLevel; i++)
{
if (_head->_nextNodes[i] == nullptr) // 当前层 _head 下一个节点为空
{
_curLevel = i; // 更新层高
break;
}
}
}
更新层高很简单,让_head
节点从最底层出发,向上检测,如果某一层_head
的下一个节点为nullptr
,说明没有更高层了,此时层高就是该层-1
。
首先通过if
判断,被删除节点的层高是否是最大层高,如果被删除节点的层高不是最大层高,那么就算被删除了也不会影响最大层高。
随后进入for
循环,从最底层开始向上检测,当第一次检测到_head
的下一个节点为空,更新层高。_curLevel = i
,此处注意是i
而不是i - 1
,因为_nextNodes[i]
层的下一个节点为空,说明_nextNodes[i]
的低一层为最高层,而由于下标与层高的错位关系,_nextNodes[i]
其实是第i + 1
层,它的低一层是i + 1 - 1 = i
,所以_curLevel = i
。
总代码
SkipList.hpp
:
#pragma once
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <time.h>
using namespace std;
template <typename T>
struct SkipListNode
{
T _val;
vector<SkipListNode*> _nextNodes;
SkipListNode(T val, int level)
: _val(val)
, _nextNodes(level, nullptr)
{}
};
template <typename T>
class SkipList
{
using Node = SkipListNode<T>;
public:
SkipList(int maxLevel = 32, double p = 0.25)
: _maxLevel(maxLevel)
, _p(p)
, _curLevel(0)
, _size(0)
{
srand((unsigned int)time(nullptr));
_head = new Node(T(), _maxLevel);
}
~SkipList()
{
Node* del = _head;
Node* tmp = nullptr;
while (del)
{
tmp = del->_nextNodes[0];
delete del;
del = tmp;
}
}
bool find(T val)
{
int level = _curLevel - 1;
Node* cur = _head;
while (level >= 0)
{
if (cur->_nextNodes[level] && val >= cur->_nextNodes[level]->_val)
cur = cur->_nextNodes[level];
else
level--;
}
return cur != nullptr && cur->_val == val;
}
vector<Node*> findPrev(T val)
{
vector<Node*> prevNodes(_maxLevel, _head);
int level = _curLevel - 1;
Node* cur = _head;
while (level >= 0)
{
if (cur->_nextNodes[level] && val > cur->_nextNodes[level]->_val)
{
cur = cur->_nextNodes[level];
}
else
{
prevNodes[level] = cur;
level--;
}
}
return prevNodes;
}
int getRandomLevel()
{
int level = 1;
while (rand() < RAND_MAX * _p && level < _maxLevel)
level++;
return level;
}
bool insert(const T& val)
{
vector<Node*> prevNodes = findPrev(val);
if (prevNodes[0]->_nextNodes[0] != nullptr
&& prevNodes[0]->_nextNodes[0]->_val == val)
return false; // 已存在
int level = getRandomLevel();
_curLevel = max(_curLevel, level);
Node* node = new Node(val, level);
for (int i = 0; i < level; i++)
{
node->_nextNodes[i] = prevNodes[i]->_nextNodes[i];
prevNodes[i]->_nextNodes[i] = node;
}
_size++;
return true;
}
bool erase(const T& val)
{
vector<Node*> prevNodes = findPrev(val);
if (prevNodes[0]->_nextNodes[0] == nullptr
|| prevNodes[0]->_nextNodes[0]->_val != val)
return false; // 不存在
Node* del = prevNodes[0]->_nextNodes[0];
for (int i = 0; i < del->_nextNodes.size(); i++)
prevNodes[i]->_nextNodes[i] = del->_nextNodes[i];
if (del->_nextNodes.size() == _curLevel)
{
for (int i = 0; i < _maxLevel; i++)
{
if (_head->_nextNodes[i] == nullptr)
{
_curLevel = i;
break;
}
}
}
delete del;
_size--;
return true;
}
void print()
{
vector<vector<T>> show(_curLevel, vector<T>(_size, 0));
Node* cur = _head->_nextNodes[0];
for (int i = 0; i < _size; i++)
{
for (int j = 0; j < cur->_nextNodes.size(); j++)
show[j][i] = cur->_val;
cur = cur->_nextNodes[0];
}
for (int i = _curLevel - 1; i >= 0; i--)
{
for (int j = 0; j < _size; j++)
{
if (show[i][j] != T())
printf("%2d", show[i][j]);
else
printf(" ");
}
printf("\n");
}
}
private:
Node* _head; // 头节点
int _maxLevel; // 最大层高
double _p; // 概率
int _curLevel; // 当前最大层高
int _size;
};
test.cpp
:
#include <iostream>
#include "SkipList.hpp"
using namespace std;
int main()
{
SkipList<int> sk;
sk.insert(1);
sk.insert(2);
sk.insert(3);
sk.insert(4);
cout << "测试重复插入:" << endl;
cout << "第一次:" << sk.insert(5) << endl;
cout << "第二次:" << sk.insert(5) << endl;
sk.insert(6);
sk.insert(7);
sk.insert(8);
sk.insert(9);
cout << "-------------------" << endl;
cout << "插入1-9:" << endl;
sk.print();
cout << "-------------------" << endl;
cout << "查询1:" << sk.find(1) << endl;
cout << "查询2:" << sk.find(2) << endl;
cout << "查询8:" << sk.find(8) << endl;
cout << "查询9:" << sk.find(9) << endl;
cout << "查询12:" << sk.find(12) << endl;
cout << "查询256:" << sk.find(256) << endl;
cout << "查询-5:" << sk.find(-5) << endl;
cout << "-------------------" << endl;
cout << "删除7:" << sk.erase(7) << endl;
cout << "重复删除7:" << sk.erase(7) << endl;
sk.print();
cout << "-------------------" << endl;
cout << "删除3:" << sk.erase(7) << endl;
sk.erase(3);
sk.print();
cout << "-------------------" << endl;
return 0;
}