1.跳躍表SkipList

redis 中zset 數據結構使用了跳躍表實現

zset 要支持隨機刪除和插入。插入特定順序,繼續保證鏈表有序,
二分查找的對象必須是數組

跳躍表一:

理解思路 鏈表加上多層的索引:

在這裏插入圖片描述

跳躍表二:

假如我們要用某種數據結構來維護一組有序的int型數據的集合,並且希望這個數據結構在插入、刪除、查找等操作上能夠儘可能着快速,那麼,你會用什麼樣的數據結構呢?

數組

一種很簡單的方法應該就是採用數組了,在查找方面,用數組存儲的話,採用二分法可以在 O(logn) 的時間裏找到指定的元素,不過數組在插入、刪除這些操作中比較不友好,找到目標位置所需時間爲 O(logn) ,進行插入和刪除這個動作所需的時間複雜度爲 O(n) ,因爲都需要移動移動元素,所以最終所需要的時間複雜度爲 O(n) 。

例如對於下面這個數組:
在這裏插入圖片描述

插入元素 3

在這裏插入圖片描述

鏈表:

另外一種簡單的方法應該就是用鏈表了,鏈表在插入、刪除的支持上就相對友好,當我們找到目標位置之後,插入、刪除元素所需的時間複雜度爲 O(1) ,注意,我說的是找到目標位置之後,插入、刪除的時間複雜度才爲O(1)。

但鏈表在查找上就不友好了,不能像數組那樣採用二分查找的方式,只能一個一個結點遍歷,所以加上查找所需的時間,插入、刪除所需的總的時間複雜度爲O(n)。

假如我們能夠提高鏈表的查找效率,使鏈表的查找的時間複雜度儘可能接近 O(logn) ,那鏈表將會是很棒的選擇。

提高鏈表的查找速度

那鏈表的查找速度可以提高嗎?

對於下面這個鏈表

在這裏插入圖片描述

假如我們要查找元素9,按道理我們需要從頭結點開始遍歷,一共遍歷8個結點才能找到元素9。能否採取某些策略,讓我們遍歷5次以內就找到元素9呢?請大家花一分鐘時間想一下如何實現?

由於元素的有序的,我們是可以通過增加一些路徑來加快查找速度的。例如

在這裏插入圖片描述

通過這種方法,我們只需要遍歷5次就可以找到元素9了(紅色的線爲查找路徑)。
在這裏插入圖片描述

還能繼續加快查找速度嗎?

答是可以的,再增加一層就行了,這樣只需要4次就能找到了,這就如同我們搭地鐵的時候,去某個站點時,有快線和慢線幾種路線,通過快線 + 慢線的搭配,我們可以更快着到達某個站點。

在這裏插入圖片描述

當然,還能在增加一層,

在這裏插入圖片描述

基於這種方法,對於具有 n 個元素的鏈表,我們可以採取 ** (logn + 1) 層指針路徑的形式,就可以實現在 O(logn) 的時間複雜度內,查找到某個目標元素了,這種數據結構,我們也稱之爲跳躍表,跳躍表也可以算是鏈表的一種變形,只是它具有二分查找的功能。

上面例子中,9個結點,一共4層,可以說是理想的跳躍表了,不過隨着我們對跳躍表進行插入/刪除結點的操作,那麼跳躍表結點數就會改變,意味着跳躍表的層數也會動態改變。

這裏我們面臨一個問題,就是新插入的結點應該跨越多少層?

這個問題已經有大牛替我們解決好了,採取的策略是通過拋硬幣來決定新插入結點跨越的層數:每次我們要插入一個結點的時候,就來拋硬幣,如果拋出來的是正面,則繼續拋,直到出現負面爲止,統計這個過程中出現正面的次數,這個次數作爲結點跨越的層數。

通過這種方法,可以儘可能着接近理想的層數。大家可以想一下爲啥會這樣呢?

插入

例如,我們要插入結點 3,4,通過拋硬幣知道3,4跨越的層數分別爲 0,2 (層數從0開始算),則插入的過程如下:

插入3,跨越2層
在這裏插入圖片描述

插入 4,跨越2層。

在這裏插入圖片描述

刪除

解決了插入之後,我們來看看刪除,刪除就比較簡單了,例如我們要刪除4,那我們直接把4及其所跨越的層數刪除就行了。
在這裏插入圖片描述

小結

跳躍表的插入與刪除至此都講完了,總結下跳躍表的有關性質:

(1). 跳躍表的每一層都是一條有序的鏈表.

(2). 跳躍表的查找次數近似於層數,時間複雜度爲O(logn),插入、刪除也爲 O(logn)。
(3). 最底層的鏈表包含所有元素。

(4). 跳躍表是一種隨機化的數據結構(通過拋硬幣來決定層數)。

時間複雜度

操作 時間複雜度
創建一個跳躍表 O(1)
釋放給定跳躍表以及其中包含的節點 O(N)
添加給定成員和分值的新節點 平均O(logN),最壞O(logN)(N爲跳躍表的長度)
刪除除跳躍表中包含給定成員和分值的節點 平均O(logN),最壞O(logN)(N爲跳躍表的長度)
返回給定成員和分值的節點再表中的排位 平均O(logN),最壞O(logN)(N爲跳躍表的長度)
返回在給定排位上的節點 平均O(logN),最壞O(logN)(N爲跳躍表的長度)
給定一個分值範圍,返回跳躍表中第一個符合這個範圍的節點 O(1)
給定一個分值範圍,返回跳躍表中最後一個符合這個範圍的節點 平均O(logN),最壞O(logN)(N爲跳躍表的長度)
給定一個分值範圍,除跳躍表中所有在這個範圍之內的節點 平均O(logN),最壞O(logN)(N爲跳躍表的長度)
給定一個排位範圍,鼎除跳躍表中所有在這個範圍之內的節點 O(N),N爲被除節點數量
給定一個分值範固(range),比如0到15,20到28,諸如此類,如果跳氏表中有至少一個節點的分值在這個範間之內,那麼返回1,否則返回0 O(N),N爲被除節點數量

跳躍表 vs 二叉查找樹

有人可能會說,也可以採用二叉查找樹啊,因爲查找查找樹的插入、刪除、查找也是近似 O(logn) 的時間複雜度。

不過,二叉查找樹是有可能出現一種極端的情況的,就是如果插入的數據剛好一直有序,那麼所有節點會偏向某一邊。例如

在這裏插入圖片描述

這種接結構會導致二叉查找樹的查找效率變爲 O(n),這會使二叉查找樹大打折扣。

跳躍表 vs 紅黑樹

紅黑可以說是二叉查找樹的一種變形,紅黑在查找,插入,刪除也是近似O(logn)的時間複雜度,但學過紅黑樹的都知道,紅黑樹比跳躍表複雜多了,反正我是被紅黑樹虐過。在選擇一種數據結構時,有時候也是需要考慮學習成本的。

當然,紅黑樹並不是一定比跳躍表差,在有些場合紅黑樹會是更好的選擇,所以選擇一種數據結構,關鍵還得看場合。

總上所述,維護一組有序的集合,並且希望在查找、插入、刪除等操作上儘可能快,那麼跳躍表會是不錯的選擇。redis 中的數據數據便是採用了跳躍表,當然,ridis也結合了哈希表等數據結構,採用的是一種複合數據結構。

代碼如下

//節點
classNode
{
    intvalue = -1;
    intlevel; //跨越幾層
    Node[] next; //指向下一個節點
    publicNode(intvalue, intlevel)
    {
        this.value = value;
        this.level = level;
        this.next = newNode[level];
    }
}
//跳躍表
publicclassSkipList
{
    //允許的最大層數
    intmaxLevel = 16;
    //頭節點,充當輔助。
    Node head = newNode(-1, 16);
    //當前跳躍表節點的個數
    intsize = 0;
    //當前跳躍表的層數,初始化爲1層。
    intlevelCount = 1;
    publicNode find(intvalue)
        {
            Node temp = head;
            for(inti = levelCount - 1; i >= 0; i--)
            {
                while(temp.next[i] != null && temp.next[i].value < value)
                {
                    temp = temp.next[i];
                }
            }
            //判斷是否有該元素存在
            if(temp.next[0] != null && temp.next[0].value == value)
            {
                System.out.println(value + " 查找成功");
                returntemp.next[0];
            }
            else
            {
                returnnull;
            }
        }
        // 爲了方便,跳躍表在插入的時候,插入的節點在當前跳躍表是不存在的
        //不允許插入重複數值的節點。
    publicvoidinsert(intvalue)
    {
        intlevel = getLevel();
        Node newNode = newNode(value, level);
        //update用於記錄要插入節點的前驅
        Node[] update = newNode[level];
        Node temp = head;
        for(inti = level - 1; i >= 0; i--)
        {
            while(temp.next[i] != null && temp.next[i].value < value)
            {
                temp = temp.next[i];
            }
            update[i] = temp;
        }
        //把插入節點的每一層連接起來
        for(inti = 0; i < level; i++)
        {
            newNode.next[i] = update[i].next[i];
            update[i].next[i] = newNode;
        }
        //判斷是否需要更新跳躍表的層數
        if(level > levelCount)
        {
            levelCount = level;
        }
        size++;
        System.out.println(value + " 插入成功");
    }
    publicvoiddelete(intvalue)
        {
            Node[] update = newNode[levelCount];
            Node temp = head;
            for(inti = levelCount - 1; i >= 0; i--)
            {
                while(temp.next[i] != null && temp.next[i].value < value)
                {
                    temp = temp.next[i];
                }
                update[i] = temp;
            }
            if(temp.next[0] != null && temp.next[0].value == value)
            {
                size--;
                System.out.println(value + " 刪除成功");
                for(inti = levelCount - 1; i >= 0; i--)
                {
                    if(update[i].next[i] != null && update[i].next[i].value == value)
                    {
                        update[i].next[i] = update[i].next[i].next[i];
                    }
                }
            }
        }
        //打印所有節點
    publicvoidprintAllNode()
        {
            Node temp = head;
            while(temp.next[0] != null)
            {
                System.out.println(temp.next[0].value + " ");
                temp = temp.next[0];
            }
        }
        //模擬拋硬幣
    privateintgetLevel()
        {
            intlevel = 1;
            while(true)
            {
                intt = (int)(Math.random() * 100);
                if(t % 2 == 0)
                {
                    level++;
                }
                else
                {
                    break;
                }
            }
            System.out.println("當前的level = " + level);
            returnlevel;
        }
        //測試數據
    publicstaticvoidmain(String[] args)
    {
        SkipList list = newSkipList();
        for(inti = 0; i < 6; i++)
        {
            list.insert(i);
        }
        list.printAllNode();
        list.delete(4);
        list.printAllNode();
        System.out.println(list.find(3));
        System.out.println(list.size + " " + list.levelCount);
    }
}

參考

https://www.sohu.com/a/293236470_298038
https://www.cnblogs.com/Leo_wl/p/11557614.html#_label1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章