跳跃表(skip list)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了跳跃表(skip list)相关的知识,希望对你有一定的参考价值。
参考技术A 我们知道二叉搜索算法能够高效的查询数据,但是需要一块连续的内存,而且增删改效率很低。跳表,是基于链表实现的一种类似“二分”的算法。它可以快速的实现增,删,改,查操作。
当我们要在该单链表中查找某个数据的时候需要的时间复杂度为O(n).
怎么提高查询效率呢?如果我们给该单链表加一级索引,将会改善查询效率。
如图所示,当我们每隔一个节点就提取出来一个元素到上一层,把这一层称作索引,其中的down指针指向原始链表。
当我们查找元素16的时候,单链表需要比较10次,而加过索引的两级链表只需要比较7次。当数据量增大到一定程度的时候,效率将会有显著的提升。
如果我们再加多几级索引的话,效率将会进一步提升。 这种链表加多级索引的结构,就叫做跳表 。
与二分查找类似,跳跃表能够在 O(㏒n)的时间复杂度之下完成查找,与红黑树等数据结构查找的时间复杂度相同,但是相比之下,跳跃表能够更好的支持并发操作,而且实现这样的结构比红黑树等数据结构要简单、直观许多。
跳跃表以有序的方式在层次化的链表中保存元素,效率和平衡树媲美:查找、删除、添加等操作都可以在对数期望时间下完成。跳跃表体现了“ 空间换时间 ”的思想,
从本质上来说,跳跃表是在单链表的基础上在选取部分结点添加索引,这些索引在逻辑关系上构成了一个新的线性表,并且索引的层数可以叠加,生成二级索引、三级索引、多级索引,以实现对结点的跳跃查找的功能。
与二分查找类似,跳跃表能够在 O(㏒n)的时间复杂度之下完成查找,与红黑树等数据结构查找的时间复杂度相同,但是相比之下,跳跃表能够更好的支持并发操作,而且实现这样的结构比红黑树等数据结构要简单、直观许多。
但是现在问题来了,上文我举的例子是一个很完美的跳跃表,它严格地按照二分法的思想建立了捷径。从理论上讲,单个跳跃表的层数按照严格二分的条件建立,层数就应该是 ㏒n 层(以2为底,n 为结点个数),但是在实际操作中,我们的数据会刚刚好是 2 的 n 次方吗?如果不是这么刚好的数据,没办法严格地二分,我要怎么开辟捷径呢?如果我对这个跳跃表进行插入或删除操作,破坏了严格地二分结构,又该怎么办?如果我们要强行解决这些问题,那就又要引入一大堆七七八八又难以实现的规则了,只怕这么做比建一棵树更为困难了。
既然我们没有办法很好地严格二分,也没有很好的规则去描述这些问题的处理方式,那么我们就不使用严格二分的方法就行了啊,不要一条绝路走到黑嘛!分析一下我们的目的,我们希望的事情是让查找操作的效率提升,如果我只开辟一条捷径,效率也确确实实是提升了的,如果能继续开辟捷径,如果最后我们能达到严格地二分,效率就会被提升,那也就是说我们并不是为了要实现二分,而是通过不断地努力去尽量地实现二分。
我们无法直接证明这个事实,但是我们可以通过“频率的稳定性”得到启发,提出了概率的定义,进而确定了事件的概率。从我举的例子里,我们不仅得到了启发,更是找到了解决问题的方法。也就是说,我们找到某种方式来实现捷径的开辟,这种方式在统计学的角度来说,可以往严格二分的情况趋近,在理论上实现 O(㏒n) 的时间复杂度。
跳跃表只需要从最上层开始遍历,由于每一层的链表都是有序的,因此当查找的“键”不存在于某一层中的时候,只需要在比查找目标的“键”要大的结点向下一次跳跃即可,重复操作,直至跳跃到最底层的链表。
1、先从顶层开始遍历,与16进行对比小,进入下一层。
2、与4进行比较,比4大,当前结点置为4结点,与16进行比较,进入下一层。
3、 与8进行比较,没有比8大,切换为当前结点4。
4、将节点4的下一个节点8和当前值进行比较,相同,取出。
1、函数实现向跳跃表中插入一个“键”为 key,“值”为 value 的结点。由于我们进行插入操作时,插入结点的层数先要确定因此需要进行抛硬币实验确定占有层数。
2、由于新结点根据占有的层数不同,它的后继可能有多个结点,因此需要用一个指针通过“键”进行试探,找到对应的“键”的所有后继结点,在创建结点之后依次修改结点每一层的后继,不要忘了给结点判空。在插入操作时,“键”可能已经存在,此时可以直接覆盖“值”就行了,也可以让用户决定,可以适当发挥。
寻找节点的位置,获取到插入节点的前一个节点,
3、与链表的操作执行相同的节点操作,地址替换。
模拟插入操作
首先我们需要用一个试探指针找到需要插入的结点的前驱,即用红色的框框出来的结点。需要注意的是,由于当前的跳跃表只有 2 层,而新结点被 3 层占有,因此新结点在第 3 层的前驱就是头结点。
接下来的操作与单链表相同,只是需要同时对每一层都操作。如图所示,红色箭头表示结点之间需要切断的逻辑联系,蓝色的箭头表示插入操作新建立的联系。
插入的最终效果应该是如图所示的。
由于需要删除的结点在每一层的前驱的后继都会因删除操作而改变,所以和插入操作相同,需要一个试探指针找到删除结点在每一层的前驱的后继,并拷贝。接着需要修改删除结点在每一层的前驱的后继为删除结点在每一层的后继,保证跳跃表的每一层的逻辑顺序仍然是能够正确描述。
1、根据删除的值找到当前值在跳表中的前驱结点 head 4
2、判断结点4的后驱结点的值是否为8,不是,直接跳出。当前值在跳表中不存在。
3、循环遍历每一层,执行地址变更。当前结点可能在其他层不存在结点,因此在变更的时候要判断是当前层是否存在该结点。
// 跳表中存储的是正整数,并且存储的数据是不重复的
public class SkipListTest
//最大索引层数
private static int MAX_LEVEL =16;
//头节点
private Node head;
//索引的层级数,默认为1
private int levelCount =1;
private Random random;
class Node
//结点值
private int value;
//当前节点的所有后驱节点。1-maxlevel 层。
private Node[]nodes =new Node[MAX_LEVEL];
//当前节点的层数
private int maxLevel;
public Node(int value,int maxLevel)
this.value = value;
this.maxLevel = maxLevel;
public Node get(int value)
//1、从最高层开始遍历
Node cur =head;
for (int i =levelCount-1; i >=0 ; i--)
//找到比该值小的那个结点
while (cur.nodes[i]!=null && cur.nodes[i].value < value)
cur = cur.nodes[i];
//开始寻找下一层,直到找到最后一层
if(cur.nodes[0]!=null&&cur.nodes[0].value == value)
return cur.nodes[0];
return null;
public void insert(int number)
//1、获取要插入的索引层数量
int level = randomLevel();
//2、创建新节点
Node newNode =new Node(number,level);
//3、获取每一层的前驱结点
Node update[] =new Node[level];
//遍历索引层
Node c =head;
for (int i =level-1; i >=0 ; i--)
while (c.nodes[i]!=null&&c.nodes[i].value
c = c.nodes[i];
update[i] = c;
//4、更新每一层的索引结构
for (int i =0; i
//当前结点的后驱结点
newNode.nodes[i] =update[i].nodes[i];
//当前结点的前驱
update[i].nodes[i] =newNode.nodes[i];
//5、更新索引层
if(levelCount
levelCount =level;
public void delete(int value)
//1、获取每一层比当前值小的前一个结点
Node[]update =new Node[levelCount];
Node p =head;
for(int i =levelCount -1; i >=0; --i)
while(p.nodes[i] !=null && p.nodes[i].value < value)
p = p.nodes[i];
update[i] = p;
//2、如果最后一层的结点的与当前值相同,进入变更指针操作。
if(p.nodes[0] !=null && p.nodes[0].value == value)
for(int i =levelCount -1; i >=0; --i)
//从最高层开始变更,如果值相等才进行变更
if(update[i].nodes[i] !=null &&update[i].nodes[i].value == value)
update[i].nodes[i] =update[i].nodes[i].nodes[i];
// 随机函数
private int randomLevel()
int level =1;
for(int i =1; i
if(random.nextInt() %2 ==1)
level++;
return level;
skip list - 跳跃表详注,常数小
跳跃表详注
具体看注释代码
luoguP3369: https://www.luogu.org/recordnew/show/11782419
1 #include<bits/stdc++.h> 2 #define repeat(a,b,c,d) for (int a=b;a<=c;a+=d) 3 using namespace std; 4 struct node{ 5 int nxt,dwn,jmp,val; 6 }a[100000 * 4]; 7 int al = 0,n,first; 8 const int maxdep = 9, INF = 1e9; 9 inline void build(){//在程序开始时调用,建造一个dep=maxdep的表 10 for (register int i=1;i<=maxdep;i++){//建造开始节点 11 a[++al].nxt = maxdep + i;//指向最后节点 12 if (i != 1) a[al].dwn = al-1;//非第一个节点指向下一个down节点 13 else a[al].dwn = -1;//最后一个 14 a[al].jmp = 0;a[al].val = -INF;//赋值,a[al].jmp可以任意赋。 15 } 16 first = al;//指向第一个节点 17 for (register int i=1;i<=maxdep;i++){ 18 a[++al].nxt = -1;//same 19 if (i != -1) a[al].dwn = al-1; 20 a[al].jmp = INF;a[al].val = INF; 21 } 22 } 23 //之后我们维护NOW指针。 24 inline int dfs_insert(int x,int NOW,int dep,int insdep){//为了常数优化,不先调用find_k_rank_and_p找位置,而是直接递归位置。 25 if (dep == 0) return 0;//边界 26 register int len_jmp = 0;//我们从现在NOW指针往前蹦的长度 27 while (x > a[a[NOW].nxt].val) len_jmp += a[NOW].jmp, NOW = a[NOW].nxt;//**记住,所有的skip list的搜索都是开区间的!** 28 register int lft_jmp = dfs_insert(x,a[NOW].dwn,dep-1,insdep);//搜到下一层 29 if (dep > insdep) a[NOW].jmp++;//如果没达到指定层数,只是NOW的jmp指针增加 30 else{ 31 a[++al].nxt = a[NOW].nxt;a[NOW].nxt = al;a[al].dwn = al-1;a[al].val = x;//维护指针 32 a[al].jmp = a[NOW].jmp - lft_jmp;a[NOW].jmp = lft_jmp + 1;//一并维护NOW 33 } 34 return len_jmp + lft_jmp;//返回跳过的层数。 35 } 36 inline void insert(int x){ 37 register int insdep = 1,random = rand()<<16|rand();//**千万不要rand来rand去,贼慢! 38 while (random%4 == 0) insdep++,random/=4;dfs_insert(x,first,maxdep,insdep);//亲测4层最快 39 } 40 inline pair<int,int> find_k_rank_and_p(int x){ 41 register int NOW = first,len = 0,dep = maxdep;//寻找x的rank并且返回NOW指针(全部开区间) 42 while (dep > 0){ 43 while (x > a[a[NOW].nxt].val) len += a[NOW].jmp, NOW = a[NOW].nxt;//能走则走 44 dep--;//维护层数 45 if (dep == 0) break;//为了维护NOW,一旦dep==0就break; 46 NOW = a[NOW].dwn; 47 } 48 return make_pair(len+1,NOW);//len是开区间,要+1 49 } 50 inline void del(int x){ 51 register int rank = find_k_rank_and_p(x).first,NOW = first,len = 0,dep = maxdep;//删除时先找出要删的位置,必须保证不多删。 52 while (dep > 0){ 53 while (x > a[a[NOW].nxt].val) len += a[NOW].jmp, NOW = a[NOW].nxt;//找 54 if (a[a[NOW].nxt].val == x && len + a[NOW].jmp == rank) // be del 55 a[NOW].jmp = a[NOW].jmp + a[a[NOW].nxt].jmp - 1,a[NOW].nxt = a[a[NOW].nxt].nxt;//维护NOW指针 56 else a[NOW].jmp--; 57 NOW = a[NOW].dwn; 58 dep--; 59 } 60 } 61 inline int find_rank_val(int x){ 62 register int NOW = first,len = 0,dep = maxdep; 63 while (dep > 0){ 64 while (x > len + a[NOW].jmp) 65 len += a[NOW].jmp, NOW = a[NOW].nxt;//能蹦则蹦 66 dep--; 67 if (dep == 0) break; 68 NOW = a[NOW].dwn; 69 } 70 return a[a[NOW].nxt].val;//返回值 71 } 72 int main(){ 73 srand(time(NULL)); 74 build(); 75 scanf("%d",&n); 76 for (int i=1;i<=n;i++){ 77 register int opt,x; 78 scanf("%d%d",&opt,&x); 79 if (opt == 1) insert(x);//插入 80 if (opt == 2) del(x);//删除 81 if (opt == 3) printf("%d ",find_k_rank_and_p(x).first);//查找x数位置 82 if (opt == 4) printf("%d ",find_rank_val(x));//查找rank为x的值 83 if (opt == 5) printf("%d ",a[find_k_rank_and_p(x).second].val);//求x的前驱 84 if (opt == 6) printf("%d ",a[a[find_k_rank_and_p(x+1).second].nxt].val);//求x的后缀 85 } 86 }
以上是关于跳跃表(skip list)的主要内容,如果未能解决你的问题,请参考以下文章
ElasticSearch实战-Skip List 跳表算法(文档定位跳跃算法)
ElasticSearch实战-Skip List 跳表算法(文档定位跳跃算法)