哈希表如何工做?

我正在尋找哈希表如何工做的解釋 - 用簡單的英語表示像我這樣的傻瓜! 算法

例如,我知道它須要密鑰,計算哈希值(我正在尋找解釋如何)而後執行某種模數來計算它存儲在存儲值的數組中的位置,但這就是個人知識中止的地方。 編程

任何人均可以澄清這個過程嗎? 數組

編輯:我沒有具體詢問如何計算哈希碼,而是概述了哈希表的工做原理。 緩存


#1樓

大家很是接近徹底解釋這一點,但遺漏了一些事情。 哈希表只是一個數組。 數組自己將在每一個插槽中包含一些內容。 您至少會將哈希值或值自己存儲在此插槽中。 除此以外,您還能夠存儲已在此插槽上發生衝突的連接/連接值列表,或者您可使用開放尋址方法。 您還能夠存儲指向要今後插槽中檢索的其餘數據的指針或指針。 框架

重要的是要注意,hashvalue自己一般不指示將值放入的槽。 例如,hashvalue多是負整數值。 顯然負數不能指向數組位置。 此外,哈希值每每會比可用的時隙數量大不少倍。 所以,須要由散列表自己執行另外一個計算,以肯定該值應該進入哪一個槽。 這是經過模數運算來完成的,例如: dom

uint slotIndex = hashValue % hashTableSize;

該值是值將進入的槽。 在開放尋址中,若是插槽已經填充了另外一個哈希值和/或其餘數據,則將再次運行模數操做以查找下一個插槽: 函數

slotIndex = (remainder + 1) % hashTableSize;

我想可能還有其餘更先進的方法來肯定插槽索引,但這是我見過的常見方法...會對其餘性能更好的人感興趣。 佈局

使用模數方法,若是您有一個大小爲1000的表,則任何介於1和1000之間的哈希值將進入相應的槽。 任何負值以及任何大於1000的值均可能會碰撞插槽值。 發生這種狀況的可能性取決於您的散列方法,以及您添加到散列表的總項數。 一般,最佳作法是使散列表的大小使得添加到其中的值的總數僅等於其大小的大約70%。 若是您的哈希函數在均勻分佈方面作得很好,您一般會遇到不多甚至沒有桶/槽衝突,而且它將對查找和寫入操做執行得很是快。 若是事先不知道要添加的值的總數,請使用任何方法進行良好的估計,而後在添加到其中的元素數量達到容量的70%時調整哈希表的大小。 性能

我但願這有幫助。 測試

PS - 在C#中, GetHashCode()方法很是慢,而且在我測試的不少條件下致使實際值衝突。 爲了一些真正的樂趣,構建本身的哈希函數並嘗試使其永遠不會碰撞您正在散列的特定數據,運行速度比GetHashCode快,而且具備至關均勻的分佈。 我使用long而不是int size hashcode值完成了這項工做,而且它在哈希表中有多達3200萬個哈希值,而且有0次衝突。 不幸的是,我沒法共享代碼,由於它屬於個人僱主...但我能夠透露它多是某些數據域。 當你能夠實現這一點時,哈希表很是快。 :)


#2樓

不少答案,但沒有一個是很是直觀的 ,哈希表能夠在可視化時輕鬆「點擊」。

散列表一般實現爲連接列表的數組。 若是咱們想象一個存儲人名的表,在幾回插入以後它可能會在內存中佈局以下,其中()封閉的數字是文本/名稱的哈希值。

bucket#  bucket content / linked list

[0]      --> "sue"(780) --> null
[1]      null
[2]      --> "fred"(42) --> "bill"(9282) --> "jane"(42) --> null
[3]      --> "mary"(73) --> null
[4]      null
[5]      --> "masayuki"(75) --> "sarwar"(105) --> null
[6]      --> "margaret"(2626) --> null
[7]      null
[8]      --> "bob"(308) --> null
[9]      null

幾點:

  • 每一個數組條目(indices [0][1] ...)被稱爲一個 ,並啓動一個 - 多是空的 - 連接列表(在這個例子中也稱爲元素 - 人名
  • 每一個值(例如,帶有散列42 "fred" )從bucket [hash % number_of_buckets]連接,例如42 % 10 == [2] ; %模運算符 - 除以桶的數量時的餘數
  • 多個數據值可能在同一個桶中發生衝突並從中連接,最多見的緣由是它們的哈希值在模運算後發生衝突(例如42 % 10 == [2]9282 % 10 == [2] ),但偶爾會由於哈希值是相同的(例如"fred""jane"都用上面的哈希42顯示)
    • 大多數哈希表處理衝突 - 性能略有下降但沒有功能混淆 - 經過將正在搜索或插入的值的完整值(此處爲文本)與散列到桶中連接列表中已有的每一個值進行比較

鏈表長度與加載因子有關,而與值的數量無關

若是表大小增長,上面實現的哈希表傾向於調整自身大小(即建立更大的桶數組,從那裏建立新的/更新的鏈表,刪除舊數組)以保持值與桶的比率(也稱爲負載)因素 )在0.5到1.0範圍內的某個地方。

Hans在下面的註釋中給出了其餘負載因子的實際公式,可是對於指示值:使用加載因子1和加密強度哈希函數,1 / e(~36.8%)的桶將傾向於爲空,另外1 / e (~36.8%)有一個元素,1 /(2e)或~18.4%兩個元素,1 /(3!e)約6.1%三元素,1 /(4!e)或~1.5%四元素,1 / (5!e)〜。3%有五個等等。 - 不管表中有多少元素,非空桶的平均鏈長是~1.58(便是否有100個元素和100個桶,或1億個元素和1億個桶),這就是爲何咱們說查找/插入/擦除是O (1)常數時間操做。

哈希表如何將鍵與值相關聯

給定如上所述的哈希表實現,咱們能夠想象建立一個值類型,例如struct Value { string name; int age; }; struct Value { string name; int age; }; ,以及僅查看name字段(忽略年齡)的相等比較和哈希函數,而後發生了一些奇妙的事情:咱們能夠在表中存儲像{"sue", 63}這樣的Value記錄,而後在沒有搜索「sue」的狀況下了解她的年齡,找到儲值,恢復甚至更新她的年齡
- 生日快樂蘇 - 有趣的是不會改變哈希值,所以不須要咱們將蘇的記錄移動到另外一個桶。

當咱們這樣作時,咱們使用哈希表做爲關聯容器map ,而且它存儲的值能夠被認爲是由一個 (名稱)和一個或多個其餘字段組成 - 仍然被稱爲 - 容易混淆 - (在個人例子中,只是年齡)。 用做映射的哈希表實現稱爲哈希映射

這與本答案前面的示例造成對比,咱們存儲了像「sue」這樣的離散值,您能夠將其視爲本身的密鑰:這種用法稱爲哈希集

還有其餘方法能夠實現哈希表

並不是全部哈希表都使用連接列表(稱爲單獨連接 ),但大多數通用連接列表都是如此,由於主要的替代方案是封閉散列 (也稱爲開放尋址 - 特別是支持擦除操做 - 具備較少穩定的性能屬性,易於發生衝突/哈希函數。


哈希函數的幾個字

強烈的哈希......

通用的,最壞狀況下的衝突最小化散列函數的工做是有效地隨機地在哈希表桶周圍噴射密鑰,同時老是爲相同的密鑰生成相同的哈希值。 即便在密鑰中任何地方改變一位,理想狀況下 - 隨機 - 在結果散列值中翻轉大約一半的位。

這一般是用數學太精心策劃的,這對我來講太複雜了。 我將提到一種易於理解的方式 - 不是最具擴展性或緩存友好性但本質上優雅(如使用一次性密碼加密!) - 由於我認爲它有助於將上述所需的品質帶回家。 假設您正在散列64位double s - 您能夠建立8個表,每一個256個隨機數(下面的代碼),而後使用double的內存表示的每一個8位/ 1字節切片索引到不一樣的表,對你查找的隨機數進行異或。 經過這種方法,很容易看出在double精度中任何位置改變的位(在二進制數字意義上)致使在其中一個表中查找不一樣的隨機數,以及徹底不相關的最終值。

// note caveats above: cache unfriendly (SLOW) but strong hashing...
size_t random[8][256] = { ...random data... };
const char* p = (const char*)&my_double;
size_t hash = random[0][p[0]] ^ random[1][p[1]] ^ ... ^ random[7][p[7]];

弱但快速的哈希......

許多庫的散列函數經過未更改的整數傳遞(稱爲平凡標識散列函數); 它是上述強烈散列的另外一個極端。 在最糟糕的狀況下,身份哈希極易發生衝突,但但願是在至關廣泛的整數鍵的狀況下,每每會遞增(可能有一些間隙),它們會映射到連續的桶中,留下的空白比隨機散列更少葉子(咱們在前面提到的載荷因子1下約爲36.8%),因​​此與隨機映射相比,碰撞元素的碰撞更少,連接列表的連接列表更少。 保存生成強哈希所需的時間也很棒,若是按順序查找密鑰,它們將在內存中的存儲桶中找到,從而提升緩存命中率。 當鍵沒有很好地增長時,但願它們是隨機的,它們不須要強大的哈希函數來徹底隨機化它們放置到桶中。


#3樓

哈希表徹底依賴於實際計算遵循隨機訪問機器模型的事實,即在O(1)時間或恆定時間內能夠訪問存儲器中任何地址的值。

因此,若是我有一個密鑰世界(我能夠在應用程序中使用的全部可能密鑰的集合,例如,對於學生來講是滾動號,若是它是4位數,那麼這個宇宙是一組從1到9999的數字),以及將它們映射到有限的大小數量的方法我能夠在個人系統中分配內存,理論上個人哈希表已準備就緒。

一般,在應用程序中,密鑰的大小很是大於我想要添加到哈希表的元素的數量(我不想浪費1 GB的內存來哈希,好比10000或100000個整數值,由於它們是32在二元reprsentaion有點長)。 因此,咱們使用這個散列。 這是一種混合類型的「數學」操做,它將個人大宇宙映射到一小部分值,我能夠在記憶中容納它們。 在實際狀況中,散列表的空間一般與(元素的數量*每一個元素的大小)具備相同的「順序」(big-O),所以,咱們不會浪費太多內存。

如今,一個大的集合映射到一個小集合,映射必須是多對一的。 所以,不一樣的密鑰將被分配到相同的空間(不公平)。 有幾種方法能夠解決這個問題,我只知道其中的兩個:

  • 使用要分配給值的空間做爲對連接列表的引用。 此連接列表將存儲一個或多個值,這些值將在多對一映射中駐留在同一插槽中。 鏈表還包含幫助搜索人員的密鑰。 這就像同一間公寓裏的不少人同樣,當一個送貨員來的時候,他去了房間並專門詢問那我的。
  • 在數組中使用雙哈希函數,每次都給出相同的值序列而不是單個值。 當我去存儲一個值時,我會看到所需的內存位置是空閒仍是佔用。 若是它是免費的,我能夠在那裏存儲個人值,若是它被佔用我從序列中取下一個值,依此類推,直到我找到一個空閒位置並將值存儲在那裏。 當搜索或檢索該值時,我返回到序列給出的相同路徑,而且在每一個位置詢問vaue是否存在,直到我找到它或搜索陣列中的全部可能位置。

CLRS的算法簡介提供了對該主題的很是好的看法。


#4樓

對於全部尋求編程用語的人來講,這是它的工做原理。 高級哈希表的內部實現對存儲分配/解除分配和搜索有許多複雜性和優化,但頂級思想將很是類似。

(void) addValue : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   if (bucket) 
   {
       //do nothing, just overwrite
   }
   else   //create bucket
   {
      create_extra_space_for_bucket();
   }
   put_value_into_bucket(bucket,value);
}

(bool) exists : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   return bucket;
}

其中calculate_bucket_from_val()是散列函數,其中必須發生全部惟一性魔法。

經驗法則是: 對於要插入的給定值,存儲桶必須是從它應該存儲的值開始的惟一和可衍生的。

Bucket是存儲值的任何空間 - 對於這裏我將int保存爲數組索引,但它也多是一個內存位置。


#5樓

如何計算哈希一般不依賴於哈希表,而是依賴於添加到它的項。 在諸如.net和Java的框架/基類庫中,每一個對象都有一個GetHashCode()(或相似)方法,返回該對象的哈希碼。 理想的哈希碼算法和確切的實現取決於對象中表示的數據。

相關文章
相關標籤/搜索