筆者之前對散列是什麼?哈希又是什麼?何謂散列表?散列函數又是個什麼東東比較的迷惑。java
經過看一些書,查找一些資料總算是有一些眉目了,現將相關的知識與體會記錄下來。留待往後算法
的再學習!數組
散列表(Hash table,也叫哈希表),是根據關鍵字(key value)而直接進行訪問的數據結構。安全
說的具體點就是它經過吧key值映射到表中的一個位置來訪問記錄,從而加快查找的速度。數據結構
實現key值映射的函數就叫作散列函數app
存放記錄的數組就就叫作散列表dom
實現散列表的過程一般就稱爲散列(hashing),也就是常說的hashide
這裏的散列的概念不只限於數據結構了,在計算機科學領域中,散列-哈希是一種對信息的函數
處理方法,經過某種特定的函數/算法(散列函數/hash()方法)將要檢索的項與用來檢索的索引-源碼分析
-( 散列值)關聯起來,生成一種便於搜索的數據結構--散列表。
現在,因爲散列算法所計算的散列值 具備不可逆(沒法逆向演算會原來的數值)的性質,
所以散列算法普遍的運用於加密技術。
散列的運用:
一、加密散列
在信息安全領域使用
二、散列表
一種使用散列函數將鍵名和鍵值關聯起來的數據結構
三、關聯數組
一種經常使用散列表來實現的數據結構
四、幾何散列
尋找相同或類似的幾何形狀的一種有效方法
經過上面能夠知道,散列技術的實現是基於散列函數的。這裏對散列函數進行一個較深
入的理解。 前面就知道了散列函數--哈希函數就是完成key值與位置的映射。通常說來key
以字符 串的形式居多,位置也就是一個數值。
能夠看出散列函數就像是實現信息的壓縮,把消息字符 串壓縮成數值摘要,是數據量
變小,格式得以固定下來。
散列函數的工做原理圖:
不過須要注意的是key值和通過散列函數處理以後的散列值並非惟一對應的,
這就形成了不一樣的key值具備相同的索引位置,這種現象叫作散列碰撞、也稱其爲哈希衝突。
對於hash衝突的解決辦法,將在後面予以總結。
至於散列函數的具體實現,有不少加密技術都有十分nice的實現,這裏咱們看看java中
HashMap的hash()方法實現就能夠了。HashMap採用的是內部哈希技術實現的,其中
hash()方法就是散列函數,完成key值到數組索引位置的映射。
/** * Retrieve object hash code and applies a supplemental hash function to the * result hash, which defends against poor quality hash functions. This is * critical because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ final int hash(Object k) { int h = 0; if (useAltHashing) { if (k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h = hashSeed; } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }上述代碼就是HashMap中散列函數的具體實現。JDK1.7
這裏筆者對經常使用的散列算法作一個展現:
在理解了上述散列\散列函數的概念以後咱們正式的進入到散列表的學習.
一個通俗的例子是,爲了查找電話簿中某人的號碼,能夠建立一個按照人名首字母順序排列
的表(即創建人名 x 到首字母 F(x) 的一個函數關係),在首字母爲 W 的表中查找「王」姓的電
話號碼,顯然比直接查找就要快得多。這裏使用人名做爲關鍵字,「取首字母」是這個例子中散
列函數的函數法則 F(),存放首字母的表對應散列表。關鍵字和函數法則理論上能夠任意肯定。
對於散列表這種數據結構來講,其散列函數的構造是十分關鍵的,散列函數實現了key的
映射,而且訪問記錄能夠更快的被定位。
通常來講散列函數的構造基於兩個標準:簡單、均勻
簡單指散列算法簡單快捷,散列值生成簡單。
均勻指對於key值集合中的任一關鍵字,散列函數可以以均與的機率映射到數組的任一一個
索引位置上,這樣可以減小散列碰撞。
散列函數構造方法:
直接取key值或者key值的某個線性函數值做爲散列地址。即hash(k)=k
或者hash(k)=a*k+b。
Tips: 簡單的思考一下這種方式就能夠知道,這種方式基本不會存在哈希衝突,
不過事 先咱們應該知道key集合的大小,並且使用線性函數值做爲散列地址的話,
很大程度上形成了 空間的浪費。hash(k)=k這種方式更加的雞肋不必,以這種方式
散列還不如直接數組索引。
所謂的數字分析法就是假設關鍵字key是以r爲基的數,而且hash表中可能出現的
關鍵字都是事先知道的,則可取關鍵字的若干數位組成hash地址。
Tips:這種方式極度不靈活,限制太多。
先經過求關鍵字的平方值擴大相近數的差異,而後根據表長度取中間的幾位數做爲
散列函數值。
Tips:這種方式中間的幾位數都和關鍵字的沒一位都有關,產生的散列地址較爲的
均勻。
將關鍵字分割成相同的幾位數(最後一位可不一樣),而後去這幾部分的疊加和。摺疊法
通常是和除留餘法一塊兒使用的。
取關鍵字被某個不大於散列表表長 m 的數 p 除後所得的餘數爲散列地址。即 hash(k)
= k mod p, p < m。不只能夠對關鍵字直接取模,也可在摺疊法、平方取中法等運算以後
取模。對 p 的選擇很重要,通常取素數或 m ,若 p 選擇很差,容易產生碰撞。
h(key)=random(key)
其中random爲僞隨機函數,但要保證函數值是在0到m-1之間。
總結:在上述的方法中,三、四、5三種方法的結合使用方式較好,在JDK之前的版本就是使用
的方法5。
經過上面的學習中,咱們知道散列函數獲得的key - 索引位置 並非惟一對應的,可能形成
不一樣的key值對應相同的索引位置。這是咱們應該解決的問題。實際的解決方法通常以下:
首先看看分離鏈接法,說白了這種方式就是鏈表數組的方式,將散列到同一個值得全部元素
保存在一個表中,產生相同的一個值在散列表中使用鏈表的形式存儲。哈希衝突的位置就是鏈表
的開始位置。在JKD中HashMap就是這種方式解決哈希衝突的!
HashMap中衝突處理代碼以下
for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }詳細的狀況能夠看看筆者之前基於HashMap源碼分析的文章:
http://blog.csdn.net/kiritor/article/details/8885961
這裏咱們定義一個填裝因子L,它表示散列表中實際的元素個數和數組的大小之比,
由此咱們能夠很容易的獲得查找一個元素的時間爲計算散列值的常數時間+ 遍歷鏈表
的時間。一次查找時間大約爲1+2/L。由此說明,散列表的大小並非影響查找的關鍵,
關鍵在於L,保證L近似爲1是十分有效的。
分離鏈接法的缺點在於使用了鏈表,因爲給新的單元分配地址耗費時間,形成算法速度
較慢,解決的方法就是開放地址法,在開放地址法中較爲經常使用的有兩種:
線性探測法、平方探測法。
開放地址法:
hash_i=(hash(key) + d(i)) mod m, i=1,2...k\,(k < m-1),其中hash(key)爲散列函數,
m爲散列表長,d(i)爲增量序列,i爲已發生碰撞的次數。增量序列可有下列取法:
d(i)=1,2,3...(m-1) 稱爲 線性探測;即 d(i)=i ,或者爲其餘線性函數。至關於逐個探測
存放地址的表,直到查找到一個空單元,把散列地址存放在該空單元。
d(i)=1^2, 2^2,3^2... k^2 (k < m/2) 稱爲 平方探測。相對線性探測,至關於發生碰
撞時探測間隔 d(i)=i^2 個單元的位置是否爲空,若是爲空,將地址存放進去。
d(i)=僞隨機數序列,稱爲 僞隨機探測。
下面筆者將以一個實例演示線性探測的過程,進而分析線性探測的特色,引出平方探測
關鍵字爲{89,18,49,58,69}插入到一個散列表中的狀況。此時線性探測的方法是取d(i)=i。
並假定取關鍵字除以 10 的餘數爲散列函數法則。
一、開始時hash(89)=9無衝突,直接插入;
二、hash(18)=8無衝突,直接插入;
三、hash(49)=9衝突了,開放地址,將49放入下一個空閒地址0
四、hash(58) =8衝突了,開放地址,將58放入9衝突 ,放入0衝突、放入1
五、hash(69) =9衝突,開放地址,將69放入0衝突,放入1衝突,放入2
Tips:思考其缺點!
線性探測的方式十分簡單,明白,每次插入老是可以找到一個地址,可是慢慢會
造成一個區塊,其結果稱爲一次彙集。任何關鍵字需探測愈來愈多的次數才能解決
衝突,且完成以後由簡介的增大了區塊。當填裝因子>0.5時,這種方式就不是個好
的方法了!
使用平方探測法能夠解決線性探測的一次彙集問題。通常選擇d(i)=i^2.。
至於其具體的步驟讀者能夠按照上面的實例自行的模擬一下。
這種方式會出現二次彙集的狀況:散列到同一位置的哪些元素將探測相同的備選
單元。
對於雙散列和再散列的方式筆者這裏就不在多提了。讀者能夠查閱下相關的資料。
總結:對於散列表的實現新手沒必要太過在乎,關鍵在於理解散列相關的概念。瞭解並
掌握散列函數的做用及通常的實現方式。瞭解通常hash衝突和經常使用解決辦法。
到這兒就結束了,但願你們看的開心,玩的愉快,端午節快樂。
By Kiritor
寫於2013 端午