1. 程式人生 > >《Java數據結構和算法》- 哈希表

《Java數據結構和算法》- 哈希表

技術分享 裏的 時間 i++ 三位數 小型 真隨機數 dem 例子

Q: 如何快速地存取員工的信息?

A: 假設現在要寫一個程序,存取一個公司的員工記錄,這個小公司大約有1000個員工,每個員工記錄需要1024個字節的存儲空間,因此整個數據庫的大小約為1MB。一般的計算機內存都可以滿足。
為了盡可能地存取每個員工的記錄,使用工號從1(公司創業者)到1000(最近雇傭的工人)。將工號作為關鍵字(事實上,用其他作為關鍵字完全沒有必要)。即使員工離職不在公司,他們的記錄也是要保存在數據庫中以供參考,在這種情況下需要使用什麽數據結構呢?

A: 一種可能使用數組,每個員工占數組裏的一個元素。數組下標是當前記錄的員工工號,數組的類型如下圖:
技術分享圖片
如果知道數組下標,要訪問特定的數組數據項非常方便。HR要查找Herman Alcazar,她知道Alcazar的工號是72,所以她鍵入這個數字,程序直接檢索數組下標為72 的元素,總共只需一條語句:

EmpRecord record = employees[72];

增加一個新記錄也非常快,只需要把它插入到最後一個元素的後面。下一個記錄將被放在第1001個元素,插入新記錄也只需要一條語句:

employees[total++] = newRecord;

備註: 數組大概需要比當前員工的總數要大一些,為擴展留出的空間,但不能指望可以大量擴展數組容量。

Q: 如何快速地查找字典裏的單詞?

A: 如果想要把一本英文字典的每個單詞,從a到zyzzyva,都寫入到內存,以便快速讀寫,那麽哈希表是一個不錯的選擇。

A: 例如,想在內存中存儲50000個英文單詞,起初可能考慮到每個單詞占據一個數組元素,那麽數組的大小是50000個,同時可以使用數組下標存取單詞,那麽數組下標和單詞有什麽關系呢?例如給出一個單詞morphosis,怎麽能找到它的數組下標呢?

A: ASCII碼從0到255,可以容納字母、標點等字符。假設我們這個字典不使用大寫字母,為了省下更多的內存,我們可以設計一種自己的編碼方案,其中a是1,b是2,c是3,以此類推,直到26代表z,還要把空格用0代表,所以總共有27個字符(這裏假設這個字典不使用大寫字母)

Q: 如何把代表單個字母的數字組合成代表整個單詞的數字呢?

A: 下面介紹兩種具有代表性的方法,以及它們的優點和缺點。

A: 方法一:把單詞裏每個字符的編碼求和。
例如把單詞cats轉化為數字,用前面創造的編碼方案轉換單詞如下:

c=3             
a=1          
t=20           
s=19

因此3+1+20+19=43,那麽在字典裏cats單詞存儲在下標為43的數組裏。所有的英文單詞都可以使用這個辦法來轉換成數組的下標。

但是這個方法有一個問題。假設我們這個字典的單詞最長有10個字母組成,那麽字典的第一個單詞a到zzzzzzzzzz(10個z的編碼之和為260) ,因此單詞的編碼範圍是從1到260,數組根本容納不了字典裏50000個單詞,也許可以考慮每個數組元素包含一個子數組或鏈表,但是這個辦法嚴重降低了存取速度。

因此,這個方法把單詞轉化為數字的嘗試還留下一些問題要解決,比如有太多的單詞需要用同一個數組下標,例如,was、tin、give、tend、moan、tick、bails、dredge和其他一百多個單詞用編碼求和都是43,和cats一樣。這個方法沒有把單詞分得足夠開,所以結果數組能表達的元素太少,需要擴展數組表達下標的空間。

A: 方法二: 冪的連乘
數字7564可以寫成10的冪的連乘,如下:

7 * 10 ^3 + 5 * 10 ^ 2 + 6 * 10 ^ 1 + 4 * 10 ^ 0

類似的方法,可以把單詞分解成字母組合,每個字母的編碼乘以27的冪,然後將結果相加。例如把單詞cats轉化為數字如下:

3 * 27 ^ 3 + 1 * 27 ^ 2 + 20 * 27 ^ 1 + 19 * 27 ^ 0 = 60337

這個過程可以為每個可能的單詞創建一個獨一無二的整數。但是如果對於一個較長的單詞會發生怎麽樣的事情呢?最長的10個字母的單詞zzzzzzzzzz,其結果轉化為:

26*27^9 + 26*27^8 + 26*27^7 + 26*27^6 + 26*27^5 + 26*27^4 + 26*27^3 + 26*27^2 + 26*27^1 + 26*27^0

僅27^9就超過了7,000,000,000,000,所以可以看到這個結果非常巨大,數組根本沒有這麽大的長度。這個問題的出現是因為這個方案為每個可能的單詞分配了一個數組元素,不管這個單詞是不是真正的英文單詞,因此數組元素就從aaaaaaaaaa, aaaaaaaaab, aaaaaaaaac, 一直到zzzzzzzzzz。如下圖顯示的情況,
技術分享圖片
A: 綜上,第一種方案產生的數組下標太少,第二種方案產生的數組下標又太多。

Q: 如何把巨大整數範圍壓縮到一個可接受的數組範圍?

A: 針對只有50000個單詞的字典,可能會假設這個數組大概就有這麽大的空間,但實際上需要多一倍的空間容納這些單詞,所以需要容量為100000長度的數組。

A: 現在就得找一種方法,把0到超過7,000,000,000,000的範圍,壓縮到從0到100000。有一種簡單的方法是取余。例如把從0到199的數字(用變量hugeNumber表示),壓縮為從0到9的數字,壓縮率為20:1。如下圖:
技術分享圖片

A: 回憶一下,長度為10個字符的單詞轉化成數字的方法:

hugeNumber = ch0*27^9 + ch1*27^8 + ch2*27^7 + ch3*27^6 + ch4*27^5 + ch5*27^4 + ch6*27^3 + ch7*27^2 + ch8*27^1 + ch9*27^0

然後取余,

arraySize = numberWords * 2;
arrayIndex = hugeNumber % arraySize;

其中arrayIndex = hugeNumber % arraySize就是一種哈希函數。它把一個大範圍的數字哈希為一個小範圍的數字,這個小範圍對應著數組的下標,使用哈希函數向數組插入數據後,這個數組就稱為哈希表。

A: 在這裏hugeNumber可能會超出變量的範圍,即使變量類型為long,後面會看到如何處理這個問題。

Q: 如何解決沖突?

A: 不可避免地把幾個不同的單詞哈希化到同一個數組元素中,假設要在數組中插入單詞melioration,通過哈希函數得到它的數組下標後,發現這個元素已經有一個單詞demystify了,這種情況就稱為沖突。如下圖,
技術分享圖片

A: 那麽如何解決沖突呢?

A: 方法一,開放地址法,當沖突發生時,通過系統的方法找到數組的一個空位(前面已經說過,指定數組的大小兩倍於需要存儲的數據量),把這個單詞填入。例如,如果cats哈希化的結果是5421,但它的位置已經被parsnip占用,那麽可能會考慮把cats放在5422的位置上。

A: 方法二,鏈地址法,創建一個存放單詞鏈表的數組,數組內不直接存儲單詞,這樣發生沖突時,新的數據項直接到這個數組下標所指的鏈表裏添加。

Q: 開放地址法有哪些具體的方法?

A: 開放地址法有三種方法:線性探測、二次探測和再哈希法。探測序列通常會用第三種方法。

Q: 什麽是線性探測(Linear Probing)?

A: 如果5421是要插入數據的位置,它已經被占用了,那麽就使用5422,然後5423,依次類推,數組下標一直遞增,直到找到空位,這就叫做線性探測。

A: 根據沖突的位置,查找算法沿著數組一個一個地察看每個元素,這個過程就叫探測,如果在找到要查找的關鍵字前遇到一個空位,說明查找失敗,不需要再做查找,下圖顯示了成功和不成功的線性探測。
技術分享圖片

Q: 什麽叫聚集(Clustering)?

A: 在哈希表中,一串連續的已填充元素叫做填充序列,增加越來越多的數據項時,填充序列變的越來越長,這叫做聚集。
技術分享圖片

Q: 如何設計一個好的哈希表?

A: 哈希表在幾乎被填滿的數組中添加數據項,效率會非常低。比如在60個長度已經容納了59個數據項的哈希表中,填入一個數據項都需要花費很長的時間。而且應該註意如果哈希表被完全填滿,算法應該停止工作。

當數據項數目占哈希表長的一半,或最多到三分之二時,哈希表的性能最好。

A: 因此設計哈希表的關鍵要確保它不會超過整個數組容量的一半,最多到三分之二。(下文會繼續討論哈希表的裝填數據的程度和探測長度的數學關系。)

Q: 線性探測哈希表的Java代碼?

A: 編程需實現查找、插入和刪除接口。

A: https://gitee.com/firewaycoding/ds_in_java/tree/acbc069ff96b4e20fb708b6c142be1103fcd52b1/chapter11/01HashTable

A: 本程序中並沒有考慮擴容數組的情況,作為演示只是想你了解哈希表內部發生沖突時的場景,應該考慮寫一個rehash()方法,只要填充因子大於0.5,insert()方法就會調用它,把整個哈希表復制到比它大兩倍的數組中。這個過程叫重新哈希化,這是一個耗時的過程,但如果數組要進行擴展,這個過程是必要的。

Q: 如何防止聚集的產生?

A: 前面已經看到在開放地址法的線性探測中會發生聚集,一旦聚集形成,它就會變得越來越大,那些哈希化後落在聚集範圍內的數據項,就得一步一步地移動,直到插在聚集的最後。這就像人群,當某個人在商場暈倒,人群就慢慢聚集,最初的人聚過來是因為看到了那個倒下的人,後面的人聚過來因為他們想知道每個人都在看什麽。人群聚得越大,吸引的人就會越多。

A: 聚集降低了哈希表的性能,二次探測就是防止聚集產生的一種嘗試。

Q: 什麽是裝填因子(Load Factor)?

A: 已填入哈希表的數據項數和哈希表容量的比值。有10000個元素的哈希表填入6667個數據後,它的裝填因子是2/3。

loadFactort = nItems / mArraySize;

當裝填因子不太大時,聚集分布得比較連貫。哈希表的某個部分可能包含大量的聚集,而另一個部分還很稀疏。

Q: 什麽是二次探測(Quadratic Probing)?

A: 二次探測的思路是探測相隔較遠的元素,而不是和原始位置相鄰的元素。
A: 步驟是步數的平方。在線性探測中,如果哈希函數計算的原始下標是x,線性探測就是x+1, x+2, x+3,依次類推。而在二次探測中,探測的過程是x+1,x+2^2,x+3^2,x+4^2,x+5^,依次類推。如下圖。

技術分享圖片

A: 當二次探測的搜索變長時,它變得越來越絕望。第一次它查找相鄰的元素,如果這個元素被占用,它認為這裏可能有一個小的聚集,所以它嘗試距離為4的步長,如果這裏也被占用,它變得有些焦慮,認為這裏有一個大的聚集,然後嘗試距離為9的步長,如果這裏還被占用,它感到一絲恐慌,調到距離為16的元素,很快,它會歇斯底裏地飛躍完整個數組空間。當哈希表幾乎填滿時,就會出現這種情況。

Q: 開放地址法為什麽建議數組的容量為質數?

A: 數組容量總是選一個質數,例如用59代替60。(小於60的質數有59,53,47,43,41,37,31,29,23,19,17,13,11,7,5,3和2)。如果數組容量不是質數,在探測過程中,步長序列就會變得無限長。

A: 在下文的“再哈希法”中,我們會看到,再哈希法要求哈希表的容量必須是一個質數。為什麽要有這個限制,假設表的容量不是質數。例如,假設表的大小是15,有一個特定的關鍵字映射到0,步長為5.探測序列就是0,5,10,0,5,10,依次類推,一直循環下去。算法只嘗試這三種元素,最終會導致程序崩潰。
如果數組容量是13,即一個質數,再哈希法探測序列最終會訪問所有元素,即0,5,10,2,7,12,4,9,1,6,11,3一直下去。只要表中有一個空位,就可以探測到。用質數作為數組容量是的任何數想整除它都是不可能的,因此探測序列最終會檢查到所有元素。

Q: 二次探測的問題?

A: 二次探測消除了線性探測中產生的聚集問題,這種聚集問題叫做主要聚集(primary clustering)。然而,二次探測產生了另外一種更細的聚集問題。是因為所有映射到同一個位置的關鍵字在尋找空位時,探測的元素是一樣的。

A: 比如,在大小長度為59的數組裏,將184,302,420和544依次插入到表中,它們都映射到7,那麽302將會以1為步長進行探測,420將會以4為步長進行探測,544則需要9為步長的探測。只要有一項,其關鍵字映射到7,就需要更長步長的探測,這個現象就叫做次要聚集(secondary clustering)。

A: 二次探測不會被經常,因為還有稍微好些的解決方案。

Q: 什麽是再哈希法?

A: 為了消除主要聚集和次要聚集,可以使用另外的一個方法:再哈希法。

A: 次要聚集產生的原因是產生的探測步長總是固定的:1,4,9,16,...。為此,為了使步長不是那麽固定,第二個哈希函數對關鍵字再做一遍哈希化。

A: 經驗說明,第二個哈希函數必須具備如下特點:
1) 和第一個哈希函數不同
2) 不能輸出0(否則每次探測都是原地踏步,算法將陷入死循環)

第二個哈希函數取下面的形式會比較好

step = constant - (key % constant);

其中constant是質數,且小於數組容量。
例如,step = 5 - (key % 5),用這個函數,步長範圍是從1到5。

Q: 再哈希法的Java代碼?

A:https://gitee.com/firewaycoding/ds_in_java/tree/acbc069ff96b4e20fb708b6c142be1103fcd52b1/chapter11/02HashDoubleTable

A: 測試用例的test2()顯示了當21個數據項插入到容量為23的哈希表的情況,處理沖突的方法使用再哈希法,步長從1到5。如下圖:
技術分享圖片
前面15個關鍵字大多數映射到空白元素(第10個是不規則的),這以後,隨著數組越來越滿,探測序列也越來越長。如下圖:
技術分享圖片

Q: 什麽是鏈地址法(Separate Chaining)?

A: 映射到同一個位置的數據項只需要加到鏈表中,而不需要在原始數組中尋找空位。如下圖,
技術分享圖片

Q: 鏈地址法的Java代碼?

A: HashChainTable程序主要難點是在於有序鏈表的插入或刪除操作。

A: testInsertCase1(),插入第1個元素的場景
技術分享圖片

A: testInsertCase2(),插入第2個元素,比第1個元素大
技術分享圖片

A: testInsertCase3(),插入第2個元素,比第1個元素小
技術分享圖片

A: testInsertCase4(),插入第3個元素,比第1個元素大,比第2個元素小
技術分享圖片

A: testInsertCase5(),插入重復元素。註意current.getKey() <= keycurrent.getKey() < key的區別。

A: 同理刪除

Q: 鏈地址法的裝填因子?

A: 鏈地址法中的裝填因子與開放地址法的不同,在鏈地址法中,可以比1大。

A: 當然如果鏈表中有許多項,存取時間會變長,因為存取特定數據項的平均需要搜索鏈表的一半的數據項。但總體上比開放地址法要好一些,因為開放地址法中,當裝填因子超過1/2或2/3之後,性能下降得很快,在鏈地址法中,裝填因子可以達到1以上,且對性能影響不大。

Q: 設計一個好的哈希函數的因素需要哪些?

A: 好的哈希函數的要求就只有一個,就是能快速地計算,如果哈希函數運行緩慢,速度就會降低。哈希函數中有許多乘法和除法是不可取的,Java/C/C++語言可以使用位操作,例如可以使用右移來代替除以2的倍數。

A: 哈希函數的目的是使得較大的關鍵字範圍壓縮成較小的數組下標的範圍,因此方法最好能夠使關鍵字隨機地分布在整個哈希表中。

A: 對於哈希函數index = key % arraySize; 如果關鍵字是一個真隨機數,得到的下標也是隨機數,就會得到一個良好的分布情況。然而,關鍵字通常不是隨機數。

A: 例如,居民身份證編碼就不是隨機數,根據《中華人民共和國國家標準GB11643-1999》中有關公民身份號碼的規定,公民身份號碼是特征組合碼,由十七位數字本體碼和一位數字校驗碼組成。排列順序從左至右依次為:六位數字地址碼,八位數字出生日期碼,三位數字順序碼和一位數字校驗碼。
例如,430181198506228889,各個數字的含義如下:
1) 0-1位:省份(43代表湖南)
2) 2-3位:地區級城市(01代表長沙)
3) 4-5位:縣級市(81代表瀏陽市)
4) 6-9位:出生年(1985年)
5) 10-11位:出生月(06月)
6) 12-13位:出生日(22日)
7) 14-16位:縣、區級政府所轄派出所的分配碼,其中單數為男性分配碼,雙數為女性分配碼。如遇同年同月同日有兩個人以上順延第二、第三、第四、第五個等分配碼,如007的就是個男生,而且和他同年月日的男生至少有001、003、005,那下一個同年月日的男生就是009
8) 17位:校驗碼,主要是為了校驗計算機輸入公民身份證前17位數字是否正確,其取值0~10,當值等於10時,用羅馬數字符X表示。其計算方法如下:
將前面的身份證號碼17位數分別乘以不同的系數。從第0位到第16位的系數分別為:7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2。然後將這些乘積相加,所得的結果除以11,看余數是多少。余數對應的最後一位身份證的號碼如下圖:
技術分享圖片

A: 壓縮關鍵字時,要把每個位都計算在內,但是校驗碼應該舍棄,因為它沒有提供任何有用的信息,在壓縮中是多余的。

A: 關鍵字提供的數據越多,哈希化後,越可能覆蓋整個下標範圍

Q: 哈希化字符串?

A: 前面我們已經介紹對於長度為4的字符串cats轉化為數字。下面是哈希化的代碼:

public static int hashFunc1(String key)
{
    int hash = 0;
    int pow27 = 1;  // 1, 27, 27*27, etc

    for(int i=key.length()-1; i>=0; i--) 
    {
        int letter = key.charAt(i) - 96;  // get char code
        hash += pow27 * letter;  // times power of 27
        pow27 *= 27;  // next power of 27
    }

    return hash % arraySize;
}

hashFunc1()方法不如想象的那麽有效,除了字符轉換外,在循環中有兩次相乘和一次相加。還有另一方法可以取代。
技術分享圖片
這個方法叫Horner方法(Horner是英國數學家,1773-1827)
從最內側的括號開始,逐漸向外運算,java代碼如下:

public static int hashFunc2(String key) {
    int hash = key.charAt(0) - 96;

    for (int i =1; i < key.length(); i++) {
        int letter = key.charAt(i) - 96;
        hash = hash * 27 + letter;
    } 

    return hash % arraySize;
}

不行的是,hashFunc2()方法不能處理大於7個字符的字符串,否則會超出int類型的範圍,可以使用下面的方法取代:

public static int hashFunc2(String key) {
    int hash = 0;

    for (int i =1; i < key.length(); i++) {
        int letter = key.charAt(i) - 96;
        hash = (hash * 27 + letter) % arraySize;
    } 

    return hash;
}

這種方法或類似的方法通常用來哈希化字符串,也可以應用不同的位操作技巧,例如使用32或者更大的2的冪作為模取代32,這樣乘法可以結合右移增加效率,右移比取模更快。

Q: 哈希化的效率?

A: 哈希表一次單獨的查找和插入時間與探測的長度成正比,還要加上哈希函數的常量時間。因此探測長度取決於裝填因子,隨著裝填因子變大,探測長度也越來越長。

A: 下面會看到不同的哈希表中,探測長度和裝填因子之間的關系。

A: 開放地址法
線性探測
隨著裝填因子變大,效率下降,比鏈地址法更嚴重。
不成功查找比成功查找花費更多的時間,在探測序列中,只要找到要目標位置,算法就能停止,平均起來,就會發生探測序列的一半,另一方面,要確定不能找到這樣的目標位置,就必須走過整個探測序列。
下面的等式顯示了線性探測時,探測序列(P)與裝填因子(L)的關系。這個公式來自Knuth,這些推導出來相當復雜。
對成功的查找來說
技術分享圖片
而對於不成功的查找來說
技術分享圖片
下圖顯示了用坐標表示這個等式的結果。
技術分享圖片
當裝填因子為0.5時,成功的搜索需要1.5次比較,不成功的搜索需要2.5次。
當裝填因子為2/3時,分別需要2.0次和5.0次比較。如果裝填因子更大,比較次數會變得非常大。
正如我們看到,應該使裝填因子保持在2/3以下,最好在1/2以下。另一方面,裝填因子越低,對於給定數量的數據項,就需要更多的空間。實際情況中,最好的裝填因子取決於存儲效率和速度的平衡。隨著裝填因子變小,存儲效率下降,而速度上升。

二次探索和再哈希法
二次探測和再哈希法的性能相當,比線性探測略好。
對於成功的搜索,公式如下(公式仍然來自Knuth):
-log2(1 - loadFactor) / loadFactor
對於不成功的查找,公式如下:
1 / (1 - loadFactor)
當裝填因子為0.5時,成功和不成功的查找都平均需要兩次比較,當裝填因子為2/3時,分別需要2.37和3.0次比較,當裝填因子為0.8時,分別需要2.9和5.0次。
技術分享圖片

A: 鏈地址法
假設哈希表包含mHashArraySize個數據項,每個數據項有一個鏈表,哈希表中有N個數據項。那麽平均起來每個鏈表的數據項如下:
sortedListSize = N / mHashArraySize
這和裝填因子的定義是相同的:
loadFactor = N / mHashArraySize
所以平均表長等於裝填因子

查找

對於成功查找,平均起來找到正確項之前,要檢查一半的數據項,因此查找時間
1 + loadFactor / 2
不管鏈表是否有序,都遵循這個公式
對於不成功查找,如果鏈表不是有序的,要檢查所有的數據項,所以時間是
1 + loadFactor
在鏈地址法中,通常裝填因子為1(數據項的個數和數組容量相同)。較小的裝填因子不能顯著地提升性能。但是如果所有操作的時間都會隨著裝填因子的變大而增大,所以不宜把裝填因子提升到2。
技術分享圖片

插入

如果鏈表是無序的,插入操作是立即完成的,這裏不需要比較。所以插入的時間為1;
如果鏈表是有序的,那麽,由於存在不成功的查找,平均要檢查一半的數據項,所以插入的時間是
1 + loadFactor / 2

Q: 開放地址法和鏈地址法的比較?

A: 對於小型的哈希表,如果使用開放地址法,再哈希法似乎比二次探測的效果好。但是前提是內存一定要充足,並且哈希表一經創建,就不在改變其容量,在這種情況下,線性探測相對容易實現,並且如果裝填因子低於0.5,幾乎沒有什麽性能下降。

如果在哈希表創建時,要填入的項數未知,鏈地址法要好過開放地址法。

當兩者都可選時,使用鏈地址法。它需要使用鏈表類,但回報是增加比預期更多的數據時,不會導致性能快速下降。

Q: 外部存儲使用哈希表的場景?

A: 前面我們已經介紹了外部存儲關於索引的內容,索引可以使用哈希表來存儲的。文件索引是由關鍵字-塊號碼組成的列表,它按關鍵字排序,這個索引也可以叫做文件指針表。

這裏還是使用“外部存儲”電話本的例子,塊大小為8192字節,一個記錄為512字節,因此可以一個塊可以容納16個記錄。哈希表的每個元素指向了某個塊。假設這個電話本有100個塊。第一個塊為0,一直到99。
我們把電話本的名字作為關鍵字哈希化到哈希表的下標,在外部哈希化中,重要的是塊不要填滿,因此每個塊平均存儲8個記錄。有的快可能多些,有的少些。
在本例中中大概有800個記錄,排列如下圖,

技術分享圖片
所有關鍵字映射為同一個值的記錄都定位到相同塊。
為找到特定關鍵字的記錄,搜索算法哈希化關鍵字,用哈希值作為哈希表的下標,得到某個下標中的塊號,然後讀取這個塊。
為了實現這個方案,必須仔細選擇哈希函數和哈希表的大小,為的是限制映射到相同的值關鍵字的數量。在本例中,平均每個值只對應8個記錄。

即使用一個好的哈希函數,塊偶爾也會填滿,這時,可以使用在內部哈希表中討論的處理沖突的不同方法:開放地址法和鏈地址法。
在開放地址法中,插入時,如果發現一個塊是滿的,算法在相鄰的塊插入新記錄。在線性探測中,這時下一個塊,但也可以使用二次探測或再哈希法選擇。在鏈地址法中,有一個溢出塊,當發現塊已滿時,新記錄插在溢出塊中。

Q: 本篇總結?

  • 哈希表基於數組
  • 關鍵字值的範圍通常比數組容量大
  • 關鍵字值通過哈希函數映射為數組的下標
  • 英文字典是一個數據庫的典型案例,它可以有效的用哈希表來處理
  • 一個關鍵字哈希化到已占用的數組單元,這種情況叫做沖突
  • 沖突可以用兩種方法解決:開放地址法和鏈地址法。
  • 在開放地址法中,把沖突的數據項放在數組的其他位置。
  • 在鏈地址法中,每個數組元素包含一個鏈表,把所有映射到同一個數組下標的數據項都插入這個鏈表中。
  • 討論了三種開放地址法:線性探測、二次探測和再哈希化。
  • 在線性探測中,步長總是1,所以如果X是哈希函數計算得出的數組下標,那麽探測序列就是X, X+1, X+2, X+3, 以此類推。
  • 找到一個特定項需要經過的步數叫做探測長度。
  • 在線性探測中,已填充單元的長度不斷增加。它們叫做主要聚集(primary clustering),這會降低哈希表的性能。
    在二次探測中,X的位移是步數的平方,所以探測序列就是X, X+1, X+4, X+9, X+16一次類推
  • 二次探測消除了主要聚焦,但是產生了次要聚集,它比主要聚集的危害略小
  • 二次聚集的發生時因為映射到同一個單元的關鍵字,在探測過程中執行了相同的序列 發生上述情況是因為步長只依賴於哈希值,與關鍵字無關
  • 在再哈希法中,步長依賴於關鍵字,且從第二個哈希函數中得到
  • 裝填因子是表中數據項數和數組容量的比值
  • 在開放地址法中,當裝填因子接近1時,查找時間趨於無限
  • 在開放地址法中,關鍵是哈希表不能填得太滿
  • 對於鏈地址法,裝填因子為1比較合適
  • 字符串可以這樣哈希化,每個字符乘以常數的不同次冪,求和,然後用取模操作符(%)縮減結果,以適應哈希表的容量
  • 哈希表的容量通常是一個質數,這在二次探測和再哈希法中非常重要
  • 哈希表可用於外部存儲,一種做法是用哈希表的單元存儲磁盤文件的塊號碼

參考

    1. 《Java數據結構和算法》第11章 - 哈希表

《Java數據結構和算法》- 哈希表