七大查找常見算法(下)

一、線性索引查找
1.1 簡介
  前面講的幾種比較高效的查找方法是基於有序的基礎之上的(詳見七大查找常見算法(上)),而事實上,數據集可能增長非常快,例如,某些微博網站或大型論壇的帖子和回覆總數每天都是成百萬上千萬條,或者一些服務器的日誌信息記錄也可能是海量數據,要保證記錄全部是按照當中的某個關鍵字有序,其時間代價是非常高昂的,所以這種數據都是按先後順序存儲的。
  對於這樣的查找表,我們如何能夠快速查找到需要的數據呢?常常使用的方法就是—-索引。索引是爲了加快查找速度而設計的一種數據結構。它是把一個關鍵字與它對應的記錄相關聯的過程,一個索引由若干個索引項構成,每個索引項至少應包含關鍵字和其對應的記錄在存儲器中的位置等信息。
  索引按照結構可以分爲線性索引、樹形索引和多級索引。我們這裏就只介紹線性索引技術。所謂線性索引就是將索引項集合組織爲線性結構,也稱爲索引表。我們重點介紹三個線性索引:稠密索引、分塊索引、和倒排索引。
1.2 稠密索引
  稠密索引如下圖所示,它是指在線性索引中,將數據集中的每個記錄對應一個索引項。

這裏寫圖片描述

  上圖中,左邊的圖像爲索引序列,它是是按照關鍵碼有序排列的。索引項有序也就意味着,我們要查找關鍵字時,可以用到折半、插值、斐波那契等有序查找算法,大大提高效率。比如查找上表中的18。如果不用索引表,需要6次。而用左側的索引表,折半兩次就可以找到18對應的指針。
  這顯然是稠密索引優點,但是如果數據集非常大,比如上億,那也就意味着索引也得同樣的數據集長度規模,對於內存有限的計算機來說,可能就需要反覆去訪問磁盤,查找性能反而大大下降了。
1.3 分塊索引
  稠密索引因爲索引項與數據集的記錄個數相同,所以空間代價很大。爲了減少索引的個數,我們可以對數據集進行分塊,使其分塊有序,然後再對每一塊建立一個索引項,從而減少索引項的個數。
  分塊有序,是把數據集的記錄分成若干塊,並且這些塊需要滿足兩個條件:
  (1)塊內無序,即每一塊內的記錄不要求有序。當然,你如果能夠讓塊內有序對查找來說更理想,不過這就要付出大量時間和空間代價,因此通常我們不要求塊內有序
  (2)塊間有序,例如要求第二塊所有記錄的關鍵字均要大於第一塊中所有記錄的關鍵字,第三塊的所有記錄的關鍵字均要大於第二塊的所有記錄關鍵字….因爲只有塊間有序,纔有可能在查找時帶來效率。
  對於分塊有序的數據集,將每塊對應一個索引項,這種索引方法叫做分塊索引。我們定義的分塊索引項由三個數據項組成,如下圖所示:

這裏寫圖片描述

  這三個數據項分別爲最大關鍵碼(它存儲每一塊中的最大關鍵字,這樣的好處就是可以使得在它之後的下一塊中最小關鍵字也能比這一塊最大的關鍵字要大)、存儲了塊中的記錄個數(以便於循環時使用)和指向塊首數據元素的指針(便於開始對這一塊中的記錄進行遍歷)。
  由上面的分析我們可以大概明白分塊索引的步驟:
  (1)在分塊索引表中查找要查關鍵字所在的塊。由於分塊索引表是塊間有序的,因此很容易利用折半、插值等算法得到結果。
  (2) 根據塊首指針找到相應的塊,並在塊中順序查找關鍵碼。因爲塊中可以是無序的,因此只能順序查獲。
1.4 倒排索引
  不知道你對搜索引擎好奇過沒,無論你查找什麼樣的信息,它都可以在極短的時間內給你一些結果,是什麼算法技術達到這樣的高效查找呢?這裏介紹一種最基礎的搜索技術—-倒排索引。
  我們來看一個例子,假設有以下兩篇文章:
  (1) Books and friends should be few but good .
  (2) A good book is a good friend.
  假設我們忽略掉如“books”,“friends”中的複數”s”以及如“A”這樣的大小寫差異。我們可以整理出這樣一張單詞表,如下圖所示,並將單詞做了排序,也就是表格顯示了每個不同的單詞分別出現在哪篇文章中,比如“good”它在兩篇文章中都有出現,而is只有在文章2中才有。

這裏寫圖片描述

  在這裏這張單詞表就是索引表,索引項的通用結構是次關鍵碼和記錄號表。 其中記錄號表存儲具有相同次字關鍵字的所有記錄的記錄號(可以指向記錄的指針或者是該記錄的主關鍵字)。因爲這種查找方法是通過屬性值來確定記錄的位置,而不是通過記錄來確定屬性值,所以我們稱其爲倒排索引。

二、樹表查找
2.1 二叉樹查找算法(最簡單的樹表查找算法)
  如果要查找的數據集是有序線性表,並且是順序存儲的,查找可以用折半、插值、斐波那契等查找算法來實現,可惜的是,因爲有序,在插入和刪除操作上就需要耗費大量的時間。有沒有一種既可以使得插入和刪除效率不錯,又可以比較高效的實現查找的算法?這是有的,二叉樹查找算法就可以實現這樣的功能。
  它的基本思想爲:二叉查找樹是先對待查找的數據生成其對應的樹,其中樹的左分支的值小於右分支的值,然後在所查數據和每個節點的父節點比較大小,查找最適合的範圍。 這個算法的查找效率很高,但是如果使用這種查找方法要首先創建樹。
  它的性質爲:(1)若任意節點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;(2)若任意節點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;(3)任意節點的左、右子樹也分別爲二叉查找樹。同時,對二叉查找樹進行中序遍歷,即可得到有序的數列。
  不同形態的二叉查找樹如下圖所示:

這裏寫圖片描述

  對它的時間複雜度進行分析:它和二分查找一樣,插入和查找的時間複雜度均爲O(logn),但是在最壞的情況下仍然會有O(n)的時間複雜度。原因在於插入和刪除元素的時候,樹沒有保持平衡(比如,我們查找上圖(b)中的“93”,我們需要進行n次查找操作)。所以可以發現二叉查找樹對於大多數情況下的查找和插入在效率上來說是沒有問題的,但是它在最差的情況下效率比較低。而我們追求的是在最壞的情況下仍然有較好的時間複雜度,所以普通的二叉樹查找算法還沒有達到目的,這也就是爲何設計平衡查找樹的原因。
2.2 平衡查找樹之2-3查找樹
  和二叉樹不一樣,2-3樹中每個節點保存1個或者兩個key值。對於普通的2節點(2-node),它保存1個key和左右兩個孩子(或沒有孩子)。對應3節點(3-node),保存兩個Key和三個孩子(或沒有孩子),2-3查找樹的定義和性質如下:
  (1)對於2節點,該節點保存一個key及對應value,以及兩個指向左右節點的節點,左節點也是一個2-3節點,所有的值都比key有效,有節點也是一個2-3節點,所有的值比key要大。
  (2)對於3節點,該節點保存兩個key及對應value,以及三個指向左中右的節點。左節點也是一個2-3節點,所有的值均比兩個key中的最小的key還要小;中間節點也是一個2-3節點,中間節點的key值在兩個跟節點key值之間;右節點也是一個2-3節點,節點的所有key值比兩個key中的最大的key還要大。
  (3)2-3樹中所有的葉子都在同一層次上。
  同樣,如果中序遍歷2-3查找樹,就可以得到排好序的序列。2-3查找樹可如下圖所示:

這裏寫圖片描述

複雜度分析:
  2-3樹的查找效率與樹的高度是息息相關的。
  (1)在最壞的情況下,也就是所有的節點都是2-node節點,查找效率爲lgN。
  (2)在最好的情況下,所有的節點都是3-node節點,查找效率爲log3N約等於0.631lgN。
2.3 平衡查找樹之紅黑樹
  2-3查找樹能保證在插入元素之後能保持樹的平衡狀態,最壞情況下即所有的子節點都是2-node,樹的高度爲lgn,從而保證了最壞情況下的時間複雜度。但是2-3樹實現起來比較複雜,於是就有了一種簡單實現2-3樹的數據結構,即紅黑樹(Red-Black Tree)。紅黑樹比一般的二叉查找樹具有更好的平衡,所以查找起來更快。
  基本思想:紅黑樹的思想就是對2-3查找樹進行編碼,尤其是對2-3查找樹中的3-nodes節點添加額外的信息。紅黑樹中將節點之間的鏈接分爲兩種不同類型,紅色鏈接,他用來鏈接兩個2-nodes節點來表示一個3-nodes節點。黑色鏈接用來鏈接普通的2-3節點。特別的,使用紅色鏈接的兩個2-nodes來表示一個3-nodes節點,並且向左傾斜,即一個2-node是另一個2-node的左子節點。這種做法的好處是查找的時候不用做任何修改,和普通的二叉查找樹相同。
  紅黑樹的定義:紅黑樹是一種具有紅色和黑色鏈接的平衡查找樹,同時滿足:(1)紅色節點向左傾斜;(2)一個節點不可能有兩個紅色鏈接;(3)整個樹完全黑色平衡,即從根節點到所以葉子結點的路徑上,黑色鏈接的個數都相同。
  下圖可以看到紅黑樹其實是2-3樹的另外一種表現形式,如果我們將紅色的連線水平繪製,那麼他鏈接的兩個2-node節點就是2-3樹中的一個3-node節點了。

這裏寫圖片描述

  紅黑樹的性質:整個樹完全黑色平衡,即從根節點到所以葉子結點的路徑上,黑色鏈接的個數都相同(2-3樹的第2)性質,從根節點到葉子節點的距離都相等)。
  複雜度分析:最壞的情況就是,紅黑樹中除了最左側路徑全部是由3-node節點組成,即紅黑相間的路徑長度是全黑路徑長度的2倍。紅黑樹的平均高度大約爲logn。紅黑樹是2-3查找樹的一種實現,它能保證最壞情況下仍然具有對數的時間複雜度。
2.4 B樹和B+樹
  平衡查找樹中的2-3樹以及其實現紅黑樹。2-3樹種,一個節點最多有2個key,而紅黑樹則使用染色的方式來標識這兩個key。
  B 樹可以看作是對2-3查找樹的一種擴展,即他允許每個節點有M-1個子節點。定義如下:
  (1)根節點至少有兩個子節點;
  (2)每個節點有M-1個key,並且以升序排列;
  (3)位於M-1和M key的子節點的值位於M-1 和M key對應的Value之間;
  (4)其它節點至少有M/2個子節點。
  下圖是一個M=4 階的B樹:

這裏寫圖片描述

  B+樹是對B樹的一種變形樹,它與B樹的差異在於:
  (1)有k個子結點的結點必然有k個關鍵碼;
  (2)非葉結點僅具有索引作用,跟記錄有關的信息均存放在葉結點中;
  (3)樹的所有葉結點構成一個有序鏈表,可以按照關鍵碼排序的次序遍歷全部記錄。
  如下圖,是一個B+樹:

這裏寫圖片描述

  B和B+樹的區別在於,B+樹的非葉子結點只包含導航信息,不包含實際的值,所有的葉子結點和相連的節點使用鏈表相連,便於區間查找和遍歷。所以B+樹特別適合帶有範圍的查找。它的插入和刪除跟B樹類似,只不過插入和刪除的元素都是在葉子節點上進行而已。
  B+ 樹的優點在於:
  (1)由於B+樹在內部節點上不包含數據信息,因此在內存頁中能夠存放更多的key。 數據存放的更加緊密,具有更好的空間局部性。因此訪問葉子節點上關聯的數據也具有更好的緩存命中率。
  (2)B+樹的葉子結點都是相鏈的,因此對整棵樹的遍歷只需要一次線性遍歷葉子結點即可。而且由於數據順序排列並且相連,所以便於區間查找和搜索。而B樹則需要進行每一層的遞歸遍歷。相鄰的元素可能在內存中不相鄰,所以緩存命中性沒有B+樹好。
  但是B樹也有優點,其優點在於,由於B樹的每一個節點都包含key和value,因此經常訪問的元素可能離根節點更近,因此訪問也更迅速。下面是B 樹和B+樹的區別圖:

這裏寫圖片描述

  B/B+樹常用於文件系統和數據庫系統中,它通過對每個節點存儲個數的擴展,使得對連續的數據能夠進行較快的定位和訪問,能夠有效減少查找時間,提高存儲的空間局部性從而減少IO操作。

三、哈希查找
3.1 簡介
  哈希查找也稱爲散列查找。O(1)的查找,即所謂的秒查。所謂的哈希其實就是在記錄的存儲位置和記錄的關鍵字之間建立一個確定的對應關係f,使得每個關鍵字key對應一個存儲位置f(key)。查找時,根據這個確定的對應關係找到給定值的映射f(key),若查找集合中存在這個記錄,則必定在f(key)的位置上。哈希技術既是一種存儲方法,也是一種查找方法。
3.2 哈希查找的操作步驟
  (1)用給定的哈希函數構造哈希表;
  (2)根據選擇的衝突處理方法解決地址衝突;
  (3)在哈希表的基礎上執行哈希查找。
3.3 哈希函數的構造方法
(1)直接定址法
  函數公式:f(key) = a * key + b(a,b爲常數)
  這種方法的優點是:簡單、均勻,不會產生衝突。但是需要事先知道關鍵字的分佈情況,適合查找表較小並且連續的情況。
(2)數字分析法
  也就是取出關鍵字中的若干位組成哈希地址。比如我們的11位手機號是“187****1234”,其中前三位是接入號,一般對應不同的電信公司。中間四位表示歸屬地。最後四位才表示真正的用戶號。
  如果現在要存儲某個部門的員工的手機號,使用手機號碼作爲關鍵字,那麼很有可能前面7位都是相同的,所以我們選擇後面的四位作爲哈希地址就不錯。
(3)平方取中法
  取關鍵字平方後的中間幾位作爲哈希地址。由於一個數的平方的中間幾位與這個數的每一位都有關,所以平方取中法產生衝突的機會相對較小。平方取中法所取的位數由表長決定。
  如:K=456,K^2=207936,如果哈希表的長度爲100,則可以取79(中間兩位)作爲哈希函數值。
(4)摺疊法
  摺疊法是將關鍵字從左到右分割成位數相等的幾個部分(最後一部分位數不夠可以短),然後將這幾部分疊加求和,並按哈希表表長,取後幾位作爲哈希地址。當關鍵字位數很多,而且關鍵字中每一位上數字分佈大致均勻時,可以使用摺疊法。
  如:我們的關鍵字是9876543210,哈希表表長三位,我們可以分爲四組:987 | 654 | 321 | 0,然後將他們疊加求和:987+654+321+0 = 1962,再取後三位就可以得到哈希地址爲962.
(5)除留餘數法
  選擇一個適當的正整數p(p<=表長),用關鍵字除以p,所得的餘數可以作爲哈希地址。即:H(key) = key % p(p<=表長),除留餘數法的關鍵是選取適當的p,一般選p爲小於或等於哈希表的長度(m)的某個素數。
  如:m = 8,p=7
    m = 16,p = 13
    m = 32,p = 31
(6)隨機數法
  函數公式:f(key) = random(key). 這裏的random是隨機函數,當關鍵字的長度不等時,採用這種方式比較合適。
  總之,哈希函數的規則就是:通過某種轉換關係,使關鍵字適度的分散到指定大小的順序結構中。越分散,查找的時間複雜度就越小,空間複雜度就越高。哈希查找明顯是一種以空間換時間的算法。但是在構建映射關係的時候往往存在的最多的問題就是衝突,即把不同的關鍵字分在了相同的位置上去。此時我們需要解決這個衝突問題。
3.4 解決哈希列表衝突的方法
(1)開放地址法(線性探測法)  
  如果兩個數據元素的哈希值相同,則在哈希表中爲後插入的數據元素另外選擇一個表項。當程序查找哈希表時,如果沒有在第一個對應的哈希表項中找到符合查找要求的數據元素,程序就會繼續往後查找,直到找到一個符合查找要求的數據元素,或者遇到一個空的表項。
(2)鏈地址法(拉鍊法)
  將哈希值相同的數據元素存放在一個鏈表中,在查找哈希表的過程中,當查找到這個鏈表時,必須採用線性查找方法。 
(3)公共溢出法
  它的基本思想是:將哈希表分爲基本表和溢出表兩部分,凡是和基本表發生衝突的元素,一律填入溢出表。在查找時,對給定值通過哈希函數計算出哈希地址後,先於基本表的相應位置進行對比,如果相等則查找成功;如果不相等,則到溢出表中去進行順序查找。
3.5 總結
  哈希表是一個在時間和空間上做出權衡的經典例子。如果沒有內存限制,那麼可以直接將鍵作爲數組的索引。那麼所有的查找時間複雜度爲O(1);如果沒有時間限制,那麼我們可以使用無序數組並進行順序查找,這樣只需要很少的內存。哈希表使用了適度的時間和空間來在這兩個極端之間找到了平衡。只需要調整哈希函數算法即可在時間和空間上做出取捨。
  複雜度分析:單純論查找複雜度,對於無衝突的Hash表而言,查找複雜度爲O(1)(注意,在查找之前我們需要構建相應的Hash表)。可以發現查找數據的效率非常高,但是我們實現快速的查找付出了什麼代價?
  Hash是一種典型以空間換時間的算法,比如原來一個長度爲100的數組,對其查找,只需要遍歷且匹配相應記錄即可,從空間複雜度上來看,假如數組存儲的是byte類型數據,那麼該數組佔用100byte空間。現在我們採用Hash算法,我們前面說的Hash必須有一個規則,約束鍵與存儲位置的關係,那麼就需要一個固定長度的hash表,此時,仍然是100byte的數組,假設我們需要的100byte用來記錄鍵與位置的關係,那麼總的空間爲200byte,而且用於記錄規則的表大小會根據規則,大小可能是不定的。
  各種查找算法的最壞和平均條件下各種操作的時間複雜度如下圖所示:

這裏寫圖片描述

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