查找

散列表


散列表是普通數組概念的推廣。由於對普通數組可以直接尋址,使得能在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時,便可刪除。



以上轉載自:http://blog.csdn.net/yang_yulei/article/details/26104921

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章