30-跳表(Skip List)

首先来思考一个问题。

一个有序链表(下图),搜索,添加,删除的平均时间复杂度是多少?

通过对链表这种数据结构的了解可以知道

  1. 搜索必须要从表头节点开始,依次往后搜索,直到搜索到为止。所以链表搜索的时间复杂度为O(n)
  2. 添加也是一样的,需要从左往右依次搜索,直到找到合适的插入位置为止,所以时间复杂度为O(n)
  3. 删除依然是从左往右依次搜索,找到需要被删除的元素后,将元素删除掉,因此时间复杂度为O(n)

那么,能否通过二分搜索来优化有序链表,将搜索,添加,删除的平均时间复杂度降低至O(logn)?

答案是不能,因为链表没有像数组一样的高效随机访问(O(1)时间复杂度),所以不能像有序数组一样,直接进行二分搜索优化, 只能从头开始,或者从尾开始依次搜索,直到找到需要找的元素。因此不能通过二分搜索来进行优化。

既然不能通过二分搜索对链表进行优化,那么,是否有其他办法,让有序链表的搜索,添加,删除的平均时间复杂度降低至O(logn),有的,跳表就可以实现。

跳表(Skip List)

跳表,又叫做跳跃表,跳跃列表,在有序链表的基础上增加的“跳跃”的功能,是由William Pugh于1990年发布,设置的初衷是为了取代平衡树(比如红黑树)

跳表对比平衡树,有以下特点:

  1. 跳表的实现和维护更加简单。相信大家都知道,红黑树的实现确实太复杂了,需要考虑很多的情况
  2. 跳表的搜索,删除,添加的平均时间复杂度是O(long)
使用跳表优化链表

例如,下图是一个非常普通的链表,而且该链表是有序的

那怎么来对链表进行优化呢?

现在,可以将链表中的每一个节点想象成为一个公交车站,其中数组为公交站的名字,现在如果想从最左边开始,到达25这个公交站,按照现在的公交路线,经过公交站的顺序是从左往右的,最终到达25公交站

那如何才能办到,减少经过中间公交站的次数呢?是否可以办到从3公交站直接到达25公交站呢?是可以的,如果现在开通一条快速公交,可以从3公交站直接到达25公交站的话,就不用经过中间的这些公交站,直接就到达25公交站了,这样就可以更快的到达最终想要达到的站,这就是对链表进行优化的基本思路。

所以对上图的链表进行初步优化的话,结果如下

这样,就在原来公交线路的基础上,增设了一条快速公交,减少了中间经过的站点,所以如果现在想要达到某个站的话,就可以优先选择快速路线,如此一来,就可以节省时间。

那么,能否再更快一点呢?更快一点则就是再增设一条经过站点更少的公交线路,这样从起点到达终点经过的站点更少,可以进一步节省时间,所以可以在原来快速路线的基础上,再提升一层,组成一个新的途径站点。重复压缩经过的站点,直到更优为止。所以上面的公交线路,经过优化后的结果如下

所以,上面的流程,就是链表的优化思路,其实就是在有序链表的基础上,增加了一个跳跃的功能

跳表的搜索

搜索流程如下:

  1. 从顶层链表的首元素开始,从左往右搜索,直到找到一个大于或等于目标的元素,或者到达当前层链表的尾部
  2. 如果该元素等于目标元素,则表明该元素已被找到
  3. 如果该元素大于目标元素或已经到达链表的尾部,则退回到当前层的前一个元素,然后转入下一层进行搜索

所以跳表的搜索实现如下:

public V remove(K key) {
    keyCheck(key);
    Node<K,V> node = first;
    Node<K,V>[] prevs = new Node[level];
    boolean exist = false;
    for (int i = level - 1; i >= 0 ; i--) {
        int cmp = -1;
        while (node.nexts[i] != null && (cmp = compare(node.nexts[i].key,key)) < 0 ) {
            node = node.nexts[i];
        }
        prevs[i] = node;
        if (cmp == 0) exist = true;
    }
    if (!exist) return null;//说明不存在
    Node<K,V> removedNode = node.nexts[0];
    for (int i = 0; i < removedNode.nexts.length; i++) {
        prevs[i].nexts[i] = removedNode.nexts[i];
    }
    size--;
    //更新跳表的层数
    int newLevel = level;
    while (--newLevel >0 && first.nexts[newLevel] == null ) {
        level = newLevel;
    }
    return removedNode.value;
}
跳表的添加

假设现在要往下面的链表中插入一个新的节点17(红色节点)

分析:

  1. 通过搜索,找到插入节点17的合适位置
  2. 随机确定节点的层数
  3. 更新对应层节点的next

根据思路,实现如下:

public V put(K key, V value) {
    keyCheck(key);
    Node<K,V> node = first;
    Node<K,V>[] prevs = new Node[level];//用来存放插入节点的前驱节点
    for (int i = level - 1; i >= 0 ; i--) {
        int cmp = -1;
        while (node.nexts[i] != null && (cmp = compare(node.nexts[i].key,key)) < 0) {
            node = node.nexts[i];//更新node
        }

        if (cmp == 0) {
            //节点本来就存在,覆盖key对应的value
            V oldValue = node.nexts[i].value;
            node.nexts[i].value = value;
            return oldValue;
        }

        //保存前驱节点
        prevs[i] = node;
    }
    //说明当前key 不存在,并且确定前驱节点为node
    int newLevel = randomLevel();
    Node<K,V> newNode = new Node<>(key,value,newLevel);
    //遍历所有的前驱节点,更新每一层的连线
    for (int i = 0; i < newLevel; i++) {
        if (i >= level) {//说明新节点的层数大于原来节点的最大层数,直接从头节点连线到当前节点
            first.nexts[i] = newNode;
        } else {
            newNode.nexts[i] =  prevs[i].nexts[i];
            prevs[i].nexts[i] = newNode;
        }
    }
    //更新跳表的层数
    level = Math.max(level,newLevel);
    size++;
    return null;
}
跳表的删除

流程如下:

  1. 搜索对应的节点
  2. 如果没搜索到,直接返回
  3. 如果搜索到了,更新对应层节点的next

根据思路,实现如下:

public V get(K key) {
    keyCheck(key);
    Node<K,V> node = first;
    //nexts的索引是0 - (level - 1)
    for (int i = level - 1; i >= 0 ; i--) {
        int cmp = -1;
        while (node.nexts[i] != null && (cmp = compare(node.nexts[i].key,key)) < 0 ) {
            //当前节点的key,小于要找的节点的ke||说明找到当前层的最后了,所以应该会退到上一个节点,然后往下一层继续找
            node = node.nexts[i];//更新node
        }

        if (cmp == 0) {
            //说明找到了
            return node.nexts[i].value;
        }
        //当前节点的key,大于要找的节点的key,进入下一层进行查找
        //返回到上一个节点,进入下一层继续查找
    }
    //直到最后,还没找到,说明不存在
    return null;
}
跳表的层数

关于跳表层数的相关结论

  • 跳表是按层构造的,底层是一个普通的有序链表,高层相当于是底层的“快速通道”
    • 在第i层中的元素按某个固定的概率P(通常为1/2或1/4)出现在第i + 1 层中,产生越高的层数,概率越低
      所以有以下结论
      1. 元素层数恰好等于1的概率为1 - P
      2. 元素层数大于等于2的概率为P,而元素层数恰好等于2的概率为P* (1 - P)
      3. 元素层数大于等于3的概率为P2,而元素层数恰好等于3的概率为P2* (1 - P)
      4. 元素层数大于等于4的概率为P3,而元素层数恰好等于4的概率为P3* (1 - P)
      5. ...
      6. 一个元素的平均层数是1 / (1- P)
  • 当P = 1 / 2时,每个元素所包含的平均指针数量是2
  • 当P = 1 / 4时,每个元素所包含的平均指针数量是1.33(从指针数量来看,跳表会比红黑树更节省内存,因为红黑树每个节点都有至少2个指针)
跳表复杂度分

通过前面的分析,可以计算出跳表每一层的元素数量

  1. 第一层(最底层)链表固定有n个元素
  2. 第二层链表平均有n * p个元素
  3. 第三层链表平均有n * p ^ 2个元素
  4. 第K层链表平均有n * p ^ K个元素

所以有以下结论:

  1. 最高层的层数为log(1/p) n,平均有1 / p个元素
  2. 在搜索时,每一层链表的预期查找步数最多是1 / P ,所以总的查找步数是 -(log(1/p) n),所以,时间复杂度为O(logn)

demo下载地址

完!

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

推荐阅读更多精彩内容