跳錶 skiplist

最初知道跳錶(Skip List)是在看redis原理的時候,redis中的有序集合使用了跳錶做爲數據結構。接着就查了一些資料,來學習一下跳錶。後面會使用java代碼來實現跳錶。java

跳錶簡介

跳錶由William Pugh發明。他在論文《Skip lists: a probabilistic alternative to balanced trees》中詳細介紹了跳錶的數據結構和插入刪除等操做。論文是這麼介紹跳錶的:node

Skip lists are a data structure that can be used in place of balanced trees.
Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees.redis

也就是說,跳錶能夠用來替代紅黑樹,使用機率均衡技術,插入、刪除操做更簡單、更快。算法

做者在文章中由鏈表的查找一步步引入到跳錶的介紹。首先來看論文裏的一張圖:數組

原始論文中的圖-擁有額外指針的鏈表

上圖a,已排好序的鏈表,查找一個結點最多須要比較N個結點。數據結構

上圖b,每隔2個結點增長一個指針,指向該結點間距爲2的後續結點,那麼查找一個結點最多須要比較ceil(N/2)+1個結點。dom

上圖c,每隔4個結點增長一個指針,指向該結點間距爲4的後續結點,那麼查找一個結點最多須要比較ceil(N/4)+1個結點。函數

若是每第2^i個結點都有一個指向間距爲2^i的後續結點的指針,這樣不斷增長指針,比較次數會降爲log(N)。這樣的話,搜索會很快,但插入和刪除會很困難。學習

一個擁有k個指針的結點稱爲一個k層結點(level k node)。按照上面的邏輯,50%的結點爲1層,25%的結點爲2層,12.5%的結點爲3層...若是每一個結點的層數隨機選取,但仍服從這樣的分佈呢(上圖e,對比上圖d)?ui

使一個k層結點的第i個指針指向第i層的下一個結點,而不是它後面的第2^(i-1)個結點,那麼結點的插入和刪除只須要原地修改操做;一個結點的層數,是在它被插入的時候隨機選取的,而且永不改變。由於這樣的數據結構是基於鏈表的,而且額外的指針會跳過中間結點,因此做者稱之爲跳錶(Skip Lists)。

跳錶的算法

原始論文使用僞代碼的形式來描述算法。本文以java語言來描述算法。

首先定義一下須要用的數據結構。表中的元素使用結點來表示,結點的層數在它被插入時隨機計算決定(與表中已有結點數目無關)。一個i層的結點有i個前向指針(java中使用結點對象數組forward來表示),索引爲從1到i。用MaxLevel來記錄跳錶的最大層數。跳錶的層數爲當前全部結點中的最大層數(若是list爲空,則層數爲1)。列表頭header擁有從1到MaxLevel的前向指針:

public class SkipList<T> {

    // 最高層數
    private final int MAX_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() {
        // 0.25, 15
        this(OPTIMAL_P, (int)Math.ceil(Math.log(Integer.MAX_VALUE) / Math.log(1 / OPTIMAL_P)) - 1);
    }

    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];
        }
    }
}

搜索

按key搜索,找到返回該key對應的value,未找到則返回null。

經過遍歷forward數組來需找特定的searchKey。假設skip list的key按照從小到大的順序排列,那麼從跳錶的當前最高層listLevel開始尋找searchKey。在某一層找到一個非小於searchKey的結點後,跳到下一層繼續找,直到最底層爲止。那麼根據最後搜索中止位置的下一個結點,就能夠判斷searchKey在不在跳錶中。

下圖爲在跳錶中找8的過程:

跳錶查找

插入和刪除

插入的刪除的方法類似,都是經過查找與鏈接(search and splice),以下圖:

跳錶插入和刪除

維護一個update數組,在搜索結束以後,update[i]保存的是待插入/刪除結點在第i層的左側結點。

1. 插入

若key不存在,則插入該key與對應的value;若key存在,則更新value。

若是待插入的結點的層數高於跳錶的當前層數listLevel,則更新listLevel。

選擇待插入結點的層數randomLevel:

randomLevel只依賴於跳錶的最高層數和機率值p。算法在後面的代碼中。

另外一種實現方法爲,若是生成的randomLevel大於當前跳錶的層數listLevel,那麼將randomLevel設置爲listLevel+1,這樣方便之後的查找,在工程上是能夠接受的,但同時也破壞了算法的隨機性。

2. 刪除

刪除特定的key與對應的value。

若是待刪除的結點爲跳錶中層數最高的結點,那麼刪除以後,要更新listLevel。

java版代碼

參考了網上的一些代碼,用java寫了一版,包含main函數,可運行。

public class SkipList<T> {

    // 最高層數
    private final int MAX_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() {
        // 0.25, 15
        this(OPTIMAL_P, (int)Math.ceil(Math.log(Integer.MAX_VALUE) / Math.log(1 / OPTIMAL_P)) - 1);
    }

    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; i > 0; i--) {
            while (curNode.forward[i].key < searchKey) {
                curNode = curNode.forward[i];
            }
        }

        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--;
            }
        }
    }

    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);
        sl.print();
        System.out.println("---");
        sl.delete(20);
        sl.delete(100);
        sl.print();
    }
}

參考資料

相關文章
相關標籤/搜索