雜湊表的一點思考

九尾猫313發表於2024-12-03

前言

最近在讀Java核心技術 卷1,和大家分享一下集合篇有關雜湊表的感悟。

正文節選

雜湊表為每個物件計算一個整數,稱為雜湊碼(hashcode)。雜湊碼是由物件的例項域產生的一個整數。更準確地說,具有不同資料域的物件將產生不同的雜湊碼。

在 Java 中,雜湊表用連結串列陣列實現。每個列表被稱為桶(bucket) (參看圖 9-10)。要想査找表中物件的位置,就要先計算它的雜湊碼,然後與桶的總數取餘,所得到的結果就是儲存這個元素的桶的索引。
例如,如果某個物件的雜湊碼為76268, 並且有128個桶,物件應該儲存在第108號桶中(76268除以128餘108)。或許會很幸運,在這個桶中沒有其他元素,此時將元素直接插人到桶中就可以了。

當然,有時候會遇到桶被佔滿的情況,這也是不可避免的。這種現象被稱為雜湊衝突(hash collision)。這時,需要用新物件與桶中的所有物件進行比較,査看這個物件是否已經存在。如果雜湊碼是合理且隨機分佈的,桶的數目也足夠大,需要比較的次數就會很少。

如果想更多地控制雜湊表的執行效能,就要指定一個初始的桶數。桶數是指用於收集具有相同雜湊值的桶的數目。如果要插入到雜湊表中的元素太多,就會增加衝突的可能性,降低執行效能。
如果大致知道最終會有多少個元素要插人到雜湊表中,就可以設定桶數。通常,將桶數設定為預計元素個數的75% ~ 150%。有些研究人員認為:儘管還沒有確鑿的證據,但最好將桶數設定為一個素數,以防鍵的集聚。
標準類庫使用的桶數是2的冪,預設值為16(為表大小提供的任何值都將被自動地轉換為2的下一個冪)。當然,並不是總能夠知道需要儲存多少個元素的, 也有可能最初的估計過低。如果雜湊表太滿,就需要再雜湊(rehashed)。如果要對雜湊表再雜湊, 就需要建立一個桶數更多的表,並將所有元素插入到這個新表中,然後丟棄原來的表。
裝填因子(load factor) 決定何時對雜湊表進行再雜湊。例如,如果裝填因子為0.75 (預設值)而表中超過75%的位置已經填人元素, 這個表就會用雙倍的桶數自動地進行再雜湊。對於大多數應用程式來說, 裝填因子為0.75 是比較合理的。

思考

原文中提到了幾個關鍵詞

效能,桶數,裝填因子

還提到了一個前提條件,也就是我們能大約估計出雜湊表的大小,或者不妨更大膽一點,這個雜湊表就是固定的,那我們就可以從桶數和裝填因子下手,對效能進行最佳化,防止觸發不必要的擴容機制。

舉個例子,我們現在有10個桶要插入雜湊表裡,我們應該如何選擇桶數和裝填因子?

// 一個存放10個水果的列表,現在要以中英文名放入雜湊表裡
List<String> fruits = Arrays.asList("蘋果", "橘子", "梨子", "葡萄", "香蕉", "西瓜", "檸檬", "橙子", "芭樂", "藍莓");

是選擇預設的大小嗎?

HashMap<String, String> fruitsMap = new HashMap<>();

讓我們計算一下桶數*裝填因子 = 16 * 0.75 = 12 > 10。
剛好滿足要求,不會觸發擴容!
但是我們的前提是固定的桶數!請注意上文程式碼的Arrays.asList(),建立的是一個不可變的列表,那麼我們多出的16 - 10 = 6個空間不就浪費了嗎?

現在讓我們來重新考慮一下這個設計,我們能不能把裝填因子設定為1呢?也就是當桶滿了才觸發擴容,正因為我們的桶數不變,所以當我們設定桶數為10 + 1 = 11時,就永遠不會觸發擴容機制,而且11正好是一個素數,剛好滿足了原文中帶有不確定性的小建議。

後話

在實際開發過程中,可以不必過於苛求這種細節,但是在提交前程式碼審視的時候,就可以對這些使用的資料結構進行最佳化,精益求精。

相關文章