散列表(Hash table,也叫哈希表),是根據鍵(Key)而直接訪問在內存存儲位置的數據結構。也就是說,它通過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱做散列函數,存放記錄的數組稱做散列表。
散列函數的規則是:通過某種轉換關系,使關鍵字適度的分散到指定大小的的順序結構中,越分散,則以后查找的時間復雜度越小,空間復雜度越高。
- 直接定址法:取關鍵字或關鍵字的某個線性函數值為散列地址。即hash(k) = k 或 hash(k) = a · k + b,其中a、b為常數(這種散列函數叫做自身函數)
- 數字分析法:假設關鍵字是以r為基的數,並且哈希表中可能出現的關鍵字都是事先知道的,則可取關鍵字的若干數位組成哈希地址。
- 平方取中法:取關鍵字平方后的中間幾位為哈希地址。通常在選定哈希函數時不一定能知道關鍵字的全部情況,取其中的哪幾位也不一定合適,而一個數平方后的中間幾位數和數的每一位都相關,由此使隨機分布的關鍵字得到的哈希地址也是隨機的。取的位數由表長決定。
- 折疊法:將關鍵字分割成位數相同的幾部分(最后一部分的位數可以不同),然后取這幾部分的疊加和(舍去進位)作為哈希地址。
- 隨機數法
- 除留余數法:取關鍵字被某個不大於散列表表長m的數p除后所得的余數為散列地址。即 hash(k) = k mod p, p<=m。不僅可以對關鍵字直接取模,也可在折疊法、平方取中法等運算之后取模。對p的選擇很重要,一般取素數或m,若p選擇不好,容易產生沖突。
Hash是一種典型以空間換時間的算法,比如原來一個長度為100的數組,對其查找,只需要遍歷且匹配相應記錄即可,從空間復雜度上來看,假如數組存儲的是byte類型數據,那么該數組占用100byte空間。現在我們采用Hash算法,我們前面說的Hash必須有一個規則,約束鍵與存儲位置的關系,那么就需要一個固定長度的hash表,此時,仍然是100byte的數組,假設我們需要的100byte用來記錄鍵與位置的關系,那么總的空間為200byte,而且用於記錄規則的表大小會根據規則,大小可能是不定的。
算法流程:
- 用給定的哈希函數構造哈希表;
- 根據選擇的沖突處理方法解決地址沖突,常見的解決沖突的方法:拉鏈法和線性探測法。
拉鏈法
通過哈希函數,我們可以將鍵轉換為數組的索引(0-M-1),但是對於兩個或者多個鍵具有相同索引值的情況,我們需要有一種方法來處理這種沖突。
一種比較直接的辦法就是,將大小為M 的數組的每一個元素指向一個鏈表,鏈表中的每一個節點都存儲散列值為該索引的鍵值對,這就是拉鏈法。下圖很清楚的描述了什么是拉鏈法。
“John Smith”和“Sandra Dee” 通過哈希函數都指向了152 這個索引,該索引又指向了一個鏈表, 在鏈表中依次存儲了這兩個字符串。
單獨鏈表法:將散列到同一個存儲位置的所有元素保存在一個鏈表中(聚集),該方法的基本思想就是選擇足夠大的M,使得所有的鏈表都盡可能的短小,以保證查找的效率。當鏈表過長、大量的鍵都會映射到相同的索引上,哈希表的順序查找會轉變為鏈表的查找,查找時間將會變大。對於開放尋址會造成性能的災難性損失。
實現基於拉鏈表的散列表,目標是選擇適當的數組大小M,使得既不會因為空鏈表而浪費內存空間,也不會因為鏈表太而在查找上浪費太多時間。拉鏈表的優點在於,這種數組大小M的選擇不是關鍵性的,如果存入的鍵多於預期,那么查找的時間只會比選擇更大的數組稍長。另外,我們也可以使用更高效的結構來代替鏈表存儲。如果存入的鍵少於預期,索然有些浪費空間,但是查找速度就會很快。所以當內存不緊張時,我們可以選擇足夠大的M,可以使得查找時間變為常數,如果內存緊張時,選擇盡量大的M仍能夠將性能提高M倍。
線性探測法
線性探測法是開放尋址法解決哈希沖突的一種方法,基本原理為,使用大小為M的數組來保存N個鍵值對,其中M>N,我們需要使用數組中的空位解決碰撞沖突。如下圖所示:
對照前面的拉鏈法,在該圖中,“Ted Baker” 是有唯一的哈希值153的,但是由於153被“Sandra Dee”占用了。而原先“Snadra Dee”和“John Smith”的哈希值都是152的,但是在對“Sandra Dee”進行哈希的時候發現152已經被占用了,所以往下找發現153沒有被占用,所以索引加1 把“Sandra Dee”存放在沒有被占用的153上,然后想把“Ted Baker”哈希到153上,發現已經被占用了,所以往下找,發現154沒有被占用,所以值存到了154上。
復雜度分析:
單純論查找復雜度:對於無沖突的Hash表而言,查找復雜度為O(1)。