6

根據谷歌發現的維基百科和各種.edu網站等各種來源,散列表解決衝突的最常見方式是線性或二次探測和鏈接。簡要提及隨機探查,但沒有引起太多關注。我已經實現了一個使用隨機探測來解決衝突的散列表。假設存在衝突,分辨率的工作原理如下:爲什麼隨機化探測在哈希表實現中更受歡迎?

  1. 對象的完整(32位)散列用於播種線性同餘隨機數生成器。
  2. 生成器生成32位數字,並採用模數來確定接下來要探測的哈希表中的哪個位置。

這具有非常好的屬性,無論模數空間中有多少個散列衝突,只要32位完全沒有衝突,查找和插入時間預計爲O(1)散列空間。由於探測序列是僞隨機的,與線性探測不同,因爲模空間衝突不會導致聚集行爲。由於整個系統是開放地址的,並且不會在任何地方使用鏈接列表,因此與鏈接不同,您無需在每次插入時執行內存分配。另外,由於散列的大小通常是地址空間的大小(32位機器上的32位),所以根本不可能在地址空間中填充足夠的項目以導致大量的散列衝突在一個好的散列方案下的32位散列空間。

那麼爲什麼要隨機探測這種不受歡迎的衝突解決策略呢?

回答

3

Python的字典實現這樣做。在dictobject.c一個很漂亮的評論說:

... 
The first half of collision resolution is to visit table indices via this 
recurrence: 

    j = ((5*j) + 1) mod 2**i 

For any initial j in range(2**i), repeating that 2**i times generates each 
int in range(2**i) exactly once (see any text on random-number generation for 
proof). 
... 

當然看起來像一個線性同餘RNG給我!

請注意,此類RNG的完整狀態僅爲位 - 必須避免重新訪問條目 - 因此您無法有效地使用「完整的(32位)散列的對象「來播種RNG。 Python最初從散列中種子ji位。如果還有其他衝突,它會從哈希中抓取另外5個比特,並將它們投入混合。 (閱讀該評論的其餘部分,特別是關於PERTURB_SHIFT的討論)。它繼續這種方式,在每次碰撞中添加更多位,直到它用完整個哈希碼。通過這種方式,Python可以使用散列碼提供的任意隨機性,而且代碼簡單快捷。

這是我讀過的最好的一些代碼。它在Beautiful Code的第18章中有介紹。所以我會說你正在做點什麼!

+0

有趣。我的實現並不能保證它永遠不會多次訪問同一個插槽。我做了一些蒙特卡洛模擬,並得出結論,在實踐中,這種情況發生的頻率很低,無法擔心。即使您允許多次訪問相同的位置,您仍然可以獲得**預計的** O(1)查找時間。 – dsimcha

0

難道你不會有問題,插入到一個非稀疏填充表中有沒有保證你會開始迭代重複元素之前擊中哈希表的所有元素?

因此插入時間不會很好定義。

+0

給定一個好的RNG,它應該經常訪問所有桶。 對於線性和二次探測,插入時間沒有很好的定義。 – Thomas

+1

你可以約束髮生的概率:) – Anna

4

可能的原因是線性或二次探測

  • 具有相同的最壞情況下的時間複雜度(O(表中的大小))
  • 具有相同的最佳情況下的時間複雜度(O(1 ))
  • 更容易實現
  • 是多好的RNG更快(因爲速度是一大賣點哈希表)

但我不確定。你是否用另一個衝突解決方案實現自己的散列表,並在不同情況下比較兩者?這將是非常有啓發性的。

6

使用線性查找(如double hasing)的原因之一是緩存局部性。 通過使第二個(rehash)函數成爲一個小整數的加法,大多數機會就是你會碰到同一個緩存行。對於大型哈希來說非常重要。

由於其簡單性,可能會使用鏈哈希。

0

我認爲隨機哈希的原因並沒有太多用處,當一個小哈希值從一個32位哈希計算出來時,哈希碰撞很少會發生,除非哈希函數有什麼「錯誤」,並且在在這種情況下,哈希函數的所有32位都將匹配的可能性很大(例如,因爲只有部分密鑰用於計算哈希)。如果散列函數體面,並且加載因子相當低,則線性和二次探測提供了良好的緩存局部性(請記住,大多數散列衝突將通過僅查看一個額外項目來解決,這將與線性和二次探測都是一個跟在第一個猜測之後)。在所有鍵映射到相同值的情況下,線性探針的性能稍好,有時甚至映射到少量值。鏈鬥散列可以輕鬆移除物品。