本文轉載自: http://rock3.info/blog/2013/12/05/hashtable%E6%B5%85%E6%9E%90/
一、Hash特點
Hash,就是雜湊算法,Hash(str1)=str2,具備四種特性:
- 長變短:Hash算法可以將任意長度的數據Hash成固定長度的數據。
- 速度快:Hash算法基本上是異或和位移操作,速度很快。
- 不可逆:由hash結果找到hash前的字符串是困難的。
- 低碰撞:存在這樣的情況,Hash前輸入不同,Hash后輸出相同,但絕大多數情況是輸入不同,輸出不同。
二、HashTable
據《算法導論》上講:“很多應用中,都需要一種動態的集合結構,它僅僅支持INSERT、SEARCH和DELETE字典操作...實現字典操作的一種有效的數據結構為散列表(Hash Table)...在散列表中,查找一個元素的時間與在鏈表中查找一個元素的時間相同,最壞情況下都是O(n)...散列表是普通數組概念的推廣...”。也就是說,Hash Table是為了解決動態的插入、搜索、刪除等操作,而專門設置的一種數據結構,目的為了降低這些操作的時間復雜度。
這里面有個“字典操作”,“字典”模型時這樣的:通過某個關鍵字,能夠查到該關鍵字相關的信息,比如通過身份證號,可以查到姓名、性別、年齡、婚否等信息。這樣,就抽象出兩個關鍵的部分key和value,身份證號碼——key,姓名、性別、年齡、婚否——value。考慮普通的數據存儲方式——數組和鏈表,來存儲key和value,先假設指有100個元素,對於數組通常這樣存:
struct person {
char key[128];
struct value v;
}p[100];
對於鏈表這樣存:
struct person_list{
struct person_list *next;
char key[128];
struct value v;
};
下文中,將上圖中的1,2,3,4,5,6這樣的數組節點稱為“數組節點“,而對p1,p4,p9,p12,p13,p15這種具體的代表key和value的結構體鏈表節點,稱為Entry。
對於INSERT、SEARCH和DELETE,以及存儲空間的影響(假設key兩兩不同,插入、搜索、刪除均以key為對象):
為了平衡數組不易插入,鏈表、數組均不易索引的問題,很容易想到將key值轉化為數組的下標、建立一個映射(將字符串轉化為整數,或者直接就是整數,先這么簡單理解,這也可能產生碰撞),然后再通過指針的方式指向具體的元素,即能快速查找,又能方便插入、刪除:
這實際上已經有了Hash Table的原型:“橫向數組,縱向鏈表”,key到下標的映射就是Hash Table中的hash。下面的樣子顯得更“好看”一些,這就是一種常見的Hash Table實現:
橫向數組的下標為key的hash值,縱向鏈表為hash值相同的元素組成的鏈表(這里就是舉個例子,hash不會產生非常多的碰撞,hash值也的長度也是較長的)。這樣做有如下好出:
- 由於hash計算速度快,對於任意一個key,可以快速的找到其所屬的鏈表(O(n),n為hash數組的長度)。
- 在鏈表里進行插入、刪除操作,比較方便(O(a),取決於鏈表中元素的個數a)。
- 整體上可以將查找的時間復雜度從原來的O(x)降低到O(1+a),其中x為原來Entry的條目數量,a=x/n,也即平均每個鏈表元素的個數,也叫裝載因子。
但是畢竟由於碰撞的存再,使得搜索的時間復雜度沒能夠達到O(1)。Hash Table的一個關鍵工作就是盡量的降低碰撞。
四、“鏈接法”和“開放尋址法”
上面一個圖畫得就是“鏈接法”:對於碰撞,選擇在相同的數組節點上建立鏈接。還有一中方法來處理“碰撞”——“開放尋址法”(時間復雜度更低):每個數組節點上就一個元素,如果待插入的Entry計算出來的hash值所在的數組節點非空閑(已經有一個Enry了),就采取某種方法再選擇一個空閑的數組節點,插入該Enry。這種情況下,整個數組必須支持動態擴容:當數組空閑節點低於一個閥值時,將擴展數組容量為原來的一倍。這個閥值通常是0.72,也即數組節點有72%的比例為非空閑,就需要將數組擴容至原來的2倍。
“開放尋址法”中如何找到下一個空閑數組,有以下幾種方法但並不局限於以下方法,這里就不展開了:
- 線形探查
- 二次探查
- 雙重散列(較好)
五、HashTable的搜索復雜度
HashTable平衡了查找速度、插入速度,但是某些情況下,碰撞是不可避免的,只要有碰撞存在,就無法使搜索的時間復雜度達到O(1)。
對於“鏈接法”,搜索的時間復雜度為O(1+a),a為轉載因子。而對於“開放尋址法”,因為每個數組節點上就1個Entry,基本上能夠達到O(1)。
還有一種“完全散列”最好,能夠使得最壞的情況時間復雜度仍然為O(1),這里就不討論了。
因此,如果簡單的用一下hash table,可以采用“鏈接法”,如果追求速度,或者數據量比較大,應該采用“完全散列”或者“開放尋址法”(我猜的,沒驗證)。
六、HashTable的應用
估計,HashTable肯定會應用到數據庫的實現中,數據庫是典型的字典模型。另外HashTable在Linux內核中也有應用,很多場景均使用了hash table(hlist),如tasklet、頁表維護等,其類型定義於include/linux/types.h:
1
2
3
4
5
6
7
|
struct
hlist_head {
struct
hlist_node *first;
};
struct
hlist_node {
struct
hlist_node *next, **pprev;
};
|
Linux內核中的hlist采用的是”鏈接法“,此處不展開了。
七、總結
java里有HashMap這個詞,看了一些文章,感覺與《算法導論》上說的Hash Table沒有太大的區別,只不過是java的一種實現而已,java里面也有一種叫做HashTable的,是散列表的不同實現。
HashTable是根據Hash的特點去解決這種問題:海量數據的索引、插入、刪除時,可以先hash一下,將海量數據進行分塊,然后再進行搜索、插入、刪除等操作,以便降低時間復雜度。在最好的情況下,能夠將時間復雜度降低到O(1)。
HashTable的“橫向數組、縱向鏈表”的樣子,可以以“鏈接法”解決時間復雜度的問題,也可以以“開放尋值法”解決時間復雜度的問題,后者每個數組節點上就一個元素,hash后在相應的數組節點位置順序向后查找,在找到的第一個位置插入該元素,這種情況下,整個數組必須支持動態擴容:當數組空閑節點低於一個閥值時,將擴展數組容量為原來的一倍。這個閥值通常是0.72。
實際上Hash以后可以接多種數據結構,HashTable就是接的鏈表,如果銜接樹也是可以的,就是HashTree。
存疑:
1、我列的hash特點是傳統的md5、sha1等hash算法的特點,hash table中用的hash算法比md5、sha1要簡單很多,可能有一定的區別。
2、感覺對hash table的理解還是不太到位,《算法導論》上比較關注hash的構造方法,怎么才能構造出好的hash,但在我理解,hash就是一個使用O(1)時間完成長變短的操作,至於分布是不是均勻,那就看設計了,設計的好一些可能更均勻吧(還得看輸出,不僅僅是hash算法本身),但不好也就哪樣了。但是《算法導論》上認為分布是否均勻與查找的時間復雜度密切相關,而且應該盡量避免碰撞的情況,並給出了幾種處理碰撞的方法。
3、關於hash的設計的方法和技巧,如何能夠更加均勻分布,如何盡量快,如何碰撞少,不列了,可能里面涉及比較多的數學知識。