LeetCode 1206. 设计跳表
难度: h a r d \color{red}{hard} hard
题目描述
不使用任何库函数,设计一个 跳表 。
跳表 是在 O ( l o g ( n ) ) O(log(n)) O(log(n)) 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树,其功能与性能相当,并且跳表的代码长度相较下更短,其设计思想与链表相似。
例如,一个跳表包含 [ 30 , 40 , 50 , 60 , 70 , 90 ] [30, 40, 50, 60, 70, 90] [30,40,50,60,70,90] ,然后增加 80 80 80、 45 45 45 到跳表中,以下图的方式操作:
Artyom Kalinin [CC BY-SA 3.0], via Wikimedia Commons
跳表中有很多层,每一层是一个短的链表。在第一层的作用下,增加、删除和搜索操作的时间复杂度不超过 O ( n ) O(n) O(n)。跳表的每一个操作的平均时间复杂度是 O ( l o g ( n ) ) O(log(n)) O(log(n)),空间复杂度是 O ( n ) O(n) O(n)。
了解更多 : https://en.wikipedia.org/wiki/Skip_list
在本题中,你的设计应该要包含这些函数:
- b o o l s e a r c h ( i n t t a r g e t ) bool search(int target) boolsearch(inttarget) : 返回target是否存在于跳表中。
- v o i d a d d ( i n t n u m ) void add(int num) voidadd(intnum): 插入一个元素到跳表。
- b o o l e r a s e ( i n t n u m ) bool erase(int num) boolerase(intnum): 在跳表中删除一个值,如果 n u m num num 不存在,直接返回false. 如果存在多个 n u m num num ,删除其中任意一个即可。
注意,跳表中可能存在多个相同的值,你的代码需要处理这种情况。
示例 1:
输入
["Skiplist", "add", "add", "add", "search", "add", "search", "erase", "erase", "search"]
[[], [1], [2], [3], [0], [4], [1], [0], [1], [1]]
输出
[null, null, null, null, false, null, true, false, true, false]
解释
Skiplist skiplist = new Skiplist();
skiplist.add(1);
skiplist.add(2);
skiplist.add(3);
skiplist.search(0); // 返回 false
skiplist.add(4);
skiplist.search(1); // 返回 true
skiplist.erase(0); // 返回 false,0 不在跳表中
skiplist.erase(1); // 返回 true
skiplist.search(1); // 返回 false,1 已被擦除
提示:
- 0 < = n u m , t a r g e t < = 2 ∗ 1 0 4 0 <= num, target <= 2 * 10^{4} 0<=num,target<=2∗104
- 调用 s e a r c h search search, a d d add add, e r a s e erase erase操作次数不大于 5 ∗ 1 0 4 5 * 10^{4} 5∗104
算法
(数据结构-单链表)
首先需要定义链表的最大高度 level
,这里取一个经验值 level=8
,Redis
中设置是 32
。
我们可以看到 head
节点在每一层都会连接一个链表,由上图引出跳表节点的结构:
-
节点值
val
-
存储当前节点在每一层的
next
指针,方便我们操作(为了方便理解我们可以把图中每个节点的高度都看成level
,没在图中画出来的就是指向NULL
,而它们的值都是相同的val
。
节点结构代码实现:
// 定义跳表节点
struct Node {
int val; // 节点值
vector<Node*> next; // 记录节点在每一层的 next,next[i] 表示当前节点第 i 层的 next
Node(int _val) : val(_val) { // 构造函数
next.resize(level, NULL); // 初始化 next 数组的大小和层数 level 相同,初始值都指向 NULL
}
}*head; // 定义头节点 head
搞清楚节点结构之后,下面就好办了,不管是查找、插入、删除都需要先找到目标值或者找到目标值的前一个节点,那么这里我们统一设计一个辅助函数 find()
:找到小于目标值的最大的节点,由于跳表是多层链表结构,所以要找的不是一层而是每一层。
find()
函数代码实现:
// 辅助函数:找到每一层 i 小于目标值 target 的最大节点 pre[i],最后 pre 中存的就是每一层小于 target 的最大节点
void find(int target, vector<Node*>& pre) {
auto p = head; // 从头节点开始遍历每一层
for (int i = level - 1; i >= 0; i -- ) { // 从上层往下层找
while (p->next[i] && p->next[i]->val < target) p = p->next[i]; // 如果当前层 i 的 next 不为空,且它的值小于 target,则 p 往后走指向这一层 p 的 next
pre[i] = p; // 退出 while 时说明找到了第 i 层小于 target 的最大节点就是 p
}
}
有了辅助函数之后,如何实现查找、插入、删除操作呢?不管是哪种操作首先调用 find()
得到每一层小于目标值的最大节点数组 pre
1、查找 search()
:由于第 0
层的数据是最全的,所以只需要在第 0
层查找是否存在即可,代码如下:
// 从跳表中查找 target
bool search(int target) {
vector<Node*> pre(level);
find(target, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]
auto p = pre[0]->next[0]; // 因为最下层【0】的节点是全的,所以只需要判断 target 是否在第 0 层即可,而 pre[0] 正好就是小于 target 的最大节点,如果 pre[0]->next[0] 的值不是 target 说明没有这个元素
return p && p->val == target;
}
2.、插入 insert()
:新建要插入的节点,从第 0
层开始插入,往上每层 50%
的插入,50%
的概率不插入,相当于两个点中有一个在上层插入(当然这并不一定),只不过这样较好实现,理论上是一样的。具体插入操作就是单链表插入,代码如下:
// 向跳表中插入元素 num
void add(int num) {
vector<Node*> pre(level);
find(num, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]
auto p = new Node(num); // 创建要插入的新节点
for (int i = 0; i < level; i ++ ) { // 遍历每一层,从下往上插入新节点
p->next[i] = pre[i]->next[i]; // 这两步就是单链表的插入
pre[i]->next[i] = p;
if (rand() % 2) break; // 每一层有 50% 的概率不插入新节点
}
}
3、删除 erase()
:因为是多层结构,所以需要把每一层等于目标值的节点都删除,具体删除操作就是单链表删除,代码如下:
// 从跳表中删除 num
bool erase(int num) {
vector<Node*> pre(level);
find(num, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]
// 先判断 num 是否存在,不存在直接返回 false
// 第 0 层存储的是全部节点,所以只需要判断 pre[0]->next[0](第 0 层小于 num 的最大节点的在第 0 层的 next) 是不是 num 即可
auto p = pre[0]->next[0];
if (!p || p->val != num) return false;
// 否则删除每一层的 num,如果 pre[i]->next[i] == p 说明第 i 层存在 p
for (int i = 0; i < level && pre[i]->next[i] == p; i ++ ) {
pre[i]->next[i] = p->next[i]; // 单链表删除
}
delete p; // 删除节点 p,防止内存泄漏
return true;
}
复杂度分析
- 时间复杂度:查询、删除、插入的时间复杂度近似 O ( l o g n ) O(logn) O(logn)
C++ 代码
class Skiplist {
public:
static const int level = 8; // 层数,经验值 8,太大浪费空间,因为每一个节点都要存在每一层的 next,层数越多节点数越多
// 定义跳表节点
struct Node {
int val; // 节点值
vector<Node*> next; // 记录节点在每一层的 next,next[i] 表示当前节点第 i 层的 next
Node(int _val) : val(_val) { // 构造函数
next.resize(level, NULL); // 初始化 next 数组的大小和层数 level 相同,初始值都指向 NULL
}
}*head; // 定义头节点 head
Skiplist() {
head = new Node(-1); // 初始化一个不存在的节点值 -1
}
~Skiplist() {
delete head; // 析构函数删除 head
}
// 辅助函数:找到每一层 i 小于目标值 target 的最大节点 pre[i],最后 pre 中存的就是每一层小于 target 的最大节点
void find(int target, vector<Node*>& pre) {
auto p = head; // 从头节点开始遍历每一层
for (int i = level - 1; i >= 0; i -- ) { // 从上层往下层找
while (p->next[i] && p->next[i]->val < target) p = p->next[i]; // 如果当前层 i 的 next 不为空,且它的值小于 target,则 p 往后走指向这一层 p 的 next
pre[i] = p; // 退出 while 时说明找到了第 i 层小于 target 的最大节点就是 p
}
}
// 从跳表中查找 target
bool search(int target) {
vector<Node*> pre(level);
find(target, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]
auto p = pre[0]->next[0]; // 因为最下层【0】的节点是全的,所以只需要判断 target 是否在第 0 层即可,而 pre[0] 正好就是小于 target 的最大节点,如果 pre[0]->next[0] 的值不是 target 说明没有这个元素
return p && p->val == target;
}
// 向跳表中插入元素 num
void add(int num) {
vector<Node*> pre(level);
find(num, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]
auto p = new Node(num); // 创建要插入的新节点
for (int i = 0; i < level; i ++ ) { // 遍历每一层,从下往上插入新节点
p->next[i] = pre[i]->next[i]; // 这两步就是单链表的插入
pre[i]->next[i] = p;
if (rand() % 2) break; // 每一层有 50% 的概率不插入新节点
}
}
// 从跳表中删除 num
bool erase(int num) {
vector<Node*> pre(level);
find(num, pre); // 先找到每一层 i 小于目标值 target 的最大节点 pre[i]
// 先判断 num 是否存在,不存在直接返回 false
// 第 0 层存储的是全部节点,所以只需要判断 pre[0]->next[0](第 0 层小于 num 的最大节点的在第 0 层的 next) 是不是 num 即可
auto p = pre[0]->next[0];
if (!p || p->val != num) return false;
// 否则删除每一层的 num,如果 pre[i]->next[i] == p 说明第 i 层存在 p
for (int i = 0; i < level && pre[i]->next[i] == p; i ++ ) {
pre[i]->next[i] = p->next[i]; // 单链表删除
}
delete p; // 删除节点 p,防止内存泄漏
return true;
}
};
/**
* Your Skiplist object will be instantiated and called as such:
* Skiplist* obj = new Skiplist();
* bool param_1 = obj->search(target);
* obj->add(num);
* bool param_3 = obj->erase(num);
*/
备注
转载自 LeetCode 1206. 设计跳表