其他分享
首页 > 其他分享> > 跳跃表

跳跃表

作者:互联网

  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