1. 程式人生 > >最簡單清晰的B樹、Trie樹詳解

最簡單清晰的B樹、Trie樹詳解

查詢(二)

散列表

散列表是普通陣列概念的推廣。由於對普通陣列可以直接定址,使得能在O(1)時間內訪問陣列中的任意位置。在散列表中,不是直接把關鍵字作為陣列的下標,而是根據關鍵字計算出相應的下標。

使用雜湊的查詢演算法分為兩步。第一步是用雜湊函式將被查詢的鍵轉化為陣列的一個索引。

我們需要面對兩個或多個鍵都會雜湊到相同的索引值的情況。因此,第二步就是一個處理碰撞衝突的過程,由兩種經典解決碰撞的方法:拉鍊法和線性探測法。

散列表是演算法在時間和空間上作出權衡的經典例子。

如果沒有記憶體限制,我們可以直接將鍵作為(可能是一個超大的)陣列的索引,那麼所有查詢操作只需要訪問記憶體一次即可完成。但這種情況不會經常出現,因此當鍵很多時需要的記憶體太大。

另一方面,如果沒有時間限制,我們可以使用無序陣列並進行順序查詢,這樣就只需要很少的記憶體。而散列表則使用了適度的空間和時間並在這兩個極端之間找到了一種平衡

●雜湊函式

我們面對的第一個問題就是雜湊函式的計算,這個過程會將鍵轉化為陣列的索引。我們要找的雜湊函式應該易於計算並且能夠均勻分佈所有的鍵。

雜湊函式和鍵的型別有關,對於每種型別的鍵我們都需要一個與之對應的雜湊函式。

正整數

將整數雜湊最常用的方法就是除留餘數法。我們選擇大小為素數M的陣列,對於任意正整數k,計算k除以M的餘數。(如果M不是素數,我們可能無法利用鍵中包含的所有資訊,這可能導致我們無法均勻地雜湊值。)

浮點數

將鍵表示為二進位制數,然後再使用除留餘數法。(讓浮點數的各個位都起作用)(Java就是這麼做的)

字串

除留餘數法也可以處理較長的鍵,例如字串,我們只需將它們當做大整數即可。即相當於將字串當做一個N位的R進位制值,將它除以M並取餘

·····軟快取

如果雜湊值的計算很耗時,那麼我們或許可以將每個鍵的雜湊值快取起來,即在每個鍵中使用一個hash變數來儲存它的hashCode()返回值。

●基於拉鍊法的散列表

一個雜湊函式能夠將鍵轉化為陣列索引。雜湊演算法的第二步是碰撞處理,也就是處理兩個或多個鍵的雜湊值相同的情況。

拉鍊法:將大小為M的陣列中的每個元素指向一條連結串列,連結串列中的每個結點都儲存了雜湊值為該元素的索引的鍵值對。

查詢分兩步:首先根據雜湊值找到對應的連結串列,然後沿著連結串列順序查詢相應的鍵。


拉鍊法在實際情況中很有用,因為每條連結串列確實都大約含有N/M個鍵值對。

基於拉鍊法的散列表的實現簡單。在鍵的順序並不重要的應用中,它可能是最快的(也是使用最廣泛的)符號表實現。

●基於線性探測法的散列表

實現散列表的另一種方式就是用大小為M的陣列儲存N個鍵值對,其中M>N。我們需要依靠陣列中的空位解決碰撞衝突。基於這種策略的所有方法被統稱為開放地址散列表。

開放地址散列表中最簡單的方法叫做線性探測法:當碰撞發生時,我們直接檢查散列表中的下一個位置(將索引值加1),如果不同則繼續查詢,直到找到該鍵或遇到一個空元素。

(開放地址類的散列表的核心思想是:與其將記憶體用作連結串列,不如將它們作為在散列表的空元素。這些空元素可以作為查詢結束的標誌。)

特點:雜湊最主要的目的在於均勻地將鍵散佈開來,因此在計算雜湊後鍵的順序資訊就丟失了,如果你需要快速找到最大或最小的鍵,或是查詢某個範圍內的鍵,散列表都不是合適的選擇。

【應用舉例】

海量處理

給定a、b兩個檔案,各存放50億個url,每個url各佔64位元組,記憶體限制是4G,讓你找出a、b檔案共同的url?

答:

可以估計每個檔案安的大小為5G×64=320G,遠遠大於記憶體限制的4G。所以不可能將其完全載入到記憶體中處理。考慮採取分而治之的方法。

 分而治之/hash對映

遍歷檔案a,對每個url求取,然後根據所取得的值將url分別儲存到1000個小檔案(記為,這裡漏寫個了a1)中。這樣每個小檔案的大約為300M。遍歷檔案b,採取和a相同的方式將url分別儲存到1000小檔案中(記為)。這樣處理後,所有可能相同的url都在對應的小檔案()中,不對應的小檔案不可能有相同的url。然後我們只要求出1000對小檔案中相同的url即可。

 hash_set統計

求每對小檔案中相同的url時,可以把其中一個小檔案的url儲存到hash_set中。然後遍歷另一個小檔案的每個url,看其是否在剛才構建的hash_set中,如果是,那麼就是共同的url,存到檔案裡面就可以了。

(此題來源於v_July_v的部落格)

B樹(多向平衡查詢樹)

B-樹是對2-3樹資料結構的擴充套件。它支援對儲存在磁碟或者網路上的符號表進行外部查詢,這些檔案可能比我們以前考慮的輸入要大的多(以前的輸入能夠儲存在記憶體中)。

(B樹和B+樹是實現資料庫的資料結構,一般程式設計師用不到它。)

和2-3樹一樣,我們限制了每個結點中能夠含有的“鍵-連結”對的上下數量界限:一個M階的B-樹,每個結點最多含有M-1對鍵-連結(假設M足夠小,使得每個M向結點都能夠存放在一個頁中),最少含有M/2對鍵-連結,但也不能少於2對。

(B樹是用於儲存海量資料的,一般其一個結點就佔用磁碟一個塊的大小。)

【注】以下B樹部分參考自July的部落格,尤其是插入及刪除示圖,為了省力直接Copy自July。


B樹中的結點存放的是鍵-值對。圖中紅色方塊即為鍵對應值的指標。

B樹中的每個結點根據實際情況可以包含大量的關鍵字資訊和分支(當然是不能超過磁碟塊的大小,根據磁碟驅動(diskdrives)的不同,一般塊的大小在1k~4k左右);這樣樹的深度降低了,這就意味著查詢一個元素只要很少結點從外存磁碟中讀入記憶體,很快訪問到要查詢的資料。

查詢

假如每個盤塊可以正好存放一個B樹的結點(正好存放2個檔名)。那麼一個BTNODE結點就代表一個盤塊,而子樹指標就是存放另外一個盤塊的地址。

下面,咱們來模擬下查詢檔案29的過程:

1.  根據根結點指標找到檔案目錄的根磁碟塊1,將其中的資訊匯入記憶體。【磁碟IO操作1次】   

2.  此時記憶體中有兩個檔名17、35和三個儲存其他磁碟頁面地址的資料。根據演算法我們發現:17<29<35,因此我們找到指標p2。

3.  根據p2指標,我們定位到磁碟塊3,並將其中的資訊匯入記憶體。【磁碟IO操作 2次】   

4.  此時記憶體中有兩個檔名26,30和三個儲存其他磁碟頁面地址的資料。根據演算法我們發現:26<29<30,因此我們找到指標p2。

5.  根據p2指標,我們定位到磁碟塊8,並將其中的資訊匯入記憶體。【磁碟IO操作 3次】   

6.  此時記憶體中有兩個檔名28,29。根據演算法我們查詢到檔名29,並定位了該檔案記憶體的磁碟地址。分析上面的過程,發現需要3 3次磁碟IO操作和次磁碟IO操作和3次記憶體查詢 次記憶體查詢操作。關於記憶體中的檔名查詢,由於是一個有序表結構,可以利用折半查詢提高效率。至於IO操作是影響整個B樹查詢效率的決定因素。

插入

想想2-3樹的插入。2-3樹結點的最大容量是2個元素,故當插入操作造成超出容量之後,就得分裂。同樣m-階B樹規定的結點的最大容量是m-1個元素,故當插入操作造成超出容量之後也得分裂,其分裂成兩個結點每個結點分m/2個元素。(副作用是在其父結點中要插入一箇中間元素,用於分隔這兩結點。和2-3樹一樣,再向父結點插入一個元素也可能會造成父結點的分裂,逐級向上操作,直到不再造成分裂為止。)

向某結點中插入一個元素使其分裂,可能會造成連鎖反應,使其之上的結點也可能造成分裂。

總結:在B樹中插入關鍵碼key的思路:

對高度為h的m階B樹,新結點一般是插在第h層。通過檢索可以確定關鍵碼應插入的結點位置。然後分兩種情況討論:

1、  若該結點中關鍵碼個數小於m-1,則直接插入即可。

2、  若該結點中關鍵碼個數等於m-1,則將引起結點的分裂。以中間關鍵碼為界將結點一分為二,產生一個新結點,並把中間關鍵碼插入到父結點(h-1層)中

重複上述工作,最壞情況一直分裂到根結點,建立一個新的根結點,整個B樹增加一層。

【例】

1、下面咱們通過一個例項來逐步講解下。插入以下字元字母到一棵空的B 樹中(非根結點關鍵字數小了(小於2個)就合併,大了(超過4個)就分裂):C N G A H E K Q M F W L T Z D P R X Y S,首先,結點空間足夠,4個字母插入相同的結點中,如下圖:


2、當咱們試著插入H時,結點發現空間不夠,以致將其分裂成2個結點,移動中間元素G上移到新的根結點中,在實現過程中,咱們把A和C留在當前結點中,而H和N放置新的其右鄰居結點中。如下圖:


3、當咱們插入E,K,Q時,不需要任何分裂操作


4、插入M需要一次分裂,注意M恰好是中間關鍵字元素,以致向上移到父節點中


5、插入F,W,L,T不需要任何分裂操作


6、插入Z時,最右的葉子結點空間滿了,需要進行分裂操作,中間元素T上移到父節點中,注意通過上移中間元素,樹最終還是保持平衡,分裂結果的結點存在2個關鍵字元素。


7、插入D時,導致最左邊的葉子結點被分裂,D恰好也是中間元素,上移到父節點中,然後字母P,R,X,Y陸續插入不需要任何分裂操作(別忘了,樹中至多5個孩子)。


8、最後,當插入S時,含有N,P,Q,R的結點需要分裂,把中間元素Q上移到父節點中,但是情況來了,父節點中空間已經滿了,所以也要進行分裂,將父節點中的中間元素M上移到新形成的根結點中,注意以前在父節點中的第三個指標在修改後包括D和G節點中。這樣具體插入操作的完成,下面介紹刪除操作,刪除操作相對於插入操作要考慮的情況多點。


刪除(delete)操作

首先查詢B樹中需刪除的元素,如果該元素在B樹中存在,則將該元素在其結點中進行刪除,如果刪除該元素後,首先判斷該元素是否有左右孩子結點,如果有,則上移孩子結點中的某相近元素(“左孩子最右邊的節點”或“右孩子最左邊的節點”)到父節點中,然後是移動之後的情況;如果沒有,直接刪除後,移動之後的情況。

刪除元素,移動相應元素之後,如果某結點中元素數目(即關鍵字數)小於ceil(m/2)-1,則需要看其某相鄰兄弟結點是否豐滿(結點中元素個數大於ceil(m/2)-1)(還記得第一節中關於B樹的第5個特性中的c點麼?: c)除根結點之外的結點(包括葉子結點)的關鍵字的個數n必須滿足: (ceil(m / 2)-1)<= n <=m-1。m表示最多含有m個孩子,n表示關鍵字數。在本小節中舉的一顆B樹的示例中,關鍵字數n滿足:2<=n<=4),如果豐滿,則向父節點借一個元素來滿足條件;如果其相鄰兄弟都剛脫貧,即借了之後其結點數目小於ceil(m/2)-1,則該結點與其相鄰的某一兄弟結點進行“合併”成一個結點,以此來滿足條件。那咱們通過下面例項來詳細瞭解吧。

以上述插入操作構造的一棵5階B樹(樹中最多含有m(m=5)個孩子,因此關鍵字數最小為ceil(m/ 2)-1=2。還是這句話,關鍵字數小了(小於2個)就合併,大了(超過4個)就分裂)為例,依次刪除H,T,R,E。

1、首先刪除元素H,當然首先查詢H,H在一個葉子結點中,且該葉子結點元素數目3大於最小元素數目ceil(m/2)-1=2,則操作很簡單,咱們只需要移動K至原來H的位置,移動L至K的位置(也就是結點中刪除元素後面的元素向前移動)



2、下一步,刪除T,因為T沒有在葉子結點中,而是在中間結點中找到,咱們發現他的繼承者W(字母升序的下個元素),將W上移到T的位置,然後將原包含W的孩子結點中的W進行刪除,這裡恰好刪除W後,該孩子結點中元素個數大於2,無需進行合併操作。


3、下一步刪除R,R在葉子結點中,但是該結點中元素數目為2,刪除導致只有1個元素,已經小於最小元素數目ceil(5/2)-1=2,而由前面我們已經知道:如果其某個相鄰兄弟結點中比較豐滿(元素個數大於ceil(5/2)-1=2),則可以向父結點借一個元素,然後將最豐滿的相鄰兄弟結點中上移最後或最前一個元素到父節點中(有沒有看到紅黑樹中左旋操作的影子?),在這個例項中,右相鄰兄弟結點中比較豐滿(3個元素大於2),所以先向父節點借一個元素W下移到該葉子結點中,代替原來S的位置,S前移;然後X在相鄰右兄弟結點中上移到父結點中,最後在相鄰右兄弟結點中刪除X,後面元素前移。


4、最後一步刪除E, 刪除後會導致很多問題,因為E所在的結點數目剛好達標,剛好滿足最小元素個數(ceil(5/2)-1=2),而相鄰的兄弟結點也是同樣的情況,刪除一個元素都不能滿足條件,所以需要該節點與某相鄰兄弟結點進行合併操作;首先移動父結點中的元素(該元素在兩個需要合併的兩個結點元素之間)下移到其子結點中,然後將這兩個結點進行合併成一個結點。所以在該例項中,咱們首先將父節點中的元素D下移到已經刪除E而只有F的結點中,然後將含有D和F的結點和含有A,C的相鄰兄弟結點進行合併成一個結點。


5、也許你認為這樣刪除操作已經結束了,其實不然,在看看上圖,對於這種特殊情況,你立即會發現父節點只包含一個元素G,沒達標(因為非根節點包括葉子結點的關鍵字數n必須滿足於2=<n<=4,而此處的n=1),這是不能夠接受的。如果這個問題結點的相鄰兄弟比較豐滿,則可以向父結點借一個元素。假設這時右兄弟結點(含有Q,X)有一個以上的元素(Q右邊還有元素),然後咱們將M下移到元素很少的子結點中,將Q上移到M的位置,這時,Q的左子樹將變成M的右子樹,也就是含有N,P結點被依附在M的右指標上。所以在這個例項中,咱們沒有辦法去借一個元素,只能與兄弟結點進行合併成一個結點,而根結點中的唯一元素M下移到子結點,這樣,樹的高度減少一層。


為了進一步詳細討論刪除的情況,再舉另外一個例項

這裡是一棵不同的5序B樹,那咱們試著刪除C


於是將刪除元素C的右子結點中的D元素上移到C的位置,但是出現上移元素後,只有一個元素的結點的情況。

又因為含有E的結點,其相鄰兄弟結點才剛脫貧(最少元素個數為2),不可能向父節點借元素,所以只能進行合併操作,於是這裡將含有A,B的左兄弟結點和含有E的結點進行合併成一個結點。


這樣又出現只含有一個元素F結點的情況,這時,其相鄰的兄弟結點是豐滿的(元素個數為3>最小元素個數2),這樣就可以想父結點借元素了,把父結點中的J下移到該結點中,相應的如果結點中J後有元素則前移,然後相鄰兄弟結點中的第一個元素(或者最後一個元素)上移到父節點中,後面的元素(或者前面的元素)前移(或者後移);注意含有K,L的結點以前依附在M的左邊,現在變為依附在J的右邊。這樣每個結點都滿足B樹結構性質。


從以上操作可看出:除根結點之外的結點(包括葉子結點)的關鍵字的個數n滿足:(ceil(m / 2)-1)<= n <= m-1,即2<=n<=4。這也佐證了咱們之前的觀點。刪除操作完。


(我思:)

(1、       關於B樹中指標的表示。指標就是線索,是為了指示你找到目標。在記憶體中用記憶體的線性地址表示,在磁碟上,用磁碟的柱面和磁軌號表示。

(2、       B樹也是一種檔案組織形式。它與OS檔案系統的區別是,檔案系統是面向磁碟上各種應用的檔案的,所有檔案的索引都被組織在一個系統檔案表中。這樣,一個相關應用的檔案之間就沒有體現有序性,我們對某組相關的檔案進行查詢,效率就會較低。  而B樹是專門對某組相關的檔案進行組織,使其之間相對有序,提高查詢效率。 --尤其是對於需要頻繁查詢訪問檔案的操作。

例如: 對10億個有序數,其分佈在1000個檔案中。普通的查詢(類2分查詢),和構造一個B樹,普通的二分查詢不僅需要多次訪問檔案,且其通過OS的檔案系統通過檔名來訪問檔案,這樣效率低——OS需要在整張系統檔案表中通過檔名查詢檔案。  而B樹,其是多叉樹,樹的深度比二分樹要小很多,需要查詢的檔案比二分查詢需要的少。且其通過自己建立的B樹來索引檔案(每次查詢檔案都通過該B樹得到檔案在磁碟上的位置)。B樹是獨立於OS的檔案系統的,它中的每個檔案都有相應的磁碟位置,而不僅是檔名。

B+樹

B+ tree:是應檔案系統所需而產生的一種B-tree的變形樹。

一棵m階的B+樹和m階的B樹的異同點在於:

1、有n棵子樹的結點中含有n-1 個關鍵字; (與B 樹n棵子樹有n-1個關鍵字 保持一致,)

2、所有的葉子結點中包含了全部關鍵字的資訊,及指向含有這些關鍵字記錄的指標,且葉子結點本身依關鍵字的大小自小而大的順序連結。 

3、所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字。 

【總結:最大的區別在於,B樹是像2-3樹那樣把資料分散到所有的結點中,而B+樹的資料都集中在葉結點,上層結點只是資料的索引,並不包含資料資訊】


【應用舉例】

1、為什麼說B+-tree比B 樹更適合實際應用中作業系統的檔案索引和資料庫索引?

資料庫索引採用B+樹的主要原因是 B樹在提高了磁碟IO效能的同時並沒有解決元素遍歷的效率低下的問題。正是為了解決這個問題,B+樹應運而生。

B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷。而且在資料庫中基於範圍的查詢是非常頻繁的,而B樹需要遍歷整棵樹,效率太低。

2、B+-tree的應用: VSAM(虛擬儲存存取法)檔案

B樹與B+樹

走進搜尋引擎的作者樑斌老師針對B樹、B+樹給出了他的意見(來源於July):

“B+樹還有一個最大的好處,方便掃庫,B樹必須用中序遍歷的方法按序掃庫,而B+樹直接從葉子結點挨個掃一遍就完了,B+樹支援range-query非常方便,而B樹不支援。這是資料庫選用B+樹的最主要原因。

比如要查 5-10之間的,B+樹一把到5這個標記,再一把到10,然後串起來就行了,B樹就非常麻煩。B樹的好處,就是成功查詢特別有利,因為樹的高度總體要比B+樹矮。不成功的情況下,B樹也比B+樹稍稍佔一點點便宜。B樹比如你的例子中查,17的話,一把就得到結果了。

有很多基於頻率的搜尋是選用B樹,越頻繁query的結點越往根上走,前提是需要對query做統計,而且要對key做一些變化。

另外B樹也好B+樹也好,根或者上面幾層因為被反覆query,所以這幾塊基本都在記憶體中,不會出現讀磁碟IO,一般已啟動的時候,就會主動換入記憶體。”

"mysql 底層儲存是用B+樹實現的,因為在記憶體中B+樹是沒有優勢的,但是一到磁碟,B+樹的威力就出來了"。

B+樹是B樹的變形,它把所有的附屬資料都放在葉子結點中,只將關鍵字和子女指標保存於內結點,內結點完全是索引的功能最大化了內結點的分支因子。不過是n個關鍵字對應著n個子女,子女中含有父輩的結點資訊,葉子結點包含所有資訊(內結點包含在葉子結點中,內結點沒有指向“附屬資料”的指標必須索引到葉子結點)。這樣的話還有一個好處就是對於每個結點所需的索引次數都是相等的,保證了穩定性

【B*樹】

       B*樹是B+樹的變體,在B+樹非根和非葉子結點再增加指向兄弟的指標B*樹定義了非葉子結點關鍵字個數至少為(2/3)*M,即塊的最低使用率為2/3(代替B+樹的1/2)。

B+樹的分裂:當一個結點滿時,分配一個新的結點,並將原結點中1/2的資料複製到新結點,最後在父結點中增加新結點的指標;B+樹的分裂隻影響原結點和父結點,而不會影響兄弟結點,所以它不需要指向兄弟的指標; B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那麼將一部分資料移到兄弟結點中,再在原結點插入關鍵字,最後修改父結點中兄弟結點的關鍵字(因為兄弟結點的關鍵字範圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各複製1/3的資料到新結點,最後在父結點增加新結點的指標; 所以,B*樹分配新結點的概率比B+樹要低,空間使用率更高;

在資料庫中的應用及效能分析

一般關係型資料庫使用B+樹來做索引,NoSQL資料庫用雜湊來做索引。例如MySQL就普遍使用B+Tree實現其索引結構。
上文說過,紅黑樹等資料結構也可以用來實現索引,但是檔案系統及資料庫系統普遍採用B/B+Tree作為索引結構。
因為索引本身也很大,不可能全部儲存在記憶體中,因此索引往往以索引檔案的形式儲存的磁碟上。這樣的話,索引查詢過程中就要產生磁碟I/O消耗,相對於記憶體存取,I/O存取的消耗要高几個數量級,所以評價一個數據結構作為索引的優劣最重要的指標就是在查詢過程中磁碟I/O操作次數的漸進複雜度。


由於儲存介質的特性,磁碟本身存取就比主存慢很多,再加上機械運動耗費,磁碟的存取速度往往是主存的幾百分之一,因此為了提高效率,要儘量減少磁碟I/O。為了達到這個目的,磁碟往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個位元組,磁碟也會從這個位置開始,順序向後讀取一定長度的資料放入記憶體。這樣做的理論依據是電腦科學中著名的區域性性原理:
當一個數據被用到時,其附近的資料也通常會馬上被使用。程式執行期間所需要的資料通常比較集中

由於磁碟順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有區域性性的程式來說,預讀可以提高I/O效率。
預讀的長度一般為頁(page)的整倍數。頁是計算機管理儲存器的邏輯塊,硬體及作業系統往往將主存和磁碟儲存區分割為連續的大小相等的塊,每個儲存塊稱為一頁(在許多作業系統中,頁得大小通常為4k),主存和磁碟以頁為單位交換資料。當程式要讀取的資料不在主存中時,會觸發一個缺頁異常,此時系統會向磁碟發出讀盤訊號,磁碟會找到資料的起始位置並向後連續讀取一頁或幾頁載入記憶體中,然後異常返回,程式繼續執行。

【下面分析B/B+Tree索引的效能】
我們使用磁碟I/O次數評價索引結構的優劣。先從B Tree分析,根據B Tree的定義,可知檢索一次最多需要訪問h個節點。資料庫系統的設計者巧妙利用了磁碟預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,在實際實現中B-Tree在每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也儲存在一個頁裡,加之計算機儲存分配都是按頁對齊的,就實現了一個node只需一次I/O。
B-Tree中一次檢索最多需要h-1次I/O(根節點常駐記憶體),漸進複雜度為O(h)=O(logdN)。一般實際應用中,出度d是非常大的數字,通常超過100,因此h非常小(通常不超過3)。
綜上所述,用B-Tree作為索引結構效率是非常高的。
而紅黑樹這種結構,h明顯要深的多。由於邏輯上很近的節點(父子)物理上可能很遠,無法利用區域性性,所以紅黑樹的I/O漸進複雜度也為O(h),效率明顯比B-Tree差很多。
B+Tree更適合外存索引,原因和內節點出度d有關。從上面分析可以看到,d越大索引的效能越好,而出度的上限取決於節點內key和data的大小,由於B+Tree內節點去掉了data域,因此可以擁有更大的出度,擁有更好的效能。

我應該使用符號表的哪種實現

對於典型的應用程式,應該在散列表和二叉查詢樹之間進行選擇。

相對於二叉查詢樹,散列表的優點在於程式碼更簡單,且查詢時間最優(常數級別)。二叉查詢樹相對於散列表的優點在於抽象結構更簡單(不需要設計雜湊函式),紅黑樹可以保證最壞情況下的效能且它能夠支援的操作更多(如排名、選擇和範圍查詢)。

大多數程式設計師的第一選擇都是散列表,在其他因素更重要時才會選擇紅黑樹。(”第一選擇”的例外:當鍵都是長字串時,我們可以構造出比紅黑樹更靈活而又比散列表更高效的資料結構 Trie樹)

=================================================字串的查詢============================================

單詞查詢樹(Trie樹)

單詞查詢樹的英文單詞trie來自於E.Fredkin在1960年玩的一個文字遊戲,因為這個資料結構的作用是取出(retrieval)資料,但發音為try是為了避免與tree相混淆。

基本性質

每個結點都含有R條連結,其中R為字母表的大小。(單詞查詢樹一般都含有大量的空連結,因此在繪製一顆單詞查詢樹時一般會忽略空連結。)

樹中的每個結點中不是包含一個或幾個關鍵字,而是隻含有組成關鍵字的符號。例如,若關鍵字是數值,則結點中只包含一個數位;若關鍵字是單詞,則結點中只包含一個字母字元。我們將每個鍵所關聯的值儲存在該鍵的最後一個字母所對應的結點中。

(這種樹會給某種型別關鍵字的表的查詢帶來方便。)

假設有如下關鍵字的集合

{ CAI、CAO、LI、LAN、CHA、CHANG、WEN、CHAO、YUN、YANG、LONG、WANG、ZHAO、LIU、WU、CHEN }


若以樹的多重連結串列來表示Trie樹,則樹的每個結點中應含有d個指標域。

若從Trie樹中某個結點到葉子結點的路徑上每個結點都只有一個孩子,則可將該路徑上所有結點壓縮成一個“葉子結點”,且在該葉子結點中儲存關鍵字及指向記錄的指標等資訊。


在Trie樹中有兩種結點:

分支結點:含有d個指標域和一個指示該結點中非空指標域的個數的整數域。(分支結點所表示的字元是由其指向子樹指標的索引位置決定的)

葉子結點:含有關鍵字域和指向記錄的指標域。

typedef structTrieNode

{

    NodeKind kind ;

    union {

        struct {KeyType K;  Record *infoptr} lf ;   //葉子結點

        struct {TrieNode *ptr[27];  int num} bh ; //分支結點

    } ;

} TrieNode,*TrieTree ;

查詢

在Trie樹上進行查詢的過程為:從根結點出發,沿給定值相應的指標逐層向下,直至葉子結點,若葉子結點中的關鍵字和給定值相等,則查詢成功。若分支結點中和給定值相應的指標為空,或葉結點中的關鍵字和給定值不相等,則查詢不成功。

分割

查詢操作的時間依賴於樹的深度。

我們可以對關鍵字集選擇一種合適的分割,以縮減Trie樹的深度。

例如:先按首字元不同分成多個子集之後,然後按最後一個字元不同分割每個子集,再按第二個字元……,前後交叉分割。

如下圖:在該樹上,除兩個葉子結點在第四層上外,其餘葉子結點均在第三層上。

若分割的合適,則可使每個葉子結點中只含有少數幾個同義詞。


插入和刪除

在Trie樹上易於進行插入和刪除,只是需要相應地增加和刪除一些分支結點。

把沿途分支結點中相應的指標域置空,再把其分支結點中的num-1,然後刪除葉子結點。當分支結點中num域的值減為1時,便可刪除。

【應用舉例】

尋找熱門查詢,300萬個查詢字串中統計最熱門的10個查詢。