日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

数据结构-----跳表

發布時間:2024/4/19 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 数据结构-----跳表 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

鏈表

同數組比較,鏈表的插入刪除操作復雜度是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數組
實現第一步,只需在插入的時候進行有序插入即可,核心步驟在第二步,構建一個可以實現在節點之間跳躍的鏈。
對于普通鏈表而言,你的節點也許會長這個樣子(或者再多個構造析構函數?):

template<class T> class linkedNode { public:T element;linkedNode<T> *next; };

這里的next指針是我們用于將此節點同后面一個節點連接的橋梁,正如你知道的那樣,next是下一個節點的指針。
需要你考慮的事情來了,如果需要在不挨著的節點之間實現跳轉,那么對于某一個節點來說,它就需要存儲一個以上的指向別的節點的next指針。多個同種類型的數據通常可以采用數組存儲(vector, list等STL容器也可以啦),這樣,我們的跳表節點或許大概差不多應該長這個樣子:

template<class T> class skipNode { public:T element;skipNode<T> **next; //一個存儲著skipNode<T>*類型變量的一維數組 };

暫且不論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來說

node5->next[2] == tailNode; node5->next[1] == node7; node5->next[0] == node6;

對于第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。
讓我們看看代碼:

template<class K, class E> pair<const K, E> *skipNode<K, E>::find(const K& theKey) {skipNode<K, E>* beforeNode = headerNode;for(int i = curLevels; i >= 0; --i) //降級{while(beforeNode->next[i]->element.first < theKey)beforeNode = beforeNode->next[i]; //跳轉到同一級的下一個}//跳出存在兩種可能//1.找到了,此時 if(beforeNode->next[0]->element.first == theKey)//2.沒找到,此時 if(beforeNode->next[0]->element.first > theKey)if(beforeNode->next[0]->element.first == theElement.first)return &beforeNode->next[0]->element;return NULL; }

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比目前最大級數還大怎么辦。這時就將跳表增加一級唄,像這樣:

int level = levels(); if(level > curLevels) {level = ++curLevels;last->next[level] = heaerNode; //增加一級的同時也要改變last數組//用于將新節點插入時newNode->next[level] = last[level]->next[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數組,保存每一級中處在待插入位置前面的節點

//隨機函數,為新節點生成級數 template<class K, class E> int skipList<K, E>::levels() {int level = 0;//cutOff預先設定的值,cutOff = prob * MAX_RAND;//prob:是第i級同時又是第i-1級的概率,人為設定while(rand() > cutOff) level++;return level; }template<class K, class E> skipNode<K, E>* skipList<K, E>::search(const K& theKey) {//與find函數相同skipNode<K, E>* beforeNode = headerNode;for(int i = curLevels; i >= 0; --i){while(beforeNode->next[i]->element.first < theKey)beforeNode = beforeNode->next[i];last[i] = beforeNode; //找到每一級待插入位置前面的節點}//返回找到的節點,用于判斷是否已存在鍵為theKey的節點return beforeNode->next[0]; }template<class K, class E> void skipList<K, E>::insert(const pair<const K, E>& thePair) {if(thePair.first >= tailNode->element.first)return;skipNode<K, E> *theNode = search(thePair.first);if(theNode->element.first == thePair.first) //判斷是否存在該節點{theNode->element.second = thePair.second; //更新值return;}int level = levels();//cutLevels:記錄當前最大級數if(level > curLevels){level = ++curLevels; //級數加一,讓新節點作為最高級節點last[level] = headerNode;}//每一級如同鏈表插入一樣進行插入skipNode<K, E> *newNode = new skipNode<K, E>(thePair, level+1);for(int i = 0; i <= level; ++i){newNode->next[i] = last[i]->next[i];last[i]->next[i] = newNode;}dSize++; }

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數據成員里。總結一下讓你感到困惑的變量:

int dSize; //記錄著跳表中節點的個數,初始為0 int curLevels; //記錄跳表當前最大級數,如果是圖片那樣的話,它的值是2 int cutOff; //用于隨機函數的隨機算法,一個很大的值 int maxLevel; //跳表可以存儲的最大級數 skipNode<K, E> *heaerNode; //跳表頭節點,鍵是跳表中最大的 skipNode<K, E> *tailNode; //尾節點,鍵同headerNode skipNode<K, E> **last; //記錄每一級中要查找的節點前面那個節點

構造函數首先初始化這些變量,然后給節點申請內存,將頭節點和尾節點連接起來(因為此時跳表是空的)。就這么簡單。

//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的析構函數中),然后釋放它自己。

完整代碼下載

總結

以上是生活随笔為你收集整理的数据结构-----跳表的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。