資料結構與演算法---跳躍表

whao2world發表於2021-04-26

前言

  最近接觸到了跳躍表,感覺很牛x,這不又把《資料結構與演算法分析》翻開了,也查了一些資料,這裡總結一下自己的理解。

概念及特點

  跳躍表是一種分層結構的有序連結串列,其查詢和插入的平均時間複雜都是O(logN)。相比陣列插入的時間複雜度O(N)和平衡二叉樹

插入過程中為滿足平衡而實施複雜旋轉操作,跳躍表有很大優勢;同時跳躍表在平行計算中也非常有用,因為跳躍表插入是區域性的操作,

可以進行並行操作。

原理分析

普通連結串列的蛻變

如下圖(圖都是擷取自《資料結構與演算法分析》,比較粗糙將就著看吧),一個簡單的有序連結串列,查詢元素時,只能進行順序掃描,

其查詢時間正比於節點的個數N。

 

 如何加速查詢呢?可以通過每隔一個節點有個附加指標指向前兩個位置上的節點,這等於在簡單連結串列的基礎上建立一層子連結串列,

這樣先遍歷上層子連結串列就可以通過跳躍的方式查詢到目標節點的範圍,此時最多比較(N/2)+1個節點。

 

繼續擴充套件,增加跳躍的間距。在上圖基礎上,每個序號是4的整數倍的節點附加一個指向下一個序號是4的整數倍的節點,

這樣只有(N/4)+1節點被比較。

 

 如果擴充套件到每序號為2i的節點都有附加指標指向下一個序號為2i的節點,如下圖,這就得到了一個跳躍表,

這裡將有k層子鏈結構的跳躍表定義為k階跳躍表。

 

時間和空間複雜度

由上述跳躍表可以推斷出階數為log(N),從上往下每層遍歷都可以看做是二分查詢,從而可以得到結論其查詢的平均時間複雜度為log(N)。

這個效果是不是跟有序陣列根據下標進行二分查詢的一樣,所以跳躍表的構建思路也可以看做是連結串列通過附加指標資訊構建二分法查詢的索引資訊。

附加指標增加了記憶體空間的佔用,接下來計算一下跳躍表的空間複雜度。

假設一個N個元素的k階跳躍表,計算一下指標數量:

已知從上往下,第一階指標(即最底層)有N個,第二階指標有N/2個,…第k階指標(即最頂層)有N/2k節點,即N,N/2,N/22,…,N/2k-1 。

所以這是一個等比數列求和:Sk=N(1-1/2k)/(1-1/2)=2N-1。

相比普通連結串列,指標佔用的空間也就翻了一倍。

跳躍表的性質:第i階指標指向前面第2i個節點,同時該節點的階數k>=i;

從跳躍表的性質可以看出,節點位置和階數是對應死的。這種資料結構插入元素時,比較死板需要進行大量操作調整保持性質。

可以適當放寬限制,通過隨機演算法來確定節點的階數。上述圖10-59跳躍表結構,可以看出一階節點(即只有一個指標指向前面節點)

的個數是N/2,二階節點的個數是N/4。所以一個節點為一階的概率是1/2,為二階的概率是1/4,…,為k階的概率是1/2k

查詢操作

一次Find操作,從上往下一階階遍歷,當遇到下一個節點大於目標元素或者為NULL的節點時,則推進到當前節點的下一階;重複上述遍歷邏輯,

直至找到目標元素。例如下圖10-60查詢目標元素11,第一階查詢遇到遍歷到下一個節點13>11,所以當前節點向下推進一階,遍歷到節點8的下一

個節點13>11,節點8向下推進一階,遍歷到節點10的下一個節點13>11,節點10繼續向下推進一階直至找到目標節點11。

插入操作

根據隨機演算法建立一個k階節點,根據Find操作類似,找每層的目標插入位置,即附加指標指向的下一個節點大於目標元素或者為NULL的節點,

進行類似普通連結串列的節點插入。如下圖,元素22的插入過程。

 

刪除操作

刪除操作跟插入過程正好是相反的過程,找到每階層目標的前一個位置,將對應指標指向目標節點附加指標指向的節點。

例如將目標元素為8的節點刪除,過程如下圖。

 

程式碼實現(c++)

資料結構與演算法---跳躍表
  1 #include <iostream>
  2 #include <random>
  3 #include <vector>
  4 
  5 using namespace std;
  6 
  7 template <typename VAL_T>
  8 struct SkipListNode{
  9     SkipListNode(VAL_T val, int level):_val(val),_nexts(level,nullptr){
 10     }
 11 
 12     size_t level() {
 13         return _nexts.size();
 14     }
 15 
 16     VAL_T _val;
 17     vector<SkipListNode<VAL_T>*> _nexts;
 18 };
 19 
 20 
 21 template <typename VAL_T, size_t M>
 22 class SkipList{
 23 public:
 24     SkipList():_head(-1,M){
 25     }
 26 
 27     void destory(){
 28         auto it = _head._nexts[0];
 29         while (it){
 30             auto tmp = it->_nexts[0];
 31             delete it;
 32             it = tmp;
 33         }
 34     }
 35 
 36     size_t random(){
 37         size_t times = 0;
 38         std::random_device rd;
 39         std::mt19937 gen(rd());
 40         std::uniform_int_distribution<> dis(0, 1);
 41         do{
 42             times++;
 43         }while(times < _M && !dis(gen));
 44         
 45         return times;
 46     }
 47 
 48     SkipListNode<VAL_T>* createNode(VAL_T val){
 49         int m = random();
 50         SkipListNode<VAL_T>* pNewNode = new SkipListNode<VAL_T>(val,m);
 51         return pNewNode;
 52     }
 53 
 54     ~SkipList(){
 55         destory();
 56     }
 57     
 58     void insert(const VAL_T& val){
 59         auto newNode = createNode(val);
 60         auto cur = &_head;
 61         int curLevel = _M;
 62         while (curLevel-- && cur){
 63             SkipListNode<VAL_T> *next = cur->_nexts[curLevel];
 64             for ( ; next && next->_val < val; cur = next,next = next->_nexts[curLevel]);
 65             
 66             if (curLevel+1 <= newNode->level()){
 67                 newNode->_nexts[curLevel] = cur->_nexts[curLevel];
 68                 cur->_nexts[curLevel] = newNode;
 69             }
 70         }
 71     }
 72     
 73     VAL_T* find(const VAL_T& val){
 74         auto cur = &_head;
 75         int curLevel = _M;
 76         while (curLevel-- && cur){
 77             SkipListNode<VAL_T> *next = cur->_nexts[curLevel];
 78             for ( ; next && next->_val < val; cur = next,next = next->_nexts[curLevel]);
 79             
 80             if (next && next->_val == val){
 81                 return &next->_val;
 82             }
 83         }
 84         return nullptr;
 85     }
 86 
 87     void erase(const VAL_T& val){
 88         int curLevel = _M;
 89         SkipListNode<VAL_T> *target = nullptr, *cur = &_head;
 90         
 91         while (curLevel-- && cur){
 92             SkipListNode<VAL_T> *next = cur->_nexts[curLevel];
 93             for ( ; next && next->_val < val; cur = next,next = next->_nexts[curLevel]);
 94             
 95             if (next && next->_val == val){
 96                 cur->_nexts[curLevel] = next->_nexts[curLevel];
 97                 target = next;
 98             }
 99         }
100         if (target)
101             delete target;
102     }
103 
104     void printStruct(){
105         auto loop = &_head;
106         int curLevel = _M;
107         string sp(curLevel,'-');
108         while (curLevel-- && loop){
109             cout << "|" << sp ;
110             for (SkipListNode<VAL_T> *it = loop->_nexts[curLevel]; it ; it = it->_nexts[curLevel])
111                 cout << it->_val << sp;
112             cout << endl;
113             sp = sp.substr(0,sp.length() -1);
114         }
115     }
116 private:
117     SkipListNode<VAL_T> _head;
118     static size_t _M;
119 };
120 
121 template<typename VAL_T, size_t M> size_t SkipList<VAL_T,M>::_M = M;
122 
123 
124 int main()
125 {
126     SkipList<int, 10> mySkipList;
127     SkipList<int, 4> list4;
128 
129     vector<int> examples = {2,5,1,3,8,9,7,0,11,6,4,10,20,18,13};
130     for (auto it : examples){
131         list4.insert(it);
132     }
133     cout << "list4.find(9527): " << (list4.find(9527) != nullptr) << endl;
134     list4.printStruct();
135     list4.erase(11);
136     list4.printStruct();
137 
138     std::random_device rd;
139         std::mt19937 gen(rd());
140         std::uniform_int_distribution<> dis(0, 10000);
141     for (int i = 0; i < 1000; i++){
142         mySkipList.insert(dis(gen));
143     }
144 
145     cout << "mySkipList.find(9527): " << (mySkipList.find(9527) != nullptr) << endl;
146     mySkipList.printStruct();
147     
148     return 0;    
149 }
View Code

 

應用場景

跳躍表的發明者William Pugh對跳躍表的評價,“跳躍列表是在很多應用中有可能替代平衡樹而作為實現方法的一種資料結構。

跳躍列表的演算法有同平衡樹一樣的漸進的預期時間邊界,並且更簡單、更快速和使用更少的空間。” 目前在很多場景中都有應用,如Redis,LevelDB等。

 

引用:

《資料結構與演算法分析》

https://en.wikipedia.org/wiki/Skip_list

https://zhuanlan.zhihu.com/p/91753863

相關文章