SkipList 跳表搜索
Redis的里面的sort-set(有序集)就是采用skiplist来实现的。skiplist的性能和Red-black差不多。算法实现比RBT要简单许多,改动的节点少,不涉及Re-Balance.
先形象的观察下skiplist的结构:
观察上图,可知skiplist的最高有4层,每层有序,第1层包含所有的节点。通过概率算法,可以控制层数越高的节点个数越来越少。那么跳表是如何来搜索的呢?比如在上图搜索17.
为了加快搜索,需要从最高层4开始搜索(因为最高层的节点少,容易定位),首先查看6节点,发现17比其大,向后搜索,发现6后面的节点指向了Nil(第4层),那么搜索的层数降低1层,
从此节点的第3层开始搜索,发现下个节点是25,大于17,那么再降低一层,从2层开始搜索,发现第2层是9,小于17,继续搜索,发现9节点的下一个数是17,搜索完成。总共查询了
4次,完成搜索(不包含NIL节点的访问。),这种情况下普通有序链表需要6次访问。可以设想下,如果层数为1层的话,那么此时跳表为最坏的情况,退化成有序单链表。复杂度O(n)。
搜索OK的话,那么Insert和Delete就没有太大问题,因为这两个操作都需要先搜索。
1、数据结构的定义
如上图中的E节点,表示的是头节点,一般跳表的实现,最大有多少层(MAX_LEVEL)是确定的。所以e的个数是固定的。
1 #define SKIPLIST_MAXLEVEL 32 2 #define ZSKIPLIST_P 0.25 3 4 typedef struct skiplistNode_t{ 5 void* value; 6 double score; 7 struct skiplistNode_t* forward[]; 8 }skiplistNode; 9 10 typedef struct skiplist{ 11 skiplistNode* head; 12 int level; 13 uint32_t length; 14 }skiplist;
MAX_LEVEL = log(1/p)N,带入上面的参数,那么N = 2^64.(具体参见William Pugh的论文)。
struct skiplistNode_t* forward[];在MS的编译器上估计会报错,GUN C支持此扩展特性,这种用法在Redis的源码中比较常见。(推荐Code Block编译器)。
初始化代码:调用此语句skiplist* my_sl = slCreate();内存空间就会把固定的e部分生成。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 skiplistNode* createNode(int level,void* value,double score) 2 { 3 skiplistNode *slNode = (skiplistNode*)malloc(sizeof(*slNode)+level*sizeof(skiplistNode)); 4 if(!slNode) 5 { 6 return NULL; 7 } 8 slNode->value = value; 9 slNode->score = score; 10 return slNode; 11 } 12 13 skiplist* slCreate(void) 14 { 15 int i; 16 skiplist* sl = (skiplist*)malloc(sizeof(*sl)); 17 if(!sl) 18 { 19 return NULL; 20 } 21 sl->length = 0; 22 sl->level = 1; 23 sl->tail = NULL; 24 sl->head = createNode(SKIPLIST_MAXLEVEL,NULL,0.0); 25 for(i=0;i<SKIPLIST_MAXLEVEL;i++) 26 { 27 sl->head->forward[i] = NULL; 28 } 29 return sl; 30 }
2、Insert operation
在上图中,需要插入17节点。其中update[i]保存待插入点的位置。
备注:插入的时候,会调用一个randomLevel的函数,他可以概率返回1~MAX_LEVEL之间的值,但是level的值越大,概率越小。
判断是否存在相同的score,是通过forwar[0]来判断的,因为level=1存放的是所有的节点。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 skiplistNode *slInsert(skiplist *sl, void* value, double score) 2 { 3 skiplistNode* sn,*update[SKIPLIST_MAXLEVEL]; 4 int i, level; 5 sn = sl->head; 6 for(i=sl->level-1;i>=0;i--) 7 { 8 while(sn->forward[i]&&(sn->forward[i]->score<score)){ 9 sn = sn->forward[i]; 10 } 11 update[i] = sn; 12 } 13 if(sn->forward[0]&&sn->forward[0]->score == score) 14 { 15 printf("insert failed,exist!!\n"); 16 return NULL; 17 } 18 19 level = slRandomLevel(); 20 printf("score:%.2lf level:%d\n",score,level); 21 if(level>sl->level){ 22 for(i=sl->level;i<level;i++) 23 { 24 update[i] = sl->head; 25 } 26 sl->level = level; 27 } 28 sn = createNode(level,value,score); 29 for(i=0;i<level;i++) 30 { 31 sn->forward[i] = update[i]->forward[i]; 32 update[i]->forward[i] = sn; 33 } 34 sl->length++; 35 return sn; 36 }
2、Delete Operation
删除的时候和插入相同,都是先搜索。唯一注意的一点是删除的节点也许要修改skiplist->length的值。
备注:也许存在level=MAX_LEVEL的节点个数大于等于2,所以按下面的更新length是安全的。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 int slDelete(skiplist *sl, double score) 2 { 3 skiplistNode *update[SKIPLIST_MAXLEVEL]; 4 skiplistNode *sn; 5 int i; 6 sn = sl->head; 7 for(i=sl->level-1;i>=0;i--) 8 { 9 while(sn->forward[i]&&sn->forward[i]->score<score){ 10 sn = sn->forward[i]; 11 } 12 update[i] = sn; 13 } 14 sn = sn->forward[0]; 15 if(sn->score != score) 16 { 17 return -1; 18 } 19 for(i=0;i<sl->level;i++) 20 { 21 if(update[i]->forward[i] != sn){ 22 break; 23 } 24 update[i]->forward[i] = sn->forward[i]; 25 } 26 free(sn); 27 while(sl->level>1&&sl->head->forward[sl->level-1] == NULL){ 28 sl->level--; 29 } 30 sl->length--; 31 return 0; 32 }
3、Free SkipList
销毁整个调表的时候,从level=1销毁即可,别忘记释放head和skiplist。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void slFree(skiplist *sl) 2 { 3 skiplistNode* sn = sl->head,*st; 4 st = sn->forward[0]; 5 free(sn); 6 while(st) 7 { 8 sn = st->forward[0]; 9 free(st); 10 st = sn; 11 } 12 free(sl); 13 }
总结:
平均复杂度 | 最坏复杂度 | |
空间代价 | O(n) | O(n*log n) |
查询操作 | O(log n) | O(n) |
插入操作 | O(log n) | O(n) |
删除操作 | O(log n) | O(n) |