跳跃表(skip list)

引入


我们知道二叉搜索算法能够高效的查询数据,但是需要一块连续的内存,而且增删改效率很低。

跳表,是基于链表实现的一种类似“二分”的算法。它可以快速的实现增,删,改,查操作。


当我们要在该单链表中查找某个数据的时候需要的时间复杂度为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;

}

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容