跳跃链表

224 阅读6分钟

场景

现在给定一个场景,给定一个有序的链表。如何根据元素的值进行高效查找?

有序查找这不是二分查找法的要素嘛?

可是注意这里给出的数据结构是链表,而不是数组。所以不能使用二分查找法。

众所周知,链表是一种查慢,但是删除,插入快的数据结构。为了解决这个查询慢这个问题。衍生了今天的主角------跳跃链表

介绍

这咋一看,看起来好像这个数据结构特别复杂。还有它又是怎么加快查找速率的呢?别急,先来给出他的定义。

定义

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

这又是一种用空间换时间的做法。


跳跃链表实现快速查找的目的,其实归根结底都是建立索引。提到索引就不得不提到目录。这个最好理解索引为何物的例子。

1.	引言	......................................................................7
	1.1	编写目的	.............................................................7
	1.2	文档约定	.............................................................7
	1.3	术语及缩写说明	...........................................................7
	1.4	预期的读者和阅读建议	......................................................8
2.	系统程序结构	................................................................9
	2.1	应用架构	..............................................................9
	2.2	功能架构	..............................................................9
	2.3	服务架构	..............................................................10
	2.4	数据库架构	..............................................................10
	2.5	部署架构	...............................................................11
	2.6	方案架构示意图	.............................................................11
		2.6.1	接口对接方案	......................................................11
		2.6.2	Data Integration模块架构	.......................................11
		2.6.3	KPI Dashboard模块架构	...........................................12
2.7	模块清单	...................................................................12

目录其实也是一种对页码进行划分的索引。而且还可以通过划分层多级索引,来加快查找的速率。

跳跃链表其实十分类似于目录。最底层的链表存储着所有的数据,而从第二层开始到顶层代表着用于加速查找的索引。层级越高,对应的索引越高级。

查找

首先来介绍一下查找。看看跳跃链表是如何快速定位到目标元素的。比如现在我们要找到元素23

顶部的头节点开始遍历。如果节点元素小于目标值,则向左移动。如果大于等于目标值则向下移动。就是同样的操作一步步下探到对应节点。

也就是 head --> 2 --> 2 --> 17 --> 17 --> 17 --> 23

复杂度分析

我们这里是每隔两个节点就建立一层索引。

假设原始链表有n个结点,那么索引的层级就是log(n),在每一层的访问次数是常量,因此查找结点的平均时间复杂度是O(logn)

增加

插入这里最重要理解的点是概率晋升。我们现在来举例将10插入上图中。

首先进行元素插入,需要先找到待插入元素的前置节点(仅小于待插节点)。然后将该节点插入其中。

这样就完了吗?不是的,随着原始链表的新结点越来越多,索引会渐渐变得不够用了,因此索引结点也需要相应作出调整。

所以,需要新插入的节点按照一定的概率晋升成为索引节点。我们上图的跳跃节点链表中,底层和第二层是按照2:1的进行划分。所以概率晋升的概率也应该为50%。所以新节点有50%的概率晋升为索引节点。同时,如果成功晋升为索引节点,仍然有50%的机会升级为上一级的索引节点。如果晋升失败。则插入操作也随之完成。

注意。这里还有一种特殊情况。就是这个节点存在着一定概率,连续晋升到最顶层的索引链表,同时它又成功晋升,此时需要增加一层索引。

删除

删除操作则相对来说简单很多。

首先同样需要找到待删除元素所在节点。然后如果这个节点上头有索引节点,同样需要删除与之对应的索引节点。

这里同样也存在一种特殊情况。好比删除掉我们新增的节点10.最顶层的索引节点10也会被对应地删除掉。这时,顶层就不存在索引节点,所以需要把这层给清空,将顶层下沉。

代码实现

先要说明跳跃链表采用的是双向链表,节点拥有上下左右四个指针。节点之间彼此互相指向。

    //链表结点类
    public class Node {
        public int data;
        //跳表结点的前后和上下都有指针
        public Node up, down, left, right;

        public Node(int data) {
            this.data = data;
        }
    }

程序中跳表的每一层首位各有一个空结点,左侧的空节点是负无穷大,右侧的空节点是正无穷大。

同时还需要维护一个头节点,尾结点,以及一个最大层数

    //结点“晋升”的概率
    private static final double PROMOTE_RATE = 0.5;
    private Node head,tail;
    private int maxLevel;

    public SkipList() {
        head = new Node(Integer.MIN_VALUE);
        tail = new Node(Integer.MAX_VALUE);
        head.right = tail;
        tail.left = head;
    }

查找节点

还记得我们说的查找节点,第一步找到对应前置节点

前置节点在这里指代两种情况,第一种就是直接找到对应元素的节点。第二种是找不到对应元素节点,只能找到它的前一个节点。

    //找到值对应的前置结点
    private Node findNode(int data){
        Node node = head;
        while(true){
            // 这里属于同层,向左挪动
            while (node.right.data!=Integer.MAX_VALUE && node.right.data<=data) {
                node = node.right;
            }
            // node.down == null 表示当前已经是最底层链表,无法下探
            if (node.down == null) {
                break;
            }
            // 向下挪动
            node = node.down;
        }
        return node;
    }

当然后会存在对应节点的值,与我们查找的目标值并不相同的情况。所以还需进行比较

    //查找结点
    public Node search(int data){
        Node p= findNode(data);
        if(p.data == data){
            return p;
        }
        return null;
    }

增加节点

在前置结点后面添加新结点

    private void appendNode(Node preNode, Node newNode){
        newNode.left=preNode;
        newNode.right=preNode.right;
        preNode.right.left=newNode;
        preNode.right=newNode;
    }

增加层数

    private void addLevel(){
        maxLevel++;
        Node p1=new Node(Integer.MIN_VALUE);
        Node p2=new Node(Integer.MAX_VALUE);
        p1.right=p2;
        p2.left=p1;
        p1.down=head;
        head.up=p1;
        p2.down=tail;
        tail.up=p2;
        head=p1;
        tail=p2;
    }

新增节点代码

    public void insert(int data){
        // 先找到这个节点对应的前置节点
        Node preNode= findNode(data);
        //如果data相同,直接返回
        if (preNode.data == data) {
            return;
        }
        Node node=new Node(data);
        appendNode(preNode, node);
        int currentLevel=0;
        int currentMaxLevel = maxLevel;
        //随机决定结点是否“晋升”
        Random random = new Random();
        // currentMaxLevel是为了确保 每次insert 理论最多只能 加 1层
        while (random.nextDouble() < PROMOTE_RATE && currentLevel <= currentMaxLevel) {
            //如果当前层已经是最高层,需要增加一层
            if (currentLevel == maxLevel) {
                addLevel();
            }
            //找到上一层的前置节点
            while (preNode.up==null) {
                preNode=preNode.left;
            }
            preNode=preNode.up;
            //把“晋升”的新结点插入到上一层
            Node upperNode = new Node(data);
            appendNode(preNode, upperNode);
            upperNode.down = node;
            node.up = upperNode;
            node = upperNode;
            currentLevel++;
        }
    }

删除节点

删除层数代码


    private void removeLevel(Node leftNode){
        Node rightNode = leftNode.right;
        //如果删除层是最高层
        if(leftNode.up == null){
            leftNode.down.up = null;
            rightNode.down.up = null;
        }else {
            leftNode.up.down = leftNode.down;
            leftNode.down.up = leftNode.up;
            rightNode.up.down = rightNode.down;
            rightNode.down.up = rightNode.up;
        }
        maxLevel --;
    }

删除节点代码


    public boolean remove(int data){
        Node removedNode = search(data);
        if(removedNode == null){
            return false;
        }

        int currentLevel=0;
        while (removedNode != null){
            removedNode.right.left = removedNode.left;
            removedNode.left.right = removedNode.right;
            //如果不是最底层,且只有无穷小和无穷大结点,删除该层
            if(currentLevel != 0 && removedNode.left.data == Integer.MIN_VALUE && removedNode.right.data == Integer.MAX_VALUE){
                removeLevel(removedNode.left);
            }else {
                currentLevel ++;
            }
            removedNode = removedNode.up;
        }

        return true;
    }

说明

此文章为查看程序员小灰的什么是 “跳表” 文章后,根据个人理解写的一篇文章。文章的代码以及对于跳跃链表的描述大部分来自于此篇文章, 图片为本人所画。