• Skip List——跳表,一个高效的索引技术


    对于一个查询任务,如果不能开辟连续空间进而采样二分查找进行处理的话,通常各种树,各种碉堡的更高级的数据结构就会被提出,用来快速进行数据查询。

    在这里,我无意显示自己的高人一等或是弱智一面,但是对于常见的数据结构,用于动态处理一组数据的索引构建工作,采样的多个数据结构都面临一个复杂编程过程,程序难以调试,算法难以理解的问题,而跳表——可以在满足O(LogN)的查询复杂度的前提下,十分简单的完成数据查询。

    通常对于动态数据的搜索,红黑树等平衡二叉树,通过模拟二分搜索过程,可以达到一个查询代价为O( log N)的结果,而编程过程相当容易的链表,查询代价确是惊人的O(N),所以一个简单的思路就出现了,通过对链表进行修改,使其能够通过空间上增加节点数据而获得时间上缩短查询时间——这就是跳表的思想。

    跳表是由William Pugh发明。他在 Communications of the ACM June 1990, 33(6) 668-676 发表了Skip lists: a probabilistic alternative to balanced trees,在该论文中详细解释了跳表的数据结构和插入删除操作。

    William Pugh是University of Maryland, College 大学的一名教授。

    在MIT的算法导论公开课上,Erik Demaine给出了一个很有创意而且很容易理解的例子——地铁系统。

    如果一个城市中,地铁有快车和慢车,快车能够快速的从一个站到达另一个站,而中间并不停,慢车就想普通的链表一样,每个站都要停(很有我经常坐的7374次列车的风范)。这样,首先通过快车到达目的地的附近,然后通过慢车倒车,到达目的地,这就是skiplist如何进行搜索的原理。

    因为我们计算机并不需要考虑动车的调度问题,所以如果感觉快车线路不够快,可以再搭建超级快车线路,超级无敌快车线路,超级无敌风火轮快车线路,火箭快车线路,,,,,

    只要我们能够良好的在c++中管理这些指针(这正是我的代码的问题),我们就可以得到一个最优的乘车线路设计结果。

    下面给出一个完整的跳表的图示:

    如果我们刚刚进入沈阳市,这时候我们位于点e,而想要去21号站台,作为高富帅的你们,肯定不会在乎车费这些毛毛雨的问题,所以选择地铁一号线,坐到6号站下车(一号线太贵了,沈阳只修了一站)。

    我们知道目的地21号站在6号站的前面,所以从6号站下车的你们,选择二号线继续前行,一口气坐到25号站(baidu的地图应用此时只能在你坐过站的时候提醒你)。所以,我们回到6号站,坐三号线。。。(有钱也不是这么花的啊)

    从三号线(沈阳的三号线现在还没有影子呢)出发,一路经过9号站,17号站,又到了25号站(现在一定会对baidu地图的开发人员进行意念上的人身攻击了),在一通纠结之后,回到17号站。

    这时候baidu地图提示我们,17号站之下就是公交了,如果在坐公交的过程中,一直到25号站都没有到达目的地,这时候只能说明你拿了过期地图。。。不过我们经过两站公交之后,成功到达21号站。

    进入沈阳我们投了9个钢镚,才成功到达21号站台,似乎在进入沈阳之后直接坐公交才花8块钱,还能省一个钢镚去买中街大果。。。。。。不过,skiplist是一个统计学上的最优结果,例子如果这个网络是整个中国,这就完全不同了。

    上面写的都是如何搜索,但是搜索在一个数据结构中是最简单的操作,增删这两个操作才是最为让我们关注的,而这才是skiplist的精髓所在,通过一个概率为50%的过程判断一个节点是否提高到上一个层次上,通过理论分析,我们可以得到一个惊人的结论——O(logN)的期望查询代价。

    代价分析:

    这个过程很容易理解,但是它真的能够为我们提供一个O(logN)的期望查询代价的数据结构吗?

    对于一个长度为N的链表,由于我们按照0.5的概率进行节点的向上升级,则上一层的节点数目的期望就是N/2,更高一层的节点数目期望是N/4,以此类推,则整个链的高度的期望就是logN。

    和各种树的查询结构一样,跳表的查询代价分析也是与跳表的高度相关的,假如两个链,长度分别是L(1)和L(2),则对于一个查询的最大代价就是:

    COST = L(2) + L(2)/L(1)

    COST = L(3) + L(3)/L(2) + L(2)/L(1) 

    对于更长的链都采用相同的代价分析,这样就可以明显看出,当这些量都相同的时候,COST获得最小值。

    对于一个跳表,存在k个链,则他的最小查询代价是K*[N^(1/K)]

    之前的代价是一个查询的最大查询代价,而得到的 K*[N^(1/K)] 是这个最大代价的最小值,但是通过摊还分析,这个代价可以作为整个平均代价进行处理,可以认为一个查询的平均代价是K*[N^(1/K)]。将K的值带入其中,最终得到代价的表达式。

     

    这样,对于跳表这个结构,查询时间代价是O(logN),具体为2logN。所有表的节点数目为N+logN,其中新加入的索引大小为logN。

    查询过程:

    对于一个查询值value,我们从链的最高层开始进行查找,在代码中就是BEGIN节点。

    1.在链L中一直向前走,直到这个节点为value或是NULL或是这个节点的值超过value。

    2.如果节点值为value,向下搜索直到最低一层。返回这个节点;不然,进入3.

    3.向前返回一个节点,进入下一层链L,如果链L存在,回到1继续探测——其中探测范围为上一层向下传递的两个节点的elem值;如果不存在,返回NULL。

    插入过程:

    skiplist是一个对排序的表进行处理的结构,每个节点插入的时候,插入最低一层,然后找出一个硬币来获得50%的概率,0表示不采取任何行动,1表示将这个节点向上提高一层,如果提高一层之后再次抛硬币,直到得到一个0才停止。。。。

    如果想要插入一个值value,插入过程如下:

    1.首先在已有跳表中进行查询,如果这个点存在,就放弃插入操作,如果不存在,创建新节点,并且返回应该插入的节点(这个过程的代码很难与查询过程完美对接,怎么整都有问题,降低重复代码这个过程任重而道远啊)。

    2.将节点插入,然后加入一个随机过程,以0.5的概率将这个节点向上提高一层,过程直到停止提高操作。

    3.提高如果新建一层链,更新BEGIN。

    删除操作:

    对于一个节点只value,想要删除这个节点的对象。

    1.用search过程找到这个节点,对于返回值处理。

    2.向上搜索获得最高层。

    3.依次删除各个层上的节点,如果删除导致层数改变,更新BEGIN。

    可以看出除了插入过程显得与众不同之外,其他的各个方面都与常见的链表操作相同,所以我们可以通过上述的分析写出一个简单的跳表的实现。

    View Code
      1 #include <iostream>
      2 #include <random>
      3 using namespace std;
      4 #define MINELEM -2147483648
      5 class Node;
      6 class Node{
      7 public:
      8     friend void print();
      9     Node* getForward(){return forward;}
     10     Node* getBackward(){return backward;}
     11     Node* getUplevel(){return uplevel;}
     12     Node* getDownlevel(){return downlevel;}
     13 
     14     void setForward(Node *obj){forward = obj;}
     15     void setBackward(Node *obj){backward = obj;}
     16     void setUplevel(Node *obj){uplevel = obj;}
     17     void setDownlevel(Node *obj){downlevel = obj;}
     18 
     19     void setLevel(int a){level = a;}
     20     int getLevel()const{return level;}
     21     int getElem()const{return elem;}
     22     Node():forward(NULL),backward(NULL),uplevel(NULL),downlevel(NULL),elem(0){}
     23     Node(int a):forward(NULL),backward(NULL),uplevel(NULL),downlevel(NULL),elem(a){}
     24     Node(const Node &obj):forward(obj.forward),backward(obj.backward),uplevel(obj.uplevel),downlevel(obj.downlevel){
     25         elem = obj.getElem();
     26     }
     27     Node(Node *forward_ptr,Node *backward_ptr,Node *uplevel_ptr,Node *downlevel_ptr,int value){
     28         forward = forward_ptr;
     29         backward = backward_ptr;
     30         uplevel = uplevel_ptr;
     31         downlevel = downlevel_ptr;
     32         elem = value;
     33     }
     34     Node& operator=(const Node &obj){
     35         forward = obj.forward;
     36         backward = obj.backward;
     37         uplevel = obj.uplevel;
     38         downlevel = obj.downlevel;
     39         elem = obj.getElem();
     40         level = obj.getElem();
     41         return* this; 
     42     }
     43 private:
     44     Node *forward,*backward,*uplevel,*downlevel;
     45     int elem;
     46     int level;
     47 };
     48 static Node *START = new Node(NULL,NULL,NULL,NULL,MINELEM);
     49 static Node *BEGIN = START;
     50 Node* Levelup(Node &obj){
     51     Node *p = &obj;
     52     Node *new_obj = new Node(obj);    
     53     obj.setUplevel(new_obj);
     54     new_obj->setDownlevel(&obj);
     55     while((p->getElem())!= (START->getElem())){
     56         p = p->getBackward();
     57     }//find the next level up position
     58     if(p->getUplevel() == NULL)
     59     {
     60         Node *q = new Node(*START);
     61         q->setDownlevel(p);
     62         p->setUplevel(q);
     63         q->setUplevel(NULL);
     64         BEGIN = q;
     65         new_obj->setBackward(q);
     66         q->setForward(new_obj);
     67         new_obj->setUplevel(NULL);
     68         new_obj->setForward(NULL);
     69         return new_obj;
     70     }
     71     else{
     72         p = obj.getBackward();
     73         new_obj->setUplevel(NULL);
     74         while(p->getUplevel() == NULL)
     75             p = p->getBackward();
     76         p = p->getUplevel();
     77         Node *q = p->getForward();
     78         new_obj->setBackward(p);
     79         p->setForward(new_obj);
     80         new_obj->setForward(q);
     81         if(q != NULL){
     82             q->setBackward(new_obj);
     83         }
     84         return new_obj;
     85     }
     86 }
     87 Node* Search(int value){
     88     Node *p = START,*q = BEGIN;
     89     while(q->getDownlevel()!=NULL){
     90         p = q;
     91         while(p->getForward()!=NULL && p->getElem()<value){
     92                 p = p->getForward();
     93         }
     94         if(p->getElem() == value){
     95             while(p->getDownlevel()!=NULL)
     96                 p = p->getDownlevel();
     97             return p;
     98         }
     99         else {
    100             q = p->getDownlevel()->getBackward();
    101         }
    102         //search the next level
    103     }//serach part return the former object!
    104     if(q->getDownlevel() != NULL){
    105         if(p->getForward() != NULL){
    106             q = p->getBackward()->getDownlevel();
    107             while(q->getElem()!=p->getElem()){
    108                 if(q->getElem() == value)
    109                     return q;
    110                 else q = q->getForward();
    111             }
    112         }
    113         else {
    114             p = p->getDownlevel();
    115             while(p!=NULL){
    116                 if(p->getElem() == value)
    117                     return p;
    118                 else p = p->getForward();
    119             }
    120         }
    121     }//there are many level
    122     else {
    123         while(p!=NULL){
    124             if(p->getElem() == value)
    125                 return p;
    126             else p = p->getForward();
    127         }
    128     }//there only one level
    129     return NULL;
    130 }
    131 int Insert(int value){
    132     Node *p = START,*q = BEGIN;
    133     if(Search(value) == NULL){
    134         Node *obj = new Node(value);
    135         while(q!=NULL){
    136             p = q;
    137             while(p->getForward()!=NULL){
    138                 if(obj->getElem()>p->getElem())
    139                 {
    140                     p = p->getForward();
    141                 }//end if
    142                 else break;
    143             }
    144             if(p->getBackward()==NULL)
    145                 break;
    146             if(p->getBackward()->getDownlevel() != NULL)
    147                 q = p->getBackward()->getDownlevel();
    148             else break;
    149             //search the next level
    150         }//serach part return the former object!
    151         if(p->getElem() == START->getElem())
    152             q = p->getForward();
    153         else {
    154             q = p;
    155             p = p->getBackward();
    156         }
    157         if(q!=NULL){
    158             if(q->getElem()>value){
    159                 q->setBackward(obj);
    160                 p->setForward(obj);
    161                 obj->setBackward(p);
    162                 obj->setForward(q);
    163             }
    164             else {
    165                 q->setForward(obj);
    166                 obj->setBackward(q);
    167             }
    168         }
    169         else {
    170             p->setForward(obj);
    171             obj->setBackward(p);
    172         }
    173         int r = rand();
    174         Node *temp = obj;
    175         while(r%2 == 0){
    176             temp = Levelup(*temp);
    177             r = rand();
    178         }
    179         return 1;
    180     }//end if value do not exist
    181     else {
    182         return 0;
    183     }
    184 }
    185 int Delete(int value){
    186     Node *temp = Search(value);
    187     if(temp == NULL)return -1;
    188     else{
    189         Node *p = temp;
    190         while(p->getUplevel()!=NULL)p = p->getUplevel();
    191         while(p != NULL){
    192             if(p->getBackward()->getElem() == START->getElem() && p->getForward()==NULL){
    193                 Node *q;
    194                 if(p->getBackward()->getDownlevel()!=NULL){
    195                     p->getBackward()->getDownlevel()->setUplevel(NULL);
    196                     q= p->getDownlevel();
    197                 }
    198                 if(BEGIN != START){
    199                     BEGIN = p->getBackward()->getDownlevel();
    200                     delete p->getBackward();                
    201                     delete p;
    202                     p = q;
    203                 }
    204                 else{
    205                     BEGIN->setForward(NULL);
    206                     START->setForward(NULL);
    207                     delete p;
    208                     p = NULL;
    209                 }//there only one node exist
    210             }//end if, 
    211             else {
    212                 if(p->getForward() != NULL)
    213                     p->getForward()->setBackward(p->getBackward());
    214                 p->getBackward()->setForward(p->getForward());
    215                 Node *q = p->getDownlevel();
    216                 delete p;
    217                 p = q;
    218             }
    219         }
    220     }
    221 }
    222 void print(){
    223     cout<<"PRINT!"<<endl;
    224     Node *q =BEGIN,*p = START;
    225     while(p!=NULL){
    226         cout<<p->getElem()<<"    ";
    227         p = p->forward;
    228     }
    229     cout<<endl;
    230     p = START->uplevel;
    231     q = p;
    232     while(p!=NULL){
    233         if(q != NULL){
    234             cout<<q->getElem()<<"    ";
    235             q = q->getForward();
    236         }
    237         Node *t = START->getForward();
    238         while(q != NULL){
    239             Node *temp = q->getBackward();            
    240             while(t->getElem()!=q->getElem()){
    241                 cout<<"    ";
    242                 t = t->getForward();
    243             }
    244             cout<<q->getElem();
    245             q = q->getForward();
    246         }
    247         p = p->uplevel;
    248         q = p;
    249         cout<<endl;
    250     }
    251 }
    252 
    253 int main(){
    254     int i;
    255     cin>>i;
    256     while(i--){    
    257         print();
    258         Insert(i);
    259     }
    260     char c;
    261     cout<<"i--Insert    d--Delete"<<endl;
    262     print();
    263     while(cin>>c>>i){
    264         switch(c){
    265         case 'd':
    266             cout<<Delete(i)<<endl;
    267             print();
    268             break;
    269         case 'i':
    270             cout<<Insert(i)<<endl;
    271             print();
    272             break;
    273         default:
    274             break;
    275         }
    276         cout<<"i--Insert    d--Delete"<<endl;
    277     }
    278     return EXIT_SUCCESS;
    279 }

    这是我的一个实现,问题有几个,但是近期并不打算修改了,实验室老板肯定不能容忍我这样逍遥法外。
    1.跳表的数据结构的设计就有问题,听着MIT算法导论的视频突击写的程序,今天分析的时候才发现,根本不需要复制对象,仅仅复制指针就可以了。

    2.跳表的头结点选择一个无穷小进行初始化,作为永恒的老小,但是忘记选择一个无穷大作为永远的老大,这就如同红黑树,设置一个NIL好处多多,可以极大地改进原有的逻辑。

    3.C++的伪随机过程真是坑爹,而且更加关键的是,我懒得整一大堆随机数进行输入,所以跳表的实现总是效果很差,如果跳表太大,又不是特别好看。

    不过整个程序就是为了验证跳表的操作,不是为了验证跳表的碉堡,就让那些完美主义的想法先压在心底吧,,,

    程序实现结果:

    8
    PRINT!
    -2147483648
    PRINT!
    -2147483648     7
    PRINT!
    -2147483648     6       7
    PRINT!
    -2147483648     5       6       7
    -2147483648     5
    -2147483648     5
    PRINT!
    -2147483648     4       5       6       7
    -2147483648     4       5
    -2147483648     4       5
    -2147483648     4
    -2147483648     4
    -2147483648     4
    PRINT!
    -2147483648     3       4       5       6       7
    -2147483648             4       5
    -2147483648             4       5
    -2147483648             4
    -2147483648             4
    -2147483648             4
    PRINT!
    -2147483648     2       3       4       5       6       7
    -2147483648                     4       5
    -2147483648                     4       5
    -2147483648                     4
    -2147483648                     4
    -2147483648                     4
    PRINT!
    -2147483648     1       2       3       4       5       6       7
    -2147483648                             4       5
    -2147483648                             4       5
    -2147483648                             4
    -2147483648                             4
    -2147483648                             4
    i--Insert       d--Delete
    PRINT!
    -2147483648     0       1       2       3       4       5       6       7
    -2147483648                                     4       5
    -2147483648                                     4       5
    -2147483648                                     4
    -2147483648                                     4
    -2147483648                                     4
    d 4
    0
    PRINT!
    -2147483648     0       1       2       3       5       6       7
    -2147483648                                     5
    -2147483648                                     5
    i--Insert       d--Delete
    i 4
    1
    PRINT!
    -2147483648     0       1       2       3       4       5       6       7
    -2147483648                                             5
    -2147483648                                             5
    i--Insert       d--Delete

    即使不看代码也能够猜出,我的插入过程就是输入一个数,然后不断插入直到0.这种插入果断显示不出来任何随机性。

    跳表缺陷:

    对于跳表,删除操作绝对是一大硬伤,而且似乎找不到什么修改的办法,因为插入过程尽管还可以随机插入(通过随机插入过程),但是删除操作可是不能随机删除的,跳表在一定数目的节点删除之后就会失去原有的随机性,而如果采用再一次的随机过程进行每个节点的提升层次,这只会导致一个结果——代价太高,让我们用链表吧!

  • 相关阅读:
    《Three.js 入门指南》3.1.1
    《Three.js 入门指南》3.1.1
    《Three.js 入门指南》3.1.1
    《Three.js 入门指南》3.1.1
    《Three.js 入门指南》3.1.1
    《Three.js 入门指南》3.0
    《Three.js 入门指南》2.4.1- 照相机
    《Three.js 入门指南》2.3.1- 照相机
    《Three.js 入门指南》2- 照相机
    《Three.js 入门指南》1.3
  • 原文地址:https://www.cnblogs.com/wangbiaoneu/p/SkipList.html
Copyright © 2020-2023  润新知