1.什麼是Hash表?算法
Hash表也稱散列表,也有直接稱爲哈希表,是一種根據關鍵字值(key-value)而直接進行訪問的數據結構。它是經過把關鍵字映射到數組的下標來加快查找速度。普通的數據結構中查找某一個關鍵字一般須要遍歷整個數據結構,時間複雜度O(n),而哈希表只須要O(1)的時間級。數組
咱們知道個重要的問題就是如何把關鍵字轉換爲數組的下標,這個轉換的函數稱爲哈希函數(也稱散列函數),轉換的過程稱爲哈希化。數據結構
2.介紹哈希函數函數
你們都用過字典,字典的優勢是咱們能夠經過前面的目錄快速定位到所要查找的單詞。若是咱們想把一本英文字典的每一個單詞,從 a 到 zyzzyva(這是牛津字典的最後一個單詞),都寫入計算機內存,以便快速讀寫,那麼哈希表是個不錯的選擇。編碼
這裏咱們將範圍縮小點,好比想在內存中存儲5000個英文單詞。咱們可能想到每一個單詞會佔用一個數組單元,那麼數組的大小是5000,同時能夠用數組下標存取單詞,這樣設想很完美,可是數組下標和單詞怎麼創建聯繫呢?設計
首先咱們要創建單詞和數字(數組下標)的關係:內存
咱們知道 ASCII 是一種編碼,其中 a 表示97,b表示98,以此類推,一直到122表示z,而每一個單詞都是由這26個字母組成,咱們能夠不用 ASCII 編碼那麼大的數字,本身設計一套相似 ASCII的編碼,好比a表示1,b表示2,依次類推,z表示26,那麼表示方法咱們就知道了。開發
接下來如何把單個字母的數字組合成表明整個單詞的數字呢?變量
①、把數字相加擴展
首先第一種簡單的方法就是把單詞的每一個字母表示的數字相加,獲得的和即是數組的下標。
好比單詞 cats 轉換成數字:
cats = 3 + 1 + 20 + 19 = 43
那麼單詞 cats 存儲在數組中的下標爲43,全部的英文單詞均可以用這個辦法轉換成數組下標。可是這個辦法真的可行嗎?
假設咱們約定一個單詞最多有 10 個字母,那麼字典的最後一個單詞爲 zzzzzzzzzz ,其轉換爲數字:
zzzzzzzzzz = 26*10 = 260
那麼咱們能夠獲得單詞編碼的範圍是從1-260。很顯然,這個範圍是不夠存儲5000個單詞的,那麼確定有一個位置存儲了多個單詞,每一個數組的數據項平均要存儲192個單詞(5000除以260)。
對於上面的問題,咱們如何解決呢?
第一種方法:考慮每一個數組項包含一個子數組或者一個子鏈表,這個辦法存數據項確實很快,可是若是咱們想要從192個單詞中查找到其中一個,那麼仍是很慢。
第二種方法:爲啥要讓那麼多單詞佔據同一個數據項呢?也就是說咱們沒有把單詞分的足夠開,數組能表示的元素太少,咱們須要擴展數組的下標,使其每一個位置都只存放一個單詞。
對於上面的第二種方法,問題產生了,咱們如何擴展數組的下標呢?
②、冪的連乘
咱們將單詞表示的數拆成數列,用適當的 27 的冪乘以這些位數(由於有26個可能的字符,以及空格,一共27個),而後把乘積相加,這樣就得出了每一個單詞獨一無二的數字。
好比把單詞cats 轉換爲數字:
cats = 3*273 + 1*272 + 20*271 + 19*270 = 59049 + 729 + 540 + 19 = 60337
這個過程會爲每一個單詞建立一個獨一無二的數,可是注意的是咱們這裏只是計算了 4 個字母組成的單詞,若是單詞很長,好比最長的10個字母的單詞 zzzzzzzzzz,僅僅是279 結果就超出了7000000000000,這個結果是很巨大的,在實際內存中,根本不可能爲一個數組分配這麼大的空間。
因此這個方案的問題就是雖然爲每一個單詞都分配了獨一無二的下標,可是隻有一小部分存放了單詞,很大一部分都是空着的。那麼如今就須要一種方法,把數位冪的連乘系統中獲得的巨大的整數範圍壓縮到可接受的數組範圍中。
對於英語字典,假設只有5000個單詞,這裏咱們選定容量爲10000 的數組空間來存放(後面會介紹爲啥須要多出一倍的空間)。那麼咱們就須要將從 0 到超過 7000000000000 的範圍,壓縮到從0到10000的範圍。
第一種方法:取餘,獲得一個數被另外一個整數除後的餘數。首先咱們假設要把從0-199的數字(用largeNumber表示),壓縮爲從0-9的數字(用smallNumber表示),後者有10個數,因此變量smallRange 的值爲10,這個轉換的表達式爲:
smallNumber = largeNumber % smallRange
當一個數被 10 整除時,餘數必定在0-9之間,這樣,咱們就把從0-199的數壓縮爲從0-9的數,壓縮率爲 20 :1。
咱們也能夠用相似的方法把表示單詞惟一的數壓縮成數組的下標:
arrayIndex = largerNumber % smallRange
這也就是哈希函數。它把一個大範圍的數字哈希(轉化)成一個小範圍的數字,這個小範圍的數對應着數組的下標。使用哈希函數向數組插入數據後,這個數組就是哈希表。
3.衝突問題
將大範圍的數字範圍壓縮到較小的數字中,確定會有幾個不一樣的單詞哈希化到同一個數字下標,即產生衝突。
解決衝突的方法有兩種
第一是開放地址法,即當衝突產生時,而經過系統的方法找到數組的一個空位,並將這個單詞填入,而再也不用哈希函數獲得數組的下標。
第二是鏈地址法,就是數組的每個數據項都建立一個子鏈表或子數組,那麼數組內不直接存儲單詞,當衝突產生時,新的數據項直接存放到這個數組下標表示的鏈表中,這種方法稱爲鏈地址法。
4.開放地址法
開發地址法中,若數據項不能直接存放在由哈希函數所計算出來的數組下標時,就要尋找其餘的位置。分別有三種方法:線性探測、二次探測以及再哈希法。
①、線性探測
在線性探測中,它會線性的查找空白單元。好比若是 5421 是要插入數據的位置,可是它已經被佔用了,那麼就使用5422,若是5422也被佔用了,那麼使用5423,以此類推,數組下標依次遞增,直到找到空白的位置。這就叫作線性探測,由於它沿着數組下標一步一步順序的查找空白單元。
須要注意的是,當哈希表變得太滿時,咱們須要擴展數組,可是須要注意的是,數據項不能放到新數組中和老數組相同的位置,而是要根據數組大小從新計算插入位置。這是一個比較耗時的過程,因此通常咱們要肯定數據的範圍,給定好數組的大小,而再也不擴容。
另外,當哈希表變得比較滿時,咱們每插入一個新的數據,都要頻繁的探測插入位置,由於可能不少位置都被前面插入的數據所佔用了,這稱爲彙集。數組填的越滿,彙集越可能發生。
這就像人羣,當某我的在商場暈倒時,人羣就會慢慢彙集。最初的人羣聚過來是由於看到了那個倒下的人,然後面聚過來的人是由於它們想知道這些人聚在一塊兒看什麼。人羣彙集的越大,吸引的人就會越多。
②、裝填因子
已填入哈希表的數據項和表長的比率叫作裝填因子,好比有10000個單元的哈希表填入了6667 個數據後,其裝填因子爲 2/3。當裝填因子不太大時,彙集分佈的比較連貫,而裝填因子比較大時,則彙集發生的很大了。
咱們知道線性探測是一步一步的日後面探測,當裝填因子比較大時,會頻繁的產生彙集,那麼若是咱們探測比較大的單元,而不是一步一步的探測呢,這就是下面要講的二次探測。
③、二次探測
二測探測是防止彙集產生的一種方式,思想是探測相距較遠的單元,而不是和原始位置相鄰的單元。
線性探測中,若是哈希函數計算的原始下標是x, 線性探測就是x+1, x+2, x+3, 以此類推;而在二次探測中,探測的過程是x+1, x+4, x+9, x+16,以此類推,到原始位置的距離是步數的平方。二次探測雖然消除了原始的彙集問題,可是產生了另外一種更細的彙集問題,叫二次彙集:好比講184,302,420和544依次插入表中,它們的映射都是7,那麼302須要以1爲步長探測,420須要以4爲步長探測, 544須要以9爲步長探測。只要有一項其關鍵字映射到7,就須要更長步長的探測,這個現象叫作二次彙集。二次彙集不是一個嚴重的問題,可是二次探測不會常用,由於還有好的解決方法,好比再哈希法。
④、再哈希法
爲了消除原始彙集和二次彙集,咱們使用另一種方法:再哈希法。
咱們知道二次彙集的緣由是,二測探測的算法產生的探測序列步長老是固定的:1,4,9,16以此類推。那麼咱們想到的是須要產生一種依賴關鍵字的探測序列,而不是每一個關鍵字都同樣,那麼,不一樣的關鍵字即便映射到相同的數組下標,也可使用不一樣的探測序列。
方法是把關鍵字用不一樣的哈希函數再作一遍哈希化,用這個結果做爲步長。對於指定的關鍵字,步長在整個探測中是不變的,不過不一樣的關鍵字使用不一樣的步長。
第二個哈希函數必須具有以下特色:
1、和第一個哈希函數不一樣
2、不能輸出0(不然,將沒有步長,每次探測都是原地踏步,算法將陷入死循環)。
專家們已經發現下面形式的哈希函數工做的很是好:stepSize = constant - key % constant; 其中constant是質數,且小於數組容量。
再哈希法要求表的容量是一個質數,假如表長度爲15(0-14),非質數,有一個特定關鍵字映射到0,步長爲5,則探測序列是0,5,10,0,5,10,以此類推一直循環下去。算法只嘗試這三個單元,因此不可能找到某些空白單元,最終算法致使崩潰。若是數組容量爲13, 質數,探測序列最終會訪問全部單元。即0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一個空位,就能夠探測到它。
5.鏈地址法
鏈地址法中,裝填因子(數據項數和哈希表容量的比值)與開放地址法不一樣,在鏈地址法中,須要有N個單元的數組中轉入N個或更多的數據項,所以裝填因子通常爲1,或比1大(有可能某些位置包含的鏈表中包含兩個或兩個以上的數據項)。
找到初始單元須要O(1)的時間級別,而搜索鏈表的時間與M成正比,M爲鏈表包含的平均項數,即O(M)的時間級別。