從演變歷史,看透本質 查詢演算法以及紅黑樹

2021-09-25 23:44:15 字數 3971 閱讀 8432

二分查詢

前提是有序,且靜止不變。

我們使用有序陣列儲存鍵,經典的二分查詢能夠根據陣列的索引大大減少每次查詢所需的比較次數。

在查詢時,我們先將被查詢的鍵和子陣列的中間鍵比較。如果被查詢的鍵小於中間鍵,我們就在左子陣列中繼續查詢,如果大於我們就在右子陣列中繼續查詢,否則中間鍵就是我們要找的鍵。

一般情況下二分查詢都比順序查詢快的多,它也是眾多實際應用程式的最佳選擇。對於乙個靜態表(不允許插入)來說,將其在初始化時就排序是值得的。

當然,二分查詢也適合很多應用。現代應用需要同時能夠支援高效的查詢和插入兩種操作的符號表實現。也就是說,我們需要在構造龐大的符號表的同時能夠任意插入(也許還有刪除)鍵值對,同時也要能夠完成查詢操作。(查詢演算法努力的方向。)

要支援高效的插入操作,我們似乎需要一種鏈式結構。當單鏈結的鍊錶是無法使用二分查詢的,因為二分查詢的高效來自於能夠快速通過索引取得任何子陣列的中間元素。為了將二分查詢的效率和鍊錶的靈活性結合起來,我們需要更加複雜的資料結構。

能夠同時擁有兩者的就是二叉查詢樹。

二叉查詢樹

一顆二叉查詢樹(bst)是一顆二叉樹,其中每個節點都含有乙個可比較的鍵(以及相關聯的值)且每個結點的鍵都大於其左子樹中的任意結點的鍵而小於右子樹的任意結點的鍵。

一顆二叉查詢樹代表了一組鍵(及其相應的值)的集合,而同乙個集合可以用多棵不同的二叉查詢樹表示。

如果我們將一棵二叉查詢樹的所有鍵投影到一條直線上,保證乙個結點的左子樹中的鍵出現在它的右邊,右子樹中的鍵出現在它的右邊,那麼我們一定可以得到一條有序的鍵列。

查詢

在二叉查詢樹中查詢乙個鍵的遞迴演算法:

如果樹是空的,則查詢未命中。如果被查詢的鍵和根結點的鍵相等,查詢命中。否則我們就在適當的子樹中繼續查詢。如果被查詢的鍵較小就選擇左子樹,較大就選擇右子樹。

在二叉查詢樹中,隨著我們不斷向下查詢,當前結點所表示的子樹的大小也在減小(理想情況下是減半)

插入

查詢**幾乎和二分查詢的一樣簡單,這種簡潔性是二叉查詢樹的重要特性之一。而二叉查詢樹的另乙個更重要的特性就是插入的實現難度和查詢差不多。

當查詢乙個不存在於樹中的結點並結束於一條空鏈結時,我們需要做的就是將鏈結指向乙個含有被查詢的鍵的新結點。如果被查詢的鍵小於根結點的鍵,我們會繼續在左子樹中插入該鍵,否則在右子樹中插入該鍵。

分析使用二叉查詢樹的演算法的執行時間取決於樹的形狀,而樹的形狀又取決於鍵被插入的先後順序

在最好的情況下,一顆含有n個結點的樹是完全平衡的,每條空鏈結和根結點的距離都為~lgn。在最壞的情況下,搜尋路徑上可能有n個結點。但在一般情況下樹的形狀和最好情況更接近。

我們假設鍵的插入順序是隨機的。對這個模型的分析而言,二叉查詢樹和快速排序幾乎就是「雙胞胎」。樹的根結點就是快速排序中的第乙個切分元素(左側的鍵都比它小,右側的鍵都比它大),而這對於所有的子樹同樣適用,這和快速排序中對於子陣列的遞迴排序完全對應。

【在由n個隨機鍵構造的二叉查詢樹中,查詢命中平均所需的比較次數為~2lgn。 n越大這個公式越準確】

在一顆含有n個結點的樹中,我們希望樹高為~lgn,這樣我們就能保證所有查詢都能在~lgn此比較內結束,就和二分查詢一樣。不幸的是,在動態插入中保證樹的完美平衡的代價太高了。我們放鬆對完美平衡的要求,使符號表api中所有操作均能夠在對數時間內完成。

2-3查詢樹

為了保證查詢樹的平衡性,我們需要一些靈活性,因此在這裡我們允許樹中的乙個結點儲存多個鍵

(2-3指的是2叉-3叉的意思)

一顆完美平衡的2-3查詢樹中的所有空鏈結到根結點的距離都是相同的。

查詢要判斷乙個鍵是否在樹中,我們先將它和根結點中的鍵比較。如果它和其中的任何乙個相等,查詢命中。否則我們就根據比較的結果找到指向相應區間的鏈結,並在其指向的子樹中遞迴地繼續查詢。如果這是個空鏈結,查詢未命中。

插入

要在2-3樹中插入乙個新結點,我們可以和二叉查詢樹一樣先進行一次未命中的查詢,然後把新結點掛在樹的底部。但這樣的話樹無法保持完美平衡性。我們使用2-3樹的主要原因就在於它能夠在插入之後繼續保持平衡

如果未命中的查詢結束於乙個2-結點,我們只要把這個2-結點替換為乙個3-結點,將要插入的鍵儲存在其中即可。如果未命中的查詢結束於乙個3-結點,事情就要麻煩一些。

熱身:先考慮最簡單的例子:只有乙個3-結點的樹,向其插入乙個新鍵。

這棵樹唯一的結點中已經沒有可插入的空間了。我們又不能把新鍵插在其空結點上(破壞了完美平衡)。為了將新鍵插入,我們先臨時將新鍵存入該結點中,使之成為乙個4-結點。建立乙個4-結點很方便,因為很容易將它轉換為一顆由3個2-結點組成的2-3樹(如圖所示),這棵樹既是一顆含有3個結點的二叉查詢樹,同時也是一顆完美平衡的2-3樹,其中所有空鏈結到根結點的距離都相等。

向乙個父結點為2-結點的3-結點中插入新鍵

假設未命中的查詢結束於乙個3-結點,而它的父結點是乙個2-結點。在這種情況下我們需要在維持樹的完美平衡的前提下為新鍵騰出空間。

我們先像剛才一樣構造乙個臨時的4-結點並將其分解,但此時我們不會為中鍵建立乙個新結點,而是將其移動至原來的父結點中。(如圖所示)

這次轉換也並不影響(完美平衡的)2-3樹的主要性質。樹仍然是有序的,因為中鍵被移動到父結點中去了,樹仍然是完美平衡的,插入後所有的空鏈結到根結點的距離仍然相同。

向乙個父結點為3-結點的3-結點中插入新鍵

假設未命中的查詢結束於乙個3-結點,而它的父結點是乙個3-結點。

我們再次和剛才一樣構造乙個臨時的4-結點並分解它,然後將它的中鍵插入它的父結點中。但父結點也是乙個3-結點,因此我們再用這個中鍵構造乙個新的臨時4-結點,然後在這個結點上進行相同的變換,即分解這個父結點並將它的中鍵插入到它的父結點中去。

我們就這樣一直向上不斷分解臨時的4-結點並將中鍵插入更高的父結點,直至遇到乙個2-結點並將它替換為乙個不需要繼續分解的3-結點,或者是到達3-結點的根。

總結:先找插入結點,若結點有空(即2-結點),則直接插入。如結點沒空(即3-結點),則插入使其臨時容納這個元素,然後**此結點,把中間元素移到其父結點中。對父結點亦如此處理。(中鍵一直往上移,直到找到空位,在此過程中沒有空位就先搞個臨時的,再**。)

★2-3樹插入演算法的根本在於這些變換都是區域性的:除了相關的結點和鏈結之外不必修改或者檢查樹的其他部分。每次變換中,變更的鏈結數量不會超過乙個很小的常數。所有區域性變換都不會影響整棵樹的有序性和平衡性。

構造和標準的二叉查詢樹由上向下生長不同,2-3樹的生長是由下向上的。

優點2-3樹在最壞情況下仍有較好的效能。每個操作中處理每個結點的時間都不會超過乙個很小的常數,且這兩個操作都只會訪問一條路徑上的結點,所以任何查詢或者插入的成本都肯定不會超過對數級別。

完美平衡的2-3樹要平展的多。例如,含有10億個結點的一顆2-3樹的高度僅在19到30之間。我們最多隻需要訪問30個結點就能在10億個鍵中進行任意查詢和插入操作。

缺點我們需要維護兩種不同型別的結點,查詢和插入操作的實現需要大量的**,而且它們所產生的額外開銷可能會使演算法比標準的二叉查詢樹更慢。

從檔案中查詢關鍵字演算法

1 原始檔為乙個txt文件,內容為符號串 2 給定乙個關鍵字檔案,內容為自定義的關鍵字 注 關鍵字有若干個,用空格隔開 3 依據關鍵字檔案中的關鍵字在原始檔中進行檢索判斷,得到關鍵字 include stdio.h include string.h include malloc.h definebu...

從檔案中查詢關鍵字演算法

1 原始檔為乙個txt文件,內容為符號串 2 給定乙個關鍵字檔案,內容為自定義的關鍵字 注 關鍵字有若干個,用空格隔開 3 依據關鍵字檔案中的關鍵字在原始檔中進行檢索判斷,得到關鍵字 include stdio.h include string.h include malloc.h define b...

從計算機的演算法,談談提高效率的本質

吳軍 谷歌方 學習筆記。本文以計算機中的排序演算法為例,說明了提高效率的本質。在排序演算法中,常見的氣泡排序,插入排序,它們的時間複雜度都是o n平方 接下來思考如何提高排序演算法的效率呢?節省20 的計算量是沒有意義的事情,就算省個三五倍也沒有意義,要省就乾脆多省一點,省它個成千上萬倍甚至更多。於...