SkipList的那点事儿

Skip List的工做原理


Skip List(跳跃表)是一种支持快速查找的数据结构,插入、查找和删除操做都仅仅只须要O(log n)对数级别的时间复杂度,它的效率甚至能够与红黑树等二叉平衡树相提并论,并且实现的难度要比红黑树简单多了。java

Skip List主要思想是将链表与二分查找相结合,它维护了一个多层级的链表结构(用空间换取时间),能够把Skip List看做一个含有多个行的链表集合,每一行就是一条链表,这样的一行链表被称为一层,每一层都是下一层的"快速通道",即若是x层和y层都含有元素a,那么x层的a会与y层的a相互链接(垂直)。最底层的链表是含有全部节点的普通序列,而越接近顶层的链表,含有的节点则越少。node

对一个目标元素的搜索会从顶层链表的头部元素开始,而后遍历该链表,直到找到元素大于或等于目标元素的节点,若是当前元素正好等于目标,那么就直接返回它。若是当前元素小于目标元素,那么就垂直降低到下一层继续搜索,若是当前元素大于目标或到达链表尾部,则移动到前一个节点的位置,而后垂直降低到下一层。正由于Skip List的搜索过程会不断地从一层跳跃到下一层的,因此被称为跳跃表。git

Skip List还有一个明显的特征,即它是一个不许确的几率性结构,这是由于Skip List在决定是否将节点冗余复制到上一层的时候(而在到达或超过顶层时,须要构建新的顶层)依赖于一个几率函数,举个栗子,咱们使用一个最简单的几率函数:丢硬币,即几率P0.5,那么依赖于该几率函数实现的Skip List会不断地"丢硬币",若是硬币为正面就将节点复制到上一层,直到硬币为反。github

插入元素的过程

理解Skip List的原理并不困难,下面咱们将使用Java来动手实现一个支持基本需求(查找,插入和删除)的Skip List。数据结构

本文做者为SylvanasSun(sylvanas.sun@gmail.com),首发于SylvanasSun’s Blog。 原文连接:https://sylvanassun.github.io/2017/12/31/2017-12-31-skip_list/ (转载请务必保留本段声明,而且保留超连接。)app

节点与基本实现


对于一个普通的链表节点通常只含有一个指向后续节点的指针(双向链表的节点含有两个指针,一个指向前节点,一个指向后节点),因为Skip List是一个多层级的链表结构,咱们的设计要让节点拥有四个指针,分别对应该节点的先后左右,为了方便地将头链表永远置于顶层,还须要设置一个int属性表示该链表所处的层级。less

protected static class Node<K extends Comparable<K>, V> {

        private K key;

        private V value;

        private int level; // 该节点所处的层级

        private Node<K, V> up, down, next, previous;

        public Node(K key, V value, int level) {
            this.key = key;
            this.value = value;
            this.level = level;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("Node[")
                    .append("key:");
            if (this.key == null)
                sb.append("None");
            else
                sb.append(this.key.toString());

            sb.append(" value:");
            if (this.value == null)
                sb.append("None");
            else
                sb.append(this.value.toString());
            sb.append("]");
            return sb.toString();
        }
		
		// 余下都是get,set方法, 这里省略
		.....
}
复制代码

接下来是SkipList的基本实现,为了可以让Key进行比较,咱们规定Key的类型必须实现了Comparable接口,同时为了支持ForEach循环,该类还实现了Iterable接口。dom

public class SkipList<K extends Comparable<K>, V> implements Iterable<K> {
	
	// 一个随机数生成器
    protected static final Random randomGenerator = new Random();
	
	// 默认的几率
    protected static final double DEFAULT_PROBABILITY = 0.5;
	
	// 头节点
    private Node<K, V> head;

    private double probability;
	
	// SkipList中的元素数量(不计算多个层级中的冗余元素)
    private int size;

    public SkipList() {
        this(DEFAULT_PROBABILITY);
    }

    public SkipList(double probability) {
        this.head = new Node<K, V>(null, null, 0);
        this.probability = probability;
        this.size = 0;
    }
	.....
}	
复制代码

咱们还须要定义几个辅助方法,以下所示(都很简单):ide

// 对key进行检查
	// 由于每条链表的头节点就是一个key为null的节点,因此不容许其余节点的key也为null
    protected void checkKeyValidity(K key) {
        if (key == null)
            throw new IllegalArgumentException("Key must be not null!");
    }
	
	// a是否小于等于b
    protected boolean lessThanOrEqual(K a, K b) {
        return a.compareTo(b) <= 0;
    }
	
	// 几率函数
    protected boolean isBuildLevel() {
        return randomGenerator.nextDouble() < probability;
    }
	
	// 将y水平插入到x的后面
    protected void horizontalInsert(Node<K, V> x, Node<K, V> y) {
        y.setPrevious(x);
        y.setNext(x.getNext());
        if (x.getNext() != null)
            x.getNext().setPrevious(y);
        x.setNext(y);
    }
	
	// x与y进行垂直链接
    protected void verticalLink(Node<K, V> x, Node<K, V> y) {
        x.setDown(y);
        y.setUp(x);
    }
复制代码

查找


查找一个节点的过程以下:函数

  • 从顶层链表的头部开始进行遍历,比较每个节点的元素与目标元素的大小。

  • 若是当前元素小于目标元素,则继续遍历。

  • 若是当前元素等于目标元素,返回该节点。

  • 若是当前元素大于目标元素,移动到前一个节点(必须小于等于目标元素),而后跳跃到下一层继续遍历。

  • 若是遍历至链表尾部,跳跃到下一层继续遍历。

protected Node<K, V> findNode(K key) {
        Node<K, V> node = head;
        Node<K, V> next = null;
        Node<K, V> down = null;
        K nodeKey = null;

        while (true) {
            // 不断遍历直到碰见大于目标元素的节点
            next = node.getNext();
            while (next != null && lessThanOrEqual(next.getKey(), key)) {
                node = next;
                next = node.getNext();
            }
			// 当前元素等于目标元素,中断循环
            nodeKey = node.getKey();
            if (nodeKey != null && nodeKey.compareTo(key) == 0)
                break;
            // 不然,跳跃到下一层级
            down = node.getDown();
            if (down != null) {
                node = down;
            } else {
                break;
            }
        }

        return node;
    }
	
    public V get(K key) {
        checkKeyValidity(key);
        Node<K, V> node = findNode(key);
		// 若是找到的节点并不等于目标元素,则目标元素不存在于SkipList中
        if (node.getKey().compareTo(key) == 0)
            return node.getValue();
        else
            return null;
    }	
复制代码

插入


插入操做的过程要稍微复杂些,主要在于复制节点到上一层与构建新层的操做上。

public void add(K key, V value) {
        checkKeyValidity(key);
		// 直接找到key,而后修改对应的value便可
        Node<K, V> node = findNode(key);
        if (node.getKey() != null && node.getKey().compareTo(key) == 0) {
            node.setValue(value);
            return;
        }
	
		// 将newNode水平插入到node以后
        Node<K, V> newNode = new Node<K, V>(key, value, node.getLevel());
        horizontalInsert(node, newNode);
        
        int currentLevel = node.getLevel();
        int headLevel = head.getLevel();
        while (isBuildLevel()) {
            // 若是当前层级已经到达或超越顶层
			// 那么须要构建一个新的顶层
            if (currentLevel >= headLevel) {
                Node<K, V> newHead = new Node<K, V>(null, null, headLevel + 1);
                verticalLink(newHead, head);
                head = newHead;
                headLevel = head.getLevel();
            }
            // 找到node对应的上一层节点
            while (node.getUp() == null) {
                node = node.getPrevious();
            }
            node = node.getUp();
		
			// 将newNode复制到上一层
            Node<K, V> tmp = new Node<K, V>(key, value, node.getLevel());
            horizontalInsert(node, tmp);
            verticalLink(tmp, newNode);
            newNode = tmp;
            currentLevel++;
        }
        size++;
    }
复制代码

删除


对于删除一个节点,须要先找到节点所在的位置(位于最底层链表中的位置),以后再自底向上地删除该节点在每一行中的冗余复制。

public void remove(K key) {
        checkKeyValidity(key);
        Node<K, V> node = findNode(key);
        if (node == null || node.getKey().compareTo(key) != 0)
            throw new NoSuchElementException("The key is not exist!");

        // 移动到最底层
        while (node.getDown() != null)
            node = node.getDown();
        // 自底向上地进行删除
        Node<K, V> prev = null;
        Node<K, V> next = null;
        for (; node != null; node = node.getUp()) {
            prev = node.getPrevious();
            next = node.getNext();
            if (prev != null)
                prev.setNext(next);
            if (next != null)
                next.setPrevious(prev);
        }

        // 对顶层链表进行调整,去除无效的顶层链表
        while (head.getNext() == null && head.getDown() != null) {
            head = head.getDown();
            head.setUp(null);
        }
        size--;
    }
复制代码

迭代器


因为咱们的SkipList实现了Iterable接口,因此还须要实现一个迭代器。对于迭代一个Skip List,只须要找到最底层的链表而且移动到它的首节点,而后进行遍历便可。

@Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        Node<K, V> node = head;

        // 移动到最底层
        while (node.getDown() != null)
            node = node.getDown();

        while (node.getPrevious() != null)
            node = node.getPrevious();

        // 第一个节点是头部节点,没有任何意义,因此须要移动到后一个节点
        if (node.getNext() != null)
            node = node.getNext();
		
		// 遍历
        while (node != null) {
            sb.append(node.toString()).append("\n");
            node = node.getNext();
        }

        return sb.toString();
    }

    @Override
    public Iterator<K> iterator() {
        return new SkipListIterator<K, V>(head);
    }

    protected static class SkipListIterator<K extends Comparable<K>, V> implements Iterator<K> {

        private Node<K, V> node;

        public SkipListIterator(Node<K, V> node) {
            while (node.getDown() != null)
                node = node.getDown();

            while (node.getPrevious() != null)
                node = node.getPrevious();

            if (node.getNext() != null)
                node = node.getNext();

            this.node = node;
        }

        @Override
        public boolean hasNext() {
            return this.node != null;
        }

        @Override
        public K next() {
            K result = node.getKey();
            node = node.getNext();
            return result;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
复制代码

本文中实现的SkipList完整代码地址

参考文献


相关文章
相关标签/搜索