1. 程式人生 > 程式設計 >搞定技術面試:字典和雜湊(雜湊表)

搞定技術面試:字典和雜湊(雜湊表)

字典和雜湊是兩種很重要的資料結構,C++裡面的unordered_map使用雜湊表做底層,但是要問這兩種資料結構底層是什麼樣的,可能很多人回答不上來。

1.字典

字典是一種關聯的資料結構,不僅能夠儲存資料,又能高效的查詢資料。字典是一種集合,這種集合中的每個元素由兩部分組成,分別為關鍵碼(也成鍵)和屬相(也稱值)組成,及字典是一種<關鍵嗎,屬性>對的集合。map是STL中用來實現關聯陣列的容器型別。

字典表項的存放位置及其關鍵碼之間的對應關係可以用關鍵碼及表項位置指標這樣一個二元組來表示。這個二元組構成了搜尋某一指定表項的索引項。

實現字典一般要藉助各種樹形搜尋結構,從而實現效率上的提升。例如紅黑樹、AVL樹、字典樹。

1. 1字典的基本操作

  • 插入具有給定關鍵碼及對應屬性的元素。
  • 在字典中尋找具有指定關鍵碼的元素
  • 從字典中刪除具有給定關鍵碼的元素

1.2搜尋運算

靜態搜尋:搜尋結構在插入與刪除等操作後不發生改變。這種方式方法簡單,但演演算法效率不高,而且容易發生溢位。例如,排隊檢票

動態搜尋:搜尋結構在執行插入刪除等操作時可自動調整自身狀態,以保證每次搜尋運算都有較高的效率。這種方式可大大減少搜尋時間、提高演演算法執行效率,而且不容易發生溢位。例如,圖書館找書。

2.雜湊(雜湊表)

2.1什麼是雜湊

雜湊在表項的儲存位置和關鍵碼之間建立一個確定的對應函式關係(Hash(),雜湊函式),以使每個關鍵碼與結構中的唯一儲存位置相對應Address = Hash( Record.key)

可見在儲存表項時,通過函式計算儲存位置,並按此位置存放,這種方法稱為雜湊方法,用雜湊方法構建的資料結構稱為散列表,散列表是一種支援資料快速插入、刪除及查詢等操作的資料結構。散列表中儲存的元素依然是二元組,該二元組由關鍵碼和屬性組成。散列表的特別之處是表中元素的排列順序。散列表中每個元素的儲存位置都是由一個叫雜湊函式的公式決定,也就是雜湊方法中使用的函式就是所謂的雜湊函式。

關鍵碼集合都要比散列表地址集合大得多,所以有可能經過雜湊函式的計算把不同的關鍵碼對映到同一個雜湊地址上。好的雜湊函式應當使元素在散列表中呈現均勻儲存,而不是堆疊在某個特定的區域。

雜湊函式需要研究的一個主要問題是,對於給定的一個關鍵碼集合,選擇一個計算簡單且地址分佈均勻的雜湊函式,避免或儘量減少衝突。

當然由於關鍵碼集合比散列表地址集合大,因此衝突在所難免,於是雜湊函式需要研究的另一個主要問題是擬定解決衝突的方案。

散列表可以用平均常數時間實現插入和查詢操作。

2.2雜湊函式

  • 直接定值法
    Hash(key) = a * key + ba、b為常數
    優點:實現方式簡單,演演算法時間複雜度小,而且不會產生衝突。
    缺點:要求雜湊地址空間的大小與關鍵碼集合的大小一致,一般很難實現,而且也沒有意義啊。

  • 除留餘數法
    Hash(key) = key % k,key<=m,k <= m 且k為質數
    優點:有效縮減雜湊地址空間的大小。
    缺點:極易發生衝突

  • 平方取中法
    A.利用一定的編碼規則,將關鍵碼轉換成識別符號;
    B.求出識別符號的內碼錶示,計算內碼的平方值;
    C.取中間x位作為元素最終的雜湊地址 ;
    優點:有較強隨機性

  • 乘餘取整法

  • 摺疊法

2.3字串雜湊

  • KDR雜湊
//並非最好的雜湊函式,但是比較有效的雜湊函式
unsigned long BKDRHash(const string& str)
{
    unsigned long seed = 31;
    unsigned long hashval = 0;
    for (int i = 0; i < str.size(); i++) {
        hashval = hashval * seed + str[i]; //無符號算數運算
    }
    return hashval % HASHSIZE;
}
複製程式碼
  • RS雜湊
//利用很小的額外開銷提供了隨機的雜湊結果,對字串關鍵字的雜湊有明顯的改進
unsigned long RSHash(const string& str)
{
    unsigned long a = 31415,b = 27183;
    unsigned long hashval = 0;
    for (int i = 0; i < str.size(); i++) {
        hashval = (hashval * a + str[i]) % HASHSIZE;
        a = a * b % (HASHSIZE - 1); //採用偽隨機係數代替固定基數,並且在演演算法執行過程中不斷產生這些係數
    }
}
複製程式碼
  • FNV雜湊

2.4衝突解決

2.4.1開放定址法

當衝突發生時,使用某種探查技術在散列表中形成一個探查序列。沿此序列逐個單元查詢,直到找到給定的關鍵字,或者碰到一個開放的地址(該地址單元是空)為止。

a)線性探查法
將散列表T[0,…,m-1]看成一個迴圈向量,若初始探查地址為dhash(key) = d,則最長探查序列為d,d+1,d+2,…,m -1,0,1,…,d-1

b)二次探查法

c)雙重雜湊法 開放定址法中最好的方法之一。使用兩個雜湊函式Hash()ReHash()。使用該方法,第一個雜湊函式Hash()按表項的關鍵碼key計算表項所在的位置。一旦發生衝突,則利用第2個雜湊函式ReHash()計算表項的下一個位置的移位量。定義ReHash()的方法很多,但無論採用什麼方法定義,都必須使ReHash()的值和m互為素數。

2.4.2 開雜湊法(拉鍊法)

將所有關鍵字為同義詞的節點連結在同一個單連結串列中。 優點:

  • a)拉鍊法直接明瞭,且無堆積現象,解決了非同義詞之間的衝突問題,平均查詢長度及搜尋複雜度有所降低

  • b)拉鍊法在各連結串列上的結點空間是動態申請的,具有更好的擴充套件性。

  • c)在拉鍊法構造的散列表中,刪除節點的操作易於實現。只要刪除連結串列上相應的節點即可。而對開放定址法來講,刪除節點不能簡單地將被刪除的位置為空,否則將截斷在它之後填入散列表的同義詞節點的查詢路徑。這是因為在開放定址法中,空地址單元都是查詢失敗的條件。因此在用開放定址法處理衝突的散列表上執行刪除操作,只能在被刪除節點上做刪除標記,而不能真正刪除節點(重新整理也可以吧???)

  • d)儘管拉鍊法相對於開放定址法有很多優點,但是當節點規模較小是選擇線性探查法還是一個比較明智的選擇,因為在這種情況下使用拉鍊法會浪費較多空間。