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.这种插入果断显示不出来任何随机性。

跳表缺陷:

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

posted @ 2013-04-27 22:30  Wang Biao NEU  阅读(2394)  评论(3编辑  收藏  举报