前言
最近接觸到了跳躍表,感覺很牛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 }
應用場景
跳躍表的發明者William Pugh對跳躍表的評價,“跳躍列表是在很多應用中有可能替代平衡樹而作為實現方法的一種資料結構。
跳躍列表的演算法有同平衡樹一樣的漸進的預期時間邊界,並且更簡單、更快速和使用更少的空間。” 目前在很多場景中都有應用,如Redis,LevelDB等。
引用:
《資料結構與演算法分析》
https://en.wikipedia.org/wiki/Skip_list
https://zhuanlan.zhihu.com/p/91753863