数据结构-----跳表
鏈表
同數組比較,鏈表的插入刪除操作復雜度是O(1),然而如果想要查找一個節點,你可能會這么寫:
LinkedNode<T> *targetNode = headerNode; while(targetNode) {if(targetNode->element == theElement)return targetNode;targetNode = targetNode->next; } return NULL;你會發現每次都是從鏈表頭節點開始,時間復雜度是n(節點個數)。為了提高算法速度,就有了跳表的概念。
二分法
介紹跳表之前,先簡單解釋一下二分法。
對于一個有序序列來說,通常在查找某一個元素時,可以不需要從頭到尾一個個比較,而是使用二分的方式,步驟如下:
1.先將中間的元素和待查找元素比較。
2.1:如果相等, 返回
2.2:如果中間元素小于待查找元素,在右側部分中查找(返回步驟1)
2.3:如果中間元素大于待查找元素,在左側部分中查找(返回步驟1)
跳表
結合鏈表和二分法的特點,將鏈表進行加工,創造一個二者的結合體:
1.鏈表從頭節點到尾節點是有序的
2.可以進行跳躍查找(形如二分法)
next數組
實現第一步,只需在插入的時候進行有序插入即可,核心步驟在第二步,構建一個可以實現在節點之間跳躍的鏈。
對于普通鏈表而言,你的節點也許會長這個樣子(或者再多個構造析構函數?):
這里的next指針是我們用于將此節點同后面一個節點連接的橋梁,正如你知道的那樣,next是下一個節點的指針。
需要你考慮的事情來了,如果需要在不挨著的節點之間實現跳轉,那么對于某一個節點來說,它就需要存儲一個以上的指向別的節點的next指針。多個同種類型的數據通常可以采用數組存儲(vector, list等STL容器也可以啦),這樣,我們的跳表節點或許大概差不多應該長這個樣子:
暫且不論next數組的大小是多少,先讓我告訴你跳表的樣子。
你可能會覺得跳表這個東西很奇怪,不過最好不是因為我畫的比較丑。先不管豎著的線是什么,橫著看每一層好像都是一個鏈表,對嗎。另外我覺得你也應該注意一下豎著看的時候每一個節點的名字,它們也都是一樣的(不用考慮為什么有的節點有一個,有的有兩個甚至更多,這是插入函數應該解決的問題)。
所以我想你現在腦子中可能有了對next數組的一種假設,盡管事實可能并不是你想的那樣,不過不要緊,讓我借著這張圖告訴你next數組是什么。
通常我們會定義跳表的級數,簡單來解釋,就是層數-1(最下面一級是0級)。所以這張圖表示的跳表的級數是(0, 1, 2)。而在第2級的節點(比如node5),它的next數組大小就是(2 + 1) == 3, 在第1級的節點(比如node4),它的next數組的大小就是(1 + 1) == 2, 在第0級的節點(比如node3),它的next數組大小是1;
所以在申請一個skipNode的指針時,傳入這個節點所在的級數好像是理所應當的事情,然后對next進行初始化,把skipNode加工一下,像這樣:
template<class T> class skipNode<T> { public:skipNode(const T& theElement, size_t size):element(theElement){next = new skipNode<T>*[size+1];}T element;skipNode<T> **next; };現在開始解釋next數組,你最好看著上面的圖:
對于第2級的node5來說
對于第1級的node2來說
node2->next[1] == node4; node2->next[0] == node3;如果你還是不理解,我想你可以把每一列的同名節點看成是不同的節點,它們存的element數據是一樣的,而且每個節點只有一個指向下一個節點的指針。再把每一層看成一個鏈表。然后再看一遍。最后把每一列合成一個節點,它們只是有一個指針數組而已,你應該這么想。
find
我覺得這個函數可以幫助你更好地理解next數組的作用。如果沒有,唔,怪我表達能力太弱。。。
繼續看著這張圖,讓我先解釋一下headerNode和tailNode是什么(如果你因為它們的存在感到一絲困惑的話),它們是設計者(就是我們啦)人為添加的兩端節點,很像存在頭節點和尾節點的鏈表(又或者像隊列?),它們不存儲需要保存的有用的數據,但是需要人為給定一個數,讓它比整個跳表中的element都大,僅僅是用來判斷是否是頭和尾。當跳表為空時,級數為0,headerNode->next[0] == tailNode;
現在回到上面那個圖片上,如果我想要查找node6,想想二分法(但不是從中間開始)。
假設targetNode記錄著此時的節點,初值賦為headerNode;
第一次你應該比較的是headerNode->next[2]->element和theElement(待查找數據),也就是node5->element和theElement。顯然node5小于theElement(別忘了跳表的數據也是有序的),**這一步的結果導致下一次應該從第2級的node5開始查詢。**也就是令targetNode = targetNode->next[2];
第二次你應該比較targetNode->next[2]->element和theElement,也就是tailNode->element和theElement。看看前面,tailNode->element是最大的,對嗎。所以結果是大于,**這一步的結果導致下一次應該從第1級的node5開始查詢。**這里從第2級跳到第1級。但是沒有改變targetNode。
第三次你應該比較targetNode->next[1]->element和theElement,也就是node7->element和theElement。顯然也是大于,**這一步的結果導致下一次應該從第0級的node5開始查詢。**這里從第1級跳到第0級。也沒有改變targetNode。
第四次你應該比較targetNode->next[0]->element和theElement,也就是node6->element和theElement。這時你應該慶幸終于相等了,此時結束。如果小于,改變targetNode = targetNode->next[0](像第一步那樣),如果大于,結束,不然還往哪一級降,沒了,對嗎。
沒懂?接著看:
比較的時候的三種情況,以targetNode->next[i]->element和theElement為例:
1.小于:令targetNode = targetNode->next[i]; //第i級鏈表的下一個
2.大于:向下降級,i- - //不改變targetNode
3.等于:向下降級,i- - //不改變targetNode
退出后,再次比較targetNode->next[0]和theElement,判斷是否找到。
所以整個運算下來,targetNode是要查找的節點前面那個節點。如果你不明白,想想找不到那個節點時的情況,比如說,額,查找node6.6。
讓我們看看代碼:
insert
像紅黑樹,AVL樹一樣,跳表也是一種平衡結構,只不過這種平衡結構是利用隨機函數產生的級數維持的(終于填了前面的坑,還記得嗎,為什么有的節點有2級,有的只有0級)。
接著上面那個圖,想想插入node6.6的過程中會發生什么(假設隨機函數給它分配的是第2級)。。。靜思一秒鐘。
啊!插入之后會破壞每一級的結構,會破壞哪些節點的結構呢。。。靜思一秒鐘。
啊!自然是第2級的node5,第1級的node5,第0級的node6這些節點的next數組。
分析過后,第一步便是找到這幾個節點。我覺得你應該停一下想想怎么找,實在想不出來,參考find函數。
find函數中,for循環內套while循環,想想每次while循環結束之后的beforeNode是什么。哇哦,它就是我們要找的節點。我們用一個last數組記錄這些節點,last[i]表示第i級要找的節點。
第二步:利用隨機函數分配級數level
找節點是為了改變這些節點的next數組,分配級數是為了只改變找到的那些節點中位于第0級到第level級的節點。啊!你又想了,萬一隨機出的level比目前最大級數還大怎么辦。這時就將跳表增加一級唄,像這樣:
萬事俱備后,開始更新涉及到的節點的next數組,看代碼之前,先想想怎么在鏈表中插入節點。
skipNode<K,E> *newNode = new skipNode<K, E>(thePair, level+1);//因為只影響了[0, level]級的節點,更高級的節點不受影響 for(int i = 0; i <= level; ++i) {//每一級都插入新節點newNode->next[i] = last[i]->next[i];last[i]->next[i] = newNode; }總結一下插入函數,需要兩個額外的函數:
1.隨機函數,用于生成新節點的級數
2.初始化last數組,保存每一級中處在待插入位置前面的節點
erase
如果你理解了插入函數,刪除函數會帶給你從未有過的輕松感,至少比起還在文章開頭徘徊的你是這樣。噢對,還有一件事,或許你應該在回憶鏈表刪除的狀態里待幾秒鐘。如果沒有頭緒,插入函數最后一個for循環說不定會幫到你。
要刪除一個節點,自然是先找到這個節點啦,同時還要初始化last數組啦。然后從第0級開始到最高級,如果last[i]->next[i]是要刪除的節點,就刪除掉。直接上代碼:
template<class K, class E> void skipList<K, E>::erase(const K& theKey) {if(theKey >= tailNode->first)return;skipNode<K, E> *theNode = search(theKey);if(theNode->element.first != theKey)return; //不存在要刪除的節點int i = 0;while(i <= curLevels && last[i]->next[i] == theNode){//每一級的鏈表刪除last[i]->next[i] = theNode->next[i];i++;}//如果刪除的是最高級,且未刪除前,最高級就一個節點,則當前節點減一while(curLevles > 0 && headerNode->next[curLevels] == tailNode)curLevels--;delete theNode;dSize--; }現在唯一可能會讓你感到頭疼的就是構造函數了。想想構造函數應該做些什么,對數據成員進行初始化?啊,對,那是所有構造函數應該做的事情。我說的是具體應該怎么做。
我猜你看上述代碼的時候會對像cutOff, curLevels這些憑空出現的變量有點不理解,沒關系,正如你想的那樣,把它們放到private數據成員里。總結一下讓你感到困惑的變量:
構造函數首先初始化這些變量,然后給節點申請內存,將頭節點和尾節點連接起來(因為此時跳表是空的)。就這么簡單。
//prob: 是第i級同時又是第i-1級的概率 //maxPairs: 跳表可以存儲的最多節點個數 //largetKey: 最大鍵 template<class K, class E> skipList<K, E>::skipList(K largerKey, int maxPairs, double prob):cutOff(prob * RAND_MAX),maxLevel((int)ceil(logf((float)maxPairs) / logf(1 / prob)) - 1),cutLevels(0),dSize(0) {pair<K, E> tailPair;tailPair.first = largerKey;headerNode = new skipNode<K, E>(tailPair, maxLevel+1);tailNode = new skipNode<K, E>(tailPair, 0);last = new skipNode<K, E>*[maxLevel+1];for(int i = 0; i <= maxLevel; ++i){headerNode->next[i] = tailNode; } }析構函數就沒什么了,對第0級的每一個節點,釋放它的next數組(可以寫在skipNode的析構函數中),然后釋放它自己。
完整代碼下載
總結
以上是生活随笔為你收集整理的数据结构-----跳表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Qt学习笔记-----事件
- 下一篇: 数据结构-----二叉树,树,森林之间的