跳跃表
作者:互联网
-
参考:
-
算法训练营6.3
-
-
简介:
-
名称:跳跃表
-
本质:可以进行二分查找的有序链表。
-
一些abstract:正常的链表查找是O(n)的,想要依靠有序性完成快速的二分查找似乎并不容易,我们尝试在一个有序链表上添加若干层索引,比如第一层是将所有奇数下标的点“提”出来,复制其data值,将新的节点的指针指向原来的节点,新的节点之间按照原来的顺序相连,即如图所示:
这样本来如果要看一个7.5的数据,寻找第一个比他大的位置,要从1找到8,但是现在从第一层(暂时只设置这一层)开始,从1到3到5到7(小于9,划到下一层索引)再到8,就可以省略2和4和6的比较。
如果一直往上设置这样的索引,比如这样:
这样就像滑滑梯一样,从上面一直往下滑,用O(logn)层的索引,总共O(n)数量的节点,每次判断是该往右走还是往右走,而决定往下走一共执行O(logn)次,并且每一层最多往右走一次,不然相当于走了一个上一层索引,所以往右走也是O(logn)次,所以最终复杂度O(logn),建表复杂度O(n)。
但事实上跳跃表并不是简单地通过奇偶次序建立索引的,而是通过随机技术实现的,因此跳跃表是一种随机化的数据结构。比如总共有n个元素,随机选n/2个元素做一级索引,因为是随机的,所以分布较为均匀,然后随机选n/4个元素做二级索引,以此类推。
跳跃表还可以提高插入和删除的性能,平衡二叉查找树在进行插入和删除等操作后需要多次进行调整,而跳跃表完全依靠随机技术,其性能和平衡二叉查找树不相上下,但是原理简单。Redis中的有序集合和LevelDB中的MemTable都是利用跳跃表实现的。
-
-
操作:
-
数据结构定义 & 初始化:
每个节点设置一个向右的和向下的指针,根据需要看看需不需要向左和向上的指针,构建四联表。
typedef struct Node {
int val;
struct Node *forward[MAX_LEVEL]; // 后继指针数组 每一层的后继都不一样
}*Nodeptr;
Nodeptr head, updata[MAX_LEVEL]; // head为跳跃表头指针,updata数组记录访问路径每一层能到达最远的地方(超过这个点就得到下一层了)
int level; // 跳跃表层次
// 初始化头节点 这里的头节点值为负无穷,和普通的节点不同,即比所有节点的值都小,方便向右查找
void Init() {
level = 0;
head = new Node;
for (int i = 0; i < MAX_LEVEL; i++) {
head -> forward[i] = NULL;
}
head -> val = -INF:
} -
查找:在跳跃表中查找元素x。
算法步骤如下:
-
从最上层的头节点开始。
-
设当前位置为p,p的后继节点的值为y,若x=y,则查找成功;若x<y,则需要向下一层移动,若x>y,则向右移动一个位置继续查找。(如果p右边没有位置,则直接向下走)。
-
如果到达了底层之后还要往下走,则查找失败。
// 查找小于val的最接近val的元素
Nodeptr Find(int val) {
Nodeptr p = head;
for (int i = level; i >= 0; i--) { // 因为索引是从下往上建的,所以点最少的层在最上面。
while(p -> forward[i] && p -> forward[i] -> val < val) { // 可以往右走,就往右走
p = p -> forward[i]; // 往右走
}
update[i] = p;
}
return p;
} -
-
插入:在跳跃表中插入一个元素,相当于在某一个位置插入一个高度为level的列,插入的位置通过查找决定,插入列的高度可以采用随机化的决策确定:
-
最开始设层次layer为0,表示不插入,
-
设定层数增加的概率P为0.5或0.25。
-
随机一个0~1的数字r,如果r小于P且layer<MAX_LEVEL,layer++,否则就在layer层索引从上到下插入这个元素。
查找复杂度为O(logn),随机复杂度为O(logn),总复杂度为O(logn)。
随机层树板子:
int RandomLevel() {
int lay = 0;
while((float)rand() / RAND_MAX < P && lay < MAX_LEVEL - 1) lay++;
return lay;
}有一个概率的问题,就是这么搞之后,经过无数次插入后,不同层的节点比例大致会趋于多少。
注意到插入最底层的概率是1-p,插入第一级索引的概率是p(1-p),插入第二层的概率是p^2(1-p),所以看起来p=0.5还是挺有道理的,这样每一层都相当于抽出去一半。所以也可以判断rand() % 2的值,如果是1往上看,如果是0就在这层插入。
插入板子:
void Insert(int val) {
Nodeptr p, s, s1;
int lay = RandomLevel();
if (lay > level) level = lay; // 如果超过了当前的最大层数,更新它
p = Find(val);
s = new Node;
s -> val = val;
for (int i = 0; i < MAX_LEVEL; i++) s -> forward[i] = NULL;
for (int i = 0; i <= lay; i++) { // 每一层根据刚才的查找结果插入这个新节点
s -> forward[i] = update[i] -> forward[i]; // update[i]的后继变成了新节点的后继,然后update的后继变成s
update[i] -> forward[i] = s;
}
}这样有了头节点之后,插入操作之后就像上面那个图一样了。
-
-
删除:跳跃表中删除一个元素val,相当于删除它所在的列。
插入的操作也需要先查找,然后根据update[i]删除,如果最上方产生了空链,则删除空链。
20删除之后,最上面为空链,层次少1。
void Delete(int val) {
Nodeptr p = Find(val);
if (p -> forward[0] && p -> forward[0] -> val == val) { // 小于的下一个看看是不是等于,如果是才能删除,不然删错了
for (int i = level; i >= 0; i--) {
if (updata[i] -> forward[i] && updata[i] -> forward[i] -> val == val) { // 如果这一层有val的值
updata[i] -> forward[i] = updata[i] -> forward[i] -> forward[i]; // 等于后继,就是跨过了中间的节点,删除。
}
}
while (level > 0 && !head -> forward[level]) { // 空链删除
level--;
}
}
}复杂度采用随机技术后,为O(logn)。
-
-
例题:
-
(HDU4006)查询第k小元素,原题是查询第k大是等价的,直接保留总数total,查询第total-k+1小即可。
那么如何在跳跃表中跳跃呢,每个节点在每一层都加一个域(其实是加一个数组),记录这个节点在这一层后面,和下一个节点的距离(最后一个节点的距离域是到total还有多少个节点),如图所示:
显然只需要看剩余的“步”够不够当前节点的距离域,如果够,就做差往后跳,不然就只能掉到下一层,直到没有步为止。
int Get_kth(int k) { // 第k小
if (k > total) k = total;
if (k < 1) k = 1;
Nodeptr p = head;
for (int i = level; i >= 0; i--) {
while(p && p -> sum[i] < k) { // 这里小于号是为了防止查询8这种,直接在第一层踩到空了
k -= p -> sum[i];
p = p -> forward[i];
}
}
// 最后退出来k为1
return p -> forward[0] -> val0;
}还可以查询有多少个数小于查询的val:
int Find(int val) {
Nodeptr p = head;
int ans = 0;
for (int i = level; i >= 0; i--) {
while(p -> forward[i] && p -> forward[i] -> < val) {
ans += p -> sum[i];
p = p -> forward[i];
}
updata[i] = p;
}
return ans;
}现在一看,好像没什么问题,有问题一会再改。
那对于插入元素可不可以维护呢?
比如我们通过查找得到了一堆updata,知道这些节点的下一个forward是哪个,所以我们知道新节点的forward是哪个,但是sum呢,sum好像不太好算,如果提前记录好每一层有多少个数比他小就好了,记为tot[i],这样updata[i]距离插入节点的距离就是tot[0]-tot[i],所以原来的节点的sum就是tot[0]-tot[i],新的节点的sum就是原来节点的sum和这个sum做差就行了。于是,每一层出来之前都像最后一层一样,存一下有多少个数比updata[i]小,于是改成这样:
int Find(int val) {
Nodeptr p = head;
tot[level] = 0
for (int i = level; i >= 0; i--) {
while(p -> forward[i] && p -> forward[i] -> < val) {
tot[i] += p -> sum[i];
p = p -> forward[i];
}
if (i > 0) tot[i-1] = tot[i]; // 继承上一层的答案
updata[i] = p;
}
return tot[0];
}
void Insert(int val) {
Nodeptr p, s;
int lay = RandomLevel();
if (lay > level) {
for (int i = level + 1; i <= lay; i++) {
head -> sum[i] = total;
}
level = lay;
}
Find(val);
s = new Node;
s -> val = val;
for (int i = 0; i < MAX_LEVEL; i++) {
s -> forward[i] = NULL;
s -> sum[i] = 0;
}
for (int i = 0; i <= lay; i++) {
s -> forward[i] = updata[i] -> forward[i]; // 插入将后继替代
updata[i] -> forward[i] = s; // 原来的小于val的那个节点的后继为s
s -> sum[i] = updata[i] -> sum[i] - (tot[0] - tot[i]); // tot[0]-tot[i]是updata[i]到s的距离,s的sum就是 原来updata的差距分出了一部分给updata[i]到s(先把新插入的s挡上不看)。
updata[i] -> sum[i] = updata[i] -> sum[i] - s -> sum[i] + 1; // 虽然少了s -> sum[i]的一段,但是新插入了s,所以这个距离还要加1
}
for (int i = lay + 1; i <= level; i++) {
updata[i] -> sum[i]++; // 对于更高层的最大节点来说,又多了一个距离,所以要++
}
} -
(P1486)郁闷的出纳员[https://www.luogu.com.cn/problem/P1486]。SBT已经可以解决了,这里用跳跃表解决。
MIN是最低工资,ans是裁员个数,total是总员工数。
还是设置全局变量add记录增加工资量,增加工资k时,add += k;
插入员工k时,如果k大于等于MIN,则插入k-add,total++;
扣除工资k时,add -= k,在跳跃表中查找小于MIN-add的元素个数sum,删除所有小于的元素,ans+=sum,total-=sum;
查询第k大的数时,若k > total,则输出-1,不然查询第total-k+1小的数,加add后输出。
其他都很简单,对于删除操作只需要Find(Min-add)后,看tot[0]的值就是小于的数量,删除就是将每一行的头节点的forward更新为updata的forward,然后是更新head的sum,也是O(logn)的。
int Delete(int val) { // 删除所有小于val的元素
int sum = Find(val);
for (int i = 0; i <= level; i++) {
head -> forward[i] = updata[i] -> forward[i];
// 这里来看这个简略的示意图:
// 1234
// 1代表头节点2代表着一层的updata3代表updata[0]4代表2的forward
// 所以这一层大概长这样:
// 124
//3
// 3以及之前都删掉了,所以head的sum[i]其实是3(删掉3其实头就到3的位置了)到4之间的距离,这个距离为2到4的距离updata[i]和2到3(tot[0]-tot[i])之间的距离差,即下面的式子。
head -> sum[i] = updata[i] -> sum[i] - (tot[0] - tot[i]);
}
while (level > 0 && !head -> forward[level]) { // 删除空链
level--;
}
return sum;
}
-
标签:val,int,updata,sum,forward,跳跃,节点 来源: https://www.cnblogs.com/fansoflight/p/16269991.html