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完整代碼地址

參考文獻


相關文章
相關標籤/搜索