ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

跳跃表

2022-05-14 14:35:12  阅读:202  来源: 互联网

标签:val int updata sum forward 跳跃 节点


  1. 参考:

    1. 算法训练营6.3

  2. 简介:

    1. 名称:跳跃表

    2. 本质:可以进行二分查找的有序链表。

    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都是利用跳跃表实现的。

  3. 操作:

    1. 数据结构定义 & 初始化:

      每个节点设置一个向右的和向下的指针,根据需要看看需不需要向左和向上的指针,构建四联表。

       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:
       }
    2. 查找:在跳跃表中查找元素x。

      算法步骤如下:

      1. 从最上层的头节点开始。

      2. 设当前位置为p,p的后继节点的值为y,若x=y,则查找成功;若x<y,则需要向下一层移动,若x>y,则向右移动一个位置继续查找。(如果p右边没有位置,则直接向下走)。

      3. 如果到达了底层之后还要往下走,则查找失败。

       

       // 查找小于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;
       }
    3. 插入:在跳跃表中插入一个元素,相当于在某一个位置插入一个高度为level的列,插入的位置通过查找决定,插入列的高度可以采用随机化的决策确定:

      1. 最开始设层次layer为0,表示不插入,

      2. 设定层数增加的概率P为0.5或0.25。

      3. 随机一个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;
        }
       }

      这样有了头节点之后,插入操作之后就像上面那个图一样了。

    4. 删除:跳跃表中删除一个元素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)。

  4. 例题:

    1. (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]++;  // 对于更高层的最大节点来说,又多了一个距离,所以要++
        }
       }
    2. (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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有