SkipList(跳表)--有序的链表

605 阅读5分钟

什么是跳表

跳表SkipList,全称跳跃列表,其实就是一个创建了目录方便查找的有序链表。

引用百度百科的定义:

跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

引用维基百科的定义:

在计算机科学中,跳跃列表是一种数据结构。它使得包含n个元素的有序序列的查找和插入操作的平均时间复杂度都是O(log n),优于数组的 O(n)复杂度。快速的查询效果是通过维护一个多层次的链表实现的,且与前一层(下面一层)链表元素的数量相比,每一层链表中的元素的数量更少(见右下角示意图)。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。跳过的元素的方法可以是随机性选择或确定性选择,其中前者更为常见。

s1.png

跳表的应用和好处

Redis中有序集合(Sorted Set)采用了这种数据结构,不仅提高了搜索能力,同时也提高插入和删除操作的性能。 跳表是一个随机化的数据结构,可以被看做二叉树的一个变种,它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单。

跳表跟平衡树(AVL、红黑树)一样都是有序,而哈希表是无序的,所以哈希表不适合范围查找。查找单个key的时候,跳表的时间复杂度要O(logn),而哈希的时间复杂度为O(1)。

范围查询的时候平衡树会比较复杂,需要通过各种遍历,而跳表比较简单,找到最小值之后,对原始数据进行遍历即可;平衡树的插入删除需要旋转调整树结构,而跳表修改指针即可,简单又快速。

跳表的算法设计比较简单。

下面以普通链表和跳表分析查找元素20的效率对比:

想要定位到链表中的节点20,无法直接定位,需要从头节点开始,顺着next指针,逐个访问下一个节点,属于线性遍历。

s3.png

下面这种情况就不一样,可以根据索引提升查询效率。
首先从最上层的索引开始查找,找到该层中小于节点20的前置索引15;
接下来,顺着节点15访问下一个层索引,在该层节点中找到20;
顺着第1层索引的节点20向下,找到原始链表的节点20;

s2.png

通过以上例子,假设原始链表有n个节点,那么索引的层级就是log(n)-1,在每一层的访问次数是常量,因此查找节点的平均时间复杂度是O(logn)。比起常规的查找方式,也就是线性一次访问链表节点的方式,效率要高很多。

跳表的增删

下面通过跳表的源码分析增删:

public class Node {
        
    // 元素值    
    private int data;
    // 元素的上下左右指针 
    // 为了方便理解 将左侧空节点设置为负无穷大,右侧空节点设置为正无穷大
    private Node up;
    private Node down;
    private Node left;
    private Node right;

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

}

public class SkipList {

    private Node head;
    private Node tail;

    private int maxLevel;
    // 节点上升索引的概率
    private static final double PROMOTE_RATE = 0.5;

    public SkipList(){
        Node max = new Node(Integer.MAX_VALUE);
        Node min = new Node(Integer.MIN_VALUE);
        head = min;
        tail = max;
        head.setRight(tail);
        tail.setLeft(head);
        maxLevel = 0;
    }
    
    /**
     * 查找前置节点
     * @param data
     * @return
     */
    public Node findNode(int data){
        Node node = head;
        while (true){
            while (node.getRight().getData() != Integer.MAX_VALUE && node.getRight().getData() <= data){
                node = node.getRight();
            }
            if(node.getDown() == null){
                break;
            }
            node = node.getDown();
        }
        return node;
    }

    // ......
    // 增删改查方法

}

新增元素

通过在原有跳表中新增元素12,如下过程:

s4.png

首先找到前置节点11,接下来按照链表的插入方式,把节点12插入到11的下一个节点;
随着原始链表的新节点越来越多,索引会逐渐变得不够用了,因此索引节点也需要相应做出调整;
假如我们在50%的概率上,得出节点需要晋升,那么把节点12作为索引节点,插到第1层索引的对应位置,并且向下指向原始链表的节点12;
如果每层上升都成功,并且超出了最高索引的最高范围,那么新增一层索引。


public void insert(int data){
    // 查找前置节点
    Node preNode = findNode(data);
    if(preNode != null && preNode.getData() == data){
        return;
    }
    // 按照链表的插入方式,把节点插入到前置节点的下一个节点;
    Node newNode = new Node(data);
    appendNewNode(preNode,newNode);
    int currentLevel = 0;
    Random random = new Random();
    // 晋升成功
    while (random.nextDouble() < PROMOTE_RATE){
        // 当前层数最大,多添加一层
        if(currentLevel == maxLevel){
            addLevel();
        }
        // 找到索引的前置节点
        while (preNode.getUp() == null){
            preNode = preNode.getLeft();
        }
        preNode = preNode.getUp();
        // 索引插入数据
        Node upperNode = new Node(data);
        appendNewNode(preNode,upperNode);
        upperNode.setDown(newNode);
        newNode.setUp(upperNode);
        newNode = upperNode;
        currentLevel ++;
    }

    return;
}

public void appendNewNode(Node preNode,Node newNode){
    newNode.setLeft(preNode);
    preNode.getRight().setLeft(newNode);
    newNode.setRight(preNode.getRight());
    preNode.setRight(newNode);
}

public void addLevel(){
   maxLevel++;
   Node p1 = new Node(Integer.MIN_VALUE);
   Node p2 = new Node(Integer.MAX_VALUE);
   p1.setRight(p2);
   p2.setLeft(p1);
   p1.setDown(head);
   p2.setDown(tail);
   head.setUp(p1);
   tail.setUp(tail);
   head = p1;
   tail = p2;
}

删除

跳表删除节点的过程,是相反的思路。

s5.png


public boolean remove(int data){
    // 找到要删除的元素
    Node removeNode = search(data);
    if(removeNode == null){
        return false;
    }
    int currentLevel = 0;
    while (removeNode != null){
        removeNode.getRight().setLeft(removeNode.getLeft());
        removeNode.getLeft().setRight(removeNode.getRight());
        // 如果不是最底层,且只有无穷小和无穷大节点,删除该层
        if(currentLevel != 0 && removeNode.getLeft().getData() == Integer.MIN_VALUE && removeNode.getRight().getData() == Integer.MAX_VALUE){
            removeLevel(removeNode.getLeft());
        }else {
            currentLevel++;
        }
        removeNode = removeNode.getUp();
    }
    return true;
}
// 删除一层
private void removeLevel(Node leftNode){
    Node rightNode = leftNode.getRight();
    // 如果删除层是最高层
    if(leftNode.getUp() == null){
        leftNode.setDown(head);
        leftNode.getDown().setUp(null);
        rightNode.getDown().setUp(null);
    }else {
        leftNode.getUp().setDown(leftNode.getDown());
        leftNode.getDown().setUp(leftNode.getUp());
        rightNode.getUp().setDown(rightNode.getDown());
        rightNode.getDown().setUp(rightNode.getUp());
    }
    maxLevel--;
}