前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如果面试官问你“跳表”,这一篇直接往他嘴里怼就行了

如果面试官问你“跳表”,这一篇直接往他嘴里怼就行了

作者头像
老李秀
发布2021-05-13 10:27:06
9770
发布2021-05-13 10:27:06
举报
文章被收录于专栏:可能是东半球最正规的API社区

大家好啊,摸完3月的?,果然4月要加倍还回去。前段时间脑子一热,我翻译了一篇《Skip Lists: A Probabilistic Alternative to Balanced Trees》。

我记得上大学的时候,菜鸡如我,能用数组解的题我从不用链表,因为这种指针指来指去的东西,真的费脑。同寝室有个大佬,那时候就会玩什么十字链表、红黑树了, 而我那时候天天lol,酷爱VN,嘴里时常念叨着一句让我们来猎杀那些陷入黑暗中的人吧= =。而今,做了5年多curd,也逐渐意识到,了解点数据结构,确实能开阔一些思维,有些时候处理业务问题的时候,说不定能用上(虽然可能性很小)。所以,今天我们就来聊聊这个niu bi的数据结构——跳表。

先来说说为什么跳表niu bi。因为它在绝大多数情况下能代替平衡树,而且实现起来简单,性能还不比平衡树家族各位差(AVL树,红黑树,这些我都不懂)。我想试问下,比如面试官让你手写树的前序遍历、中序遍历、后序遍历,假如你回忆半天,好不容易用递归的方式写出来了,面试官继续刁难,说不要用递归,你换种方式给我实现。是不是瞬间就崩溃了,可见非递归代码来操作树的复杂度有多高,反正我感觉我身边应该基本上没有能手写的。但是跳表不一样,它简简单单,在你仔细读完我这篇文章的时候,百分百能手写出跳表的查询、插入、删除。

像redis这么牛逼的项目,都是用跳表来实现ZSet(有序集合)的。

跳表的起源

我们先从一个链表说起,对于一个单向链表,如果要你查找链表当中的一个元素,那么时间复杂度是O(n),我们只能从头节点开始遍历,直到找到为止。

如图a所示

那我们加以改造,每隔一个节点,我们增加一个指向下一个的下一个节点的指针。这样子,是不是我们查找的效率就可以提升到O(n/2 +1)了。如果b所示

我们再进一步,再每四节点加上指向前面4节点的指针,那么我们时间复杂度就更进一步降低为O(n/4 +2)了 如果图c所示

所以每2^i个节点都有指向前面2^i的前向指针(如图d),可以让我们查找变得方便跟快速,但是插入跟删除节点就日了狗了,超级难维护。

基于这样的背景,1990年有个叫William Pugh的计算机科学家,就发明了跳表。我们先来了解一个叫做level的概念,一个链表的节点有k个前向指针,那么就是level k。下图e的6节点就是level 4,9节点是level 3。如果我们每插入一个节点的时候,随机一个level,后续无需更改, 这种带有跳过额外节点的链表数据结构,就叫做跳表。就长的像下图e一样。

论文结论

在精通跳表算法之前,我们先来记住一些概念跟公式。

每个跳表有个头结点head和一个尾结点NIL,跳表初始化的时候level为1,然后头节点指向NIL。每个跳表都有一个maxLevel, 还有一个listLevel(当前跳表所有节点中最高的level值),maxLevel的定义就是当前跳表能达到的最高level。论文作者经过严密的数学计算,让我们选择一个常量p ,且p为0.25最佳。

maxLevel = log(1/p) n。

其中n代表跳表元素的个数。比如我们要存65536个元素,那么

maxLevel=log(1/0.25)65535 = 8。

我们每个节点的level就是会从1-8中通过随机算法产生一个。至于为什么要这样,别问,我也不懂,结论就是这样。当然你也可以去通读这篇论文,作者给出了详细的数学证明过程。还有,跳表并不是无敌的,这种随机性level的做法,在极小极小概率的情况下会导致性能很差,因为这概率非常小,小到可以忽略不计。还有,跳表的时间复杂度为O(logN)。

查找算法

比如我们要搜索上图e的节点17

我们从头节点的最高层level 4开始往前,前面是6 小于17,继续向前,6的level 4指针指向NIL 所以我们回来,在6节点减少到level 3,这时候往前是25,25大于17,我们继续减少level,6的level 2指向9,9小于17,继续从9的level2 往前,就搜到了17。

总结起来从头结点的最高层level开始搜索,遇到比他大的就减少level,一直遍历下去,直到找到为止。

插入&&删除

既然我们精通了查询,那么其实插入跟删除就是先查询,然后再拼接即可。

比如我们要插入一个17的节点,我们使用搜索算法,然后随机生成一个level值(随机算法下文会介绍),然后我们还要根据搜索路径保留一个update[i]的数组向量。这个update数组里面记录什么呢,比如刚才插入17这条搜索路径,update数组记录了搜索路径每层level最后的那个node节点,比如level 4最后那个是node 6。(数组下标从0开始,所以level4的跳表为update[3])

update[3] = node 6

update[2] = node 6

update[1] = node 9

update[0] = node 12

这时候准备工作都已经完毕,然后怎么拼接呢。这时候我们要随机生成一个level。比如这个17 生成的level为2 那么我们只需要这样拼接

17节点的level 2的next = node 9 level 2的next(update[1])

node 9的level 2 next指向node 17的level 2

node 17 level1的next = node 12 level 1的next (update[0])

node 12的的level 1 next指向 node 17的level 1

就像上图我标红的所示。

这时候有聪明的小伙伴要问了。安尼特老师,安尼特老师,你不是说level随机么,那我生成一个level 更高的,那怎么办呢。这简单。我们只要把大于当前跳表的level 都从头节点指向它。就像下图一样。

删除就更简单了,直接查询找到该节点,然后替换掉就行了。比如我们要删除节点17,我们只需要查找到17。把每层指向17节点的,直接指向下一个节点就行了。

比如这里,9的level 2就指向25了 12的level1直接指向19

但是有一种特殊情况比如我们删掉了一个节点,我们需要检查下,如果当前跳表的头节点的listLevel层指针指向NIL,那么我们需要把当前listLevel-1

talk is cheap show me the code

鉴于跳表的伪代码可能看起来吃力一些,我就用java代码演示。这样更容易理解。

代码语言:javascript
复制
package com.company.skiplist;

public class SkipList<T> {

    // 跳表能达到的最大level
    private final int MAX_LEVEL;
    // 当前跳表所有节点的最大level
    private int listLevel;
    // 表头
    private SkipListNode<T> listHead;
    // 表尾
    private SkipListNode<T> NIL;
    // 生成randomLevel用到的概率值
    private final double P;
    // 论文里给出的最佳概率值
    private static final double OPTIMAL_P = 0.25;

    public SkipList() {
        // 假如我们的跳表最大元素个数为65536个
        // 那么p = 0.25 MAX_LEVEL = 8
        // 因为java没有直接log能自定义底数的函数所以用这样计算 log(1/p) n = ln(65536)/ln(1/0.25) 这个高中应该学过
        this(OPTIMAL_P, (int) Math.ceil(Math.log(65536) / Math.log(1 / OPTIMAL_P)));
    }

    public SkipList(double probability, int maxLevel) {
        P = probability;
        MAX_LEVEL = maxLevel;

        listLevel = 1;
        listHead = new SkipListNode<T>(Integer.MIN_VALUE, null, maxLevel);
        NIL = new SkipListNode<T>(Integer.MAX_VALUE, null, maxLevel);
        for (int i = listHead.forward.length - 1; i >= 0; i--) {
            listHead.forward[i] = NIL;
        }
    }

    // 内部类
    class SkipListNode<T> {
        int key;
        T value;
        SkipListNode[] forward;

        public SkipListNode(int key, T value, int level) {
            this.key = key;
            this.value = value;
            this.forward = new SkipListNode[level];
        }
    }

    public T search(int searchKey) {
        SkipListNode<T> curNode = listHead;

        for (int i = listLevel - 1; i >= 0; i--) {
            while (curNode.forward[i].key < searchKey) {
                curNode = curNode.forward[i];
            }
        }

        curNode = curNode.forward[0];
        if (curNode.key == searchKey) {
            return curNode.value;
        } else {
            return null;
        }
    }

    public void insert(int searchKey, T newValue) {
        SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL];
        SkipListNode<T> curNode = listHead;

        for (int i = listLevel - 1; i >= 0; i--) {
            while (curNode.forward[i].key < searchKey) {
                curNode = curNode.forward[i];
            }
            // curNode.key < searchKey <= curNode.forward[i].key
            update[i] = curNode;
        }

        curNode = curNode.forward[0];

        if (curNode.key == searchKey) {
            curNode.value = newValue;
        } else {
            int lvl = randomLevel();

            if (listLevel < lvl) {
                for (int i = listLevel; i < lvl; i++) {
                    update[i] = listHead;
                }
                listLevel = lvl;
            }

            SkipListNode<T> newNode = new SkipListNode<T>(searchKey, newValue, lvl);

            for (int i = 0; i < lvl; i++) {
                newNode.forward[i] = update[i].forward[i];
                update[i].forward[i] = newNode;
            }
        }
    }

    public void delete(int searchKey) {
        SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL];
        SkipListNode<T> curNode = listHead;

        for (int i = listLevel - 1; i >= 0; i--) {
            while (curNode.forward[i].key < searchKey) {
                curNode = curNode.forward[i];
            }
            // curNode.key < searchKey <= curNode.forward[i].key
            update[i] = curNode;
        }

        curNode = curNode.forward[0];

        if (curNode.key == searchKey) {
            for (int i = 0; i < listLevel; i++) {
                if (update[i].forward[i] != curNode) {
                    break;
                }
                update[i].forward[i] = curNode.forward[i];
            }

            while (listLevel > 0 && listHead.forward[listLevel - 1] == NIL) {
                listLevel--;
            }
        }
    }

    //生成随机level的算法
    private int randomLevel() {
        int lvl = 1;
        while (lvl < MAX_LEVEL && Math.random() < P) {
            lvl++;
        }
        return lvl;
    }

    public void print() {
        for (int i = listLevel - 1; i >= 0; i--) {
            SkipListNode<T> curNode = listHead.forward[i];
            while (curNode != NIL) {
                System.out.print(curNode.key + "->");
                curNode = curNode.forward[i];
            }
            System.out.println("NIL");
        }
    }

    public static void main(String[] args) {
        SkipList<Integer> sl = new SkipList<Integer>();
        sl.insert(20, 20);
        sl.insert(5, 5);
        sl.insert(10, 10);
        sl.insert(1, 1);
        sl.insert(100, 100);
        sl.insert(80, 80);
        sl.insert(60, 60);
        sl.insert(30, 30);
        Integer sKey = sl.search(10);
        System.out.println(sKey);
        System.out.println("---");
        sl.print();
        System.out.println("---");
        sl.delete(20);
        sl.delete(100);
        sl.print();
    }
}

不知道多少人还看到了这里,可能你感觉学完也没什么卵用,我感觉可能对于面试还是有点用的。我再举个最实际的应用场景,比如你要上一个大型应用,里面大量使用了redis,那熟悉熟悉各种数据结构,也差不多能估算下会占用多少内存吧- -。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档