哈希表就是一種以 鍵-值(key-indexed) 存儲數據的結構,咱們只要輸入待查找的值即key,便可查找到其對應的值。java
哈希的思路很簡單,若是全部的鍵都是整數,那麼就能夠使用一個簡單的無序數組來實現:將鍵做爲索引,值即爲其對應的值,這樣就能夠快速訪問任意鍵的值。這是對於簡單的鍵的狀況,咱們將其擴展到能夠處理更加複雜的類型的鍵。node
使用哈希查找有兩個步驟:python
1. 使用哈希函數將被查找的鍵轉換爲數組的索引。在理想的狀況下,不一樣的鍵會被轉換爲不一樣的索引值,可是在有些狀況下咱們須要處理多個鍵被哈希到同一個索引值的狀況。因此哈希查找的第二個步驟就是處理衝突android
2. 處理哈希碰撞衝突。有不少處理哈希碰撞衝突的方法,本文後面會介紹拉鍊法和線性探測法。算法
哈希表是一個在時間和空間上作出權衡的經典例子。若是沒有內存限制,那麼能夠直接將鍵做爲數組的索引。那麼全部的查找時間複雜度爲O(1);若是沒有時間限制,那麼咱們能夠使用無序數組並進行順序查找,這樣只須要不多的內存。哈希表使用了適度的時間和空間來在這兩個極端之間找到了平衡。只須要調整哈希函數算法便可在時間和空間上作出取捨。數據庫
在Hash表中,記錄在表中的位置和其關鍵字之間存在着一種肯定的關係。這樣咱們就能預先知道所查關鍵字在表中的位置,從而直接經過下標找到記錄。使ASL趨近與0. (ASL:Average Search Length,平均查找長度)數組
1) 哈希(Hash)函數是一個映象,即: 將關鍵字的集合映射到某個地址集合上,它的設置很靈活,只要這個地址集合的大小不超出容許範圍便可;安全
2) 因爲哈希函數是一個壓縮映象,所以,在通常狀況下,很容易產生「衝突」現象,即: key1!=key2,而 f (key1) = f(key2);服務器
3) 只能儘可能減小衝突而不能徹底避免衝突,這是由於一般關鍵字集合比較大,其元素包括全部可能的關鍵字, 而地址集合的元素僅爲哈希表中的地址值。網絡
在構造這種特殊的「查找表」 時,除了須要選擇一個「好」(儘量少產生衝突)的哈希函數以外;還須要找到一 種「處理衝突」 的方法。因此,下文咱們從這兩方面分別來介紹:
直接定址法是以數據元素關鍵字k自己或它的線性函數做爲它的哈希地址,即:H(k)=k 或 H(k)=a * k + b ; (其中a,b爲常數)
例1,有一我的口統計表,記錄了從1歲到100歲的人口數目,其中年齡做爲關鍵字,哈希函數取關鍵字自己,如圖(1):
地址 |
A1 |
A2 |
…… |
A99 |
A100 |
年齡 |
1 |
2 |
…… |
99 |
100 |
人數 |
980 |
800 |
…… |
495 |
107 |
能夠看到,當須要查找某一年齡的人數時,直接查找相應的項便可。如查找99歲的老人數,則直接讀出第99項便可。
地址 |
A0 |
A1 |
…… |
A99 |
A100 |
年份 |
1980 |
1981 |
…… |
1999 |
2000 |
人數 |
980 |
800 |
…… |
495 |
107 |
若是咱們要統計的是80後出生的人口數,如上表所示,那麼咱們隊出生年份這個關鍵字能夠用年份減去1980來做爲地址,此時f(key)=key-1980
這種哈希函數簡單,而且對於不一樣的關鍵字不會產生衝突,但能夠看出這是一種較爲特殊的哈希函數,實際生活中,關鍵字的元素不多是連續的。用該方法產生的哈希表會形成空間大量的浪費,所以這種方法適應性並不強。
此法僅適合於:地址集合的大小 = 關鍵字集合的大小,其中a和b爲常數。
假設關鍵字集合中的每一個關鍵字都是由 s 位數字組成 (u1, u2, …, us),分析關鍵字集中的全體,並從中提取分佈均勻的若干位或它們的組合做爲地址。
數字分析法是取數據元素關鍵字中某些取值較均勻的數字位做爲哈希地址的方法。即當關鍵字的位數不少時,能夠經過對關鍵字的各位進行分析,丟掉分佈不均勻的位,做爲哈希值。它只適合於全部關鍵字值已知的狀況。經過分析分佈狀況把關鍵字取值區間轉化爲一個較小的關鍵字取值區間。
例2,要構造一個數據元素個數n=80,哈希長度m=100的哈希表。不失通常性,咱們這裏只給出其中8個關鍵字進行分析,8個關鍵字以下所示:
K1=61317602 K2=61326875 K3=62739628 K4=61343634
K5=62706815 K6=62774638 K7=61381262 K8=61394220
分析上述8個關鍵字可知,關鍵字從左到右的第一、二、三、6位取值比較集中,不宜做爲哈希地址,剩餘的第四、五、七、8位取值較均勻,可選取其中的兩位做爲哈希地址。設選取最後兩位做爲哈希地址,則這8個關鍵字的哈希地址分別爲:2,75,28,34,15,38,62,20。
此法適於:能預先估計出全體關鍵字的每一位上各類數字出現的頻度。
將關鍵字分割成若干部分,而後取它們的疊加和爲哈希地址。兩種疊加處理的方法:移位疊加:將分 割後的幾部分低位對齊相加;邊界疊加:從一端沿分割界來回摺疊,而後對齊相加。
所謂摺疊法是將關鍵字分割成位數相同的幾部分(最後一部分的位數能夠不一樣),而後取這幾部分的疊加和(捨去進位),這方法稱爲摺疊法。這種方法適用於關鍵字位數較多,並且關鍵字中每一位上數字分佈大體均勻的狀況。
摺疊法中數位摺疊又分爲移位疊加和邊界疊加兩種方法,移位疊加是將分割後是每一部分的最低位對齊,而後相加;邊界疊加是從一端向另外一端沿分割界來回摺疊,而後對齊相加。
例3,當哈希表長爲1000時,關鍵字key=110108331119891,容許的地址空間爲三位十進制數,則這兩種疊加狀況如圖:
移位疊加 邊界疊加 8 9 1 8 9 1 1 1 9 9 1 1 3 3 1 3 3 1 1 0 8 8 0 1 + 1 1 0 + 1 1 0 (1) 5 5 9 (3)0 4 4
用移位疊加獲得的哈希地址是559,而用邊界疊加所獲得的哈希地址是44。若是關鍵字不是數值而是字符串,則可先轉化爲數。轉化的辦法能夠用ASCⅡ字符或字符的次序值。
此法適於:關鍵字的數字位數特別多。
這是一種經常使用的哈希函數構造方法。這個方法是先取關鍵字的平方,而後根據可以使用空間的大小,選取平方數是中間幾位爲哈希地址。
哈希函數 H(key)=key2 的中間幾位」由於這種方法的原理是經過取平方擴大差異,平方值的中間幾位和這個數的每一位都相關,則對不一樣的關鍵字獲得的哈希函數值不易產生衝突,由此產生的哈希地址也較爲均勻。
例4,若設哈希表長爲1000則可取關鍵字平方值的中間三位,如圖所示:
關鍵字 |
關鍵字的平方 |
哈希函數值 |
1234 |
1522756 |
227 |
2143 |
4592449 |
924 |
4132 |
17073424 |
734 |
3214 |
10329796 |
297 |
此法適於:關鍵字中的每一位都有某些數字重複出現頻度很高的現象
減去法是數據的鍵值減去一個特定的數值以求得數據存儲的位置。
例5,公司有一百個員工,而員工的編號介於1001到1100,減去法就是員工編號減去1000後即爲數據的位置。編號1001員工的數據在數據中的第一筆。編號1002員工的數據在數據中的第二筆…依次類推。從而得到有關員工的全部信息,由於編號1000之前並無數據,全部員工編號都從1001開始編號。
將十進制數X看做其餘進制,好比十三進制,再按照十三進制數轉換成十進制數,提取其中若干爲做爲X的哈希值。通常取大於原來基數的數做爲轉換的基數,而且兩個基數應該是互素的。
例6:Hash(80127429)=(80127429)13=8*137+0*136+1*135+2*134+7*133+4*132+2*131+9=(502432641)10若是取中間三位做爲哈希值,得Hash(80127429)=432
爲了得到良好的哈希函數,能夠將幾種方法聯合起來使用,好比先變基,再摺疊或平方取中等等,只要散列均勻,就能夠隨意拼湊。
取關鍵字被某個不大於哈希表表長m的數p除後所得餘數爲哈希地址,即設定哈希函數爲 Hash(key)=key mod p (p≤m),其中,除數p稱做模。
除留餘數法不只能夠對關鍵字直接取模,也能夠在摺疊、平方取中等運算後取模。對於除留餘數法求哈希地址,關鍵在於模p的選擇。使得數據元素集合中每個關鍵字經過該哈希函數映射到內存單元的任意地址上的機率相等,從而儘量減小發生哈希衝突的可能性。
理論研究代表,除留餘數法的模p取不大於表長且最接近表長m素數時效果最好,且p最好取1.1n~1.7n之間的一個素數(n爲存在的數據元素個數)。例如:當n=7時,p最好取十一、13等素數。 又例下圖:
表長m |
8 |
16 |
32 |
64 |
128 |
256 |
512 |
1000 |
模p |
7 |
13 |
31 |
61 |
127 |
251 |
503 |
997 |
因爲除留餘數法的地址計算方法簡單,並且在許多狀況下效果較好。
例7,公司有236個員工,而員工編號介於1000到9999,除留餘數法就是員工編號除以數據個數236後,去餘數即爲數據的位置。編號5428員工的數據(編號5428除以236取餘數得0)放數據中的第一筆,編號3512員工數據(編號3512除以236取餘數得8)放數據中的第九筆…依次類推。
亦稱爲「乘餘取整法」。隨機乘數法使用一個隨機實數f,0≤f<1,乘積f*k的分數部分在0~1之間,用這個分數部分的值與n(哈希表的長度)相乘,乘積的整數部分就是對應的哈希值,顯然這個哈希值落在0~n-1之間。其表達公式爲:Hash(k)=「n*(f*k%1)」其中「f*k%1」表示f*k 的小數部分,即f*k%1=f*k-「f*k」[5] ↑
例8,對下列關鍵字值集合採用隨機乘數法計算哈希值,隨機數f=0.103149002 哈希表長度n=100得圖(6):
k |
f*k |
n*((f*k)的小數部分) |
Hash(k) |
319426 |
32948.47311 |
47.78411 |
47 |
718309 |
74092.85648 |
86.50448 |
86 |
629443 |
64926.41727 |
42.14427 |
42 |
919697 |
84865.82769 |
83.59669 |
83 |
此方法的優勢是對n的選擇不很關鍵。一般若地址空間爲p位就是選n=2p.Knuth對常數f的取法作了仔細的研究,他認爲f取任何值均可以,但某些值效果更好。如f=(-1)/2=0.6180329...比較理想。
亦稱爲「乘餘取整法」。隨機乘數法使用一個隨機實數f,0≤f<1,乘積f*k的分數部分在0~1之間,用這個分數部分的值與n(哈希表的長度)相乘,乘積的整數部分就是對應的哈希值,顯然這個哈希值落在0~n-1之間。其表達公式爲:Hash(k)=「n*(f*k%1)」其中「f*k%1」表示f*k 的小數部分,即f*k%1=f*k-「f*k」
例9,對下列關鍵字值集合採用隨機乘數法計算哈希值,隨機數f=0.103149002 哈希表長度n=100得圖:
k |
f*k |
n*((f*k)的小數部分) |
Hash(k) |
319426 |
32948.47311 |
47.78411 |
47 |
718309 |
74092.85648 |
86.50448 |
86 |
629443 |
64926.41727 |
42.14427 |
42 |
919697 |
84865.82769 |
83.59669 |
83 |
此方法的優勢是對n的選擇不很關鍵。一般若地址空間爲p位就是選n=2p.Knuth對常數f的取法作了仔細的研究,他認爲f取任何值均可以,但某些值效果更好。如f=(-1)/2=0.6180329...比較理想。
在很都狀況下關鍵字是字符串,所以這樣對字符串設計Hash函數是一個須要討論的問題。下列函數是取字符串前10個字符來設計的哈希函數
Int Hash _ char (char *X) { int I ,sum i=0; while (i 10 && X[i]) Sum +=X[i++]; sum%=N; //N是記錄的條數 }
這種函數把字符串的前10個字符的ASCⅡ值之和對N取摸做爲Hash地址,只要N較小,Hash地址將較均勻分佈[0,N]區間內,所以這個函數仍是可用的。對於N很大的情形,可以使用下列函數
int ELFhash (char *key ) { Unsigned long h=0,g; whie (*key) { h=(h<<4)+ *key; key++; g=h & 0 xF0000000L; if (g) h^=g>>24; h & =~g; } h=h % N return (h); }
這個函數稱爲ELFHash(Exextable and Linking Format ,ELF,可執行連接格式)函數。它把一個字符串的絕對長度做爲輸入,並經過一種方式把字符的十進制值結合起來,對長字符串和短字符串都有效,這種方式產生的位置不可能不均勻分佈。
旋轉法是將數據的鍵值中進行旋轉。旋轉法一般並不直接使用在哈希函數上,而是搭配其餘哈希函數使用。
例11,某學校同一個系的新生(小於100人)的學號前5位數是相同的,只有最後2位數不一樣,咱們將最後一位數,旋轉放置到第一位,其他的往右移。
新生學號 |
旋轉過程 |
旋轉後的新鍵值 |
5062101 |
5062101 |
1506210 |
5062102 |
5062102 |
2506210 |
5062103 |
5062103 |
3506210 |
5062104 |
5062104 |
4506210 |
5062105 |
5062105 |
5506210 |
運用這種方法能夠只輸入一個數值從而快速地查到有關學生的信息。
在實際應用中,應根據具體狀況,靈活採用不一樣的方法,並用實際數據測試它的性能,以便作出正確斷定。一般應考慮如下五個因素 :
1.計算哈希函數所需時間 (簡單)
2.關鍵字的長度
3.哈希表大小
4.關鍵字分佈狀況
5.記錄查找頻率
經過構造性能良好的哈希函數,能夠減小衝突,但通常不可能徹底避免衝突,所以解決衝突是哈希法的另外一個關鍵問題。建立哈希表和查找哈希表都會遇到衝突,兩種狀況下解決衝突的方法應該一致。下面以建立哈希表爲例,說明解決衝突的方法。經常使用的解決衝突方法有如下四種:
所謂的開放定址法就是一旦發生了衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入
公式爲:fi(key) = (f(key)+di) MOD m (di=1,2,3,……,m-1)
※ 用開放定址法解決衝突的作法是:當衝突發生時,使用某種探測技術在散列表中造成一個探測序列。沿此序列逐個單元地查找,直到找到給定的關鍵字,或者
碰到一個開放的地址(即該地址單元爲空)爲止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探測到開放的地址則代表表
中無待查的關鍵字,即查找失敗。
好比說,咱們的關鍵字集合爲{12,67,56,16,25,37,22,29,15,47,48,34},表長爲12。 咱們用散列函數f(key) = key mod l2
當計算前S個數{12,67,56,16,25}時,都是沒有衝突的散列地址,直接存入:
計算key = 37時,發現f(37) = 1,此時就與25所在的位置衝突。
因而咱們應用上面的公式f(37) = (f(37)+1) mod 12 = 2。因而將37存入下標爲2的位置:
再哈希法又叫雙哈希法,有多個不一樣的Hash函數,當發生衝突時,使用第二個,第三個,….,等哈希函數
計算地址,直到無衝突。雖然不易發生彙集,可是增長了計算時間。
鏈地址法的基本思想是:每一個哈希表節點都有一個next指針,多個哈希表節點能夠用next指針構成一個單向鏈表,被分配到同一個索引上的多個節點能夠用這個單向
鏈表鏈接起來,如:
鍵值對k2, v2與鍵值對k1, v1經過計算後的索引值都爲2,這時及產生衝突,可是能夠通道next指針將k2, k1所在的節點鏈接起來,這樣就解決了哈希的衝突問題
這種方法的基本思想是:將哈希表分爲基本表和溢出表兩部分,凡是和基本表發生衝突的元素,一概填入溢出表
先舉個例子。咱們每一個活在世上的人,爲了可以參與各類社會活動,都須要一個用於識別本身的標誌。也許你以爲名字或是身份證就足以表明你這我的,可是這種表明性很是脆弱,由於重名的人不少,身份證也能夠僞造。最可靠的辦法是把一我的的全部基因序列記錄下來用來表明這我的,但顯然,這樣作並不實際。而指紋看上去是一種不錯的選擇,雖然一些專業組織仍然能夠模擬某我的的指紋,但這種代價實在過高了。
而對於在互聯網世界裏傳送的文件來講,如何標誌一個文件的身份一樣重要。好比說咱們下載一個文件,文件的下載過程當中會通過不少網絡服務器、路由器的中轉,如何保證這個文件就是咱們所須要的呢?咱們不可能去一一檢測這個文件的每一個字節,也不能簡單地利用文件名、文件大小這些極容易假裝的信息,這時候,咱們就須要一種指紋同樣的標誌來檢查文件的可靠性,這種指紋就是咱們如今所用的Hash算法(也叫散列算法)。
散列算法(Hash Algorithm),又稱哈希算法,雜湊算法,是一種從任意文件中創造小的數字「指紋」的方法。與指紋同樣,散列算法就是一種以較短的信息來保證文件惟一性的標誌,這種標誌與文件的每個字節都相關,並且難以找到逆向規律。所以,當原有文件發生改變時,其標誌值也會發生改變,從而告訴文件使用者當前的文件已經不是你所需求的文件。
這種標誌有何意義呢?以前文件下載過程就是一個很好的例子,事實上,如今大部分的網絡部署和版本控制工具都在使用散列算法來保證文件可靠性。而另外一方面,咱們在進行文件系統同步、備份等工具時,使用散列算法來標誌文件惟一性能幫助咱們減小系統開銷,這一點在不少雲存儲服務器中都有應用。
固然,做爲一種指紋,散列算法最重要的用途在於給證書、文檔、密碼等高安全係數的內容添加加密保護。這一方面的用途主要是得益於散列算法的不可逆性,這種不可逆性體如今,你不只不可能根據一段經過散列算法獲得的指紋來得到原有的文件,也不可能簡單地創造一個文件並讓它的指紋與一段目標指紋相一致。散列算法的這種不可逆性維持着不少安全框架的運營,而這也將是本文討論的重點。
一個優秀的 hash 算法,將能實現:
但在不一樣的使用場景中,如數據結構和安全領域裏,其中對某一些特色會有所側重。
在用到hash進行管理的數據結構中,就對速度比較重視,對抗碰撞不太看中,只要保證hash均勻分佈就能夠。好比hashmap,hash值(key)存在的目的是加速鍵值對的查找,key的做用是爲了將元素適當地放在各個桶裏,對於抗碰撞的要求沒有那麼高。換句話說,hash出來的key,只要保證value大體均勻的放在不一樣的桶裏就能夠了。但整個算法的set性能,直接與hash值產生的速度有關,因此這時候的hash值的產生速度就尤其重要,以JDK中的String.hashCode()方法爲例:
public int hashCode() { int h = hash; //hash default value : 0 if (h == 0 && value.length > 0) { //value : char storage char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
很簡潔的一個乘加迭代運算,在很多的hash算法中,使用的是異或+加法進行迭代,速度和前者差很少。
在密碼學中,hash算法的做用主要是用於消息摘要和簽名,換句話說,它主要用於對整個消息的完整性進行校驗。舉個例子,咱們登錄知乎的時候都須要輸入密碼,那麼知乎若是明文保存這個密碼,那麼黑客就很容易竊取你們的密碼來登錄,特別不安全。那麼知乎就想到了一個方法,使用hash算法生成一個密碼的簽名,知乎後臺只保存這個簽名值。因爲hash算法是不可逆的,那麼黑客即使獲得這個簽名,也絲毫沒有用處;而若是你在網站登錄界面上輸入你的密碼,那麼知乎後臺就會從新計算一下這個hash值,與網站中儲存的原hash值進行比對,若是相同,證實你擁有這個帳戶的密碼,那麼就會容許你登錄。銀行也是如此,銀行是萬萬不敢保存用戶密碼的原文的,只會保存密碼的hash值而而已。在這些應用場景裏,對於抗碰撞和抗篡改能力要求極高,對速度的要求在其次。一個設計良好的hash算法,其抗碰撞能力是很高的。以MD5爲例,其輸出長度爲128位,設計預期碰撞機率爲1/2128,這是一個極小極小的數字——而即使是在MD5被王小云教授破解以後,其碰撞機率也很是低。而對於兩個類似的字符串,MD5加密結果以下:
MD5("version1") = "966634ebf2fc135707d6753692bf4b1e"; MD5("version2") = "2e0e95285f08a07dea17e7ee111b21c8"
能夠看到僅僅一個比特位的改變,兩者的MD5值就天差地別了.
ps : 其實把hash算法當成是一種加密算法,這是不許確的,咱們知道加密老是相對於解密而言的,沒有解密何談加密呢,HASH的設計以沒法解爲目的的。而且若是咱們不附加一個隨機的salt值,HASH口令是很容易被字典攻擊入侵的。
密碼學和信息安全發展到如今,各類加密算法和散列算法已經不是隻言片語所能解釋得了的。在這裏咱們僅提供幾個簡單的概念供你們參考。
做爲散列算法,首要的功能就是要使用一種算法把原有的體積很大的文件信息用若干個字符來記錄,還要保證每個字節都會對最終結果產生影響。那麼你們也許已經想到了,求模這種算法就能知足咱們的須要。
事實上,求模算法做爲一種不可逆的計算方法,已經成爲了整個現代密碼學的根基。只要是涉及到計算機安全和加密的領域,都會有模計算的身影。散列算法也並不例外,一種最原始的散列算法就是單純地選擇一個數進行模運算,好比如下程序。
1 # 構造散列函數 2 def hash(a): 3 return a % 8 4 5 # 測試散列函數功能 6 print(hash(233)) 7 print(hash(234)) 8 print(hash(235))
# 輸出結果 - 1 - 2 - 3
很顯然,上述的程序完成了一個散列算法所應當實現的初級目標:用較少的文本量表明很長的內容(求模以後的數字確定小於8)。但也許你已經注意到了,單純使用求模算法計算以後的結果帶有明顯的規律性,這種規律將致使算法將能難保證不可逆性。因此咱們將使用另一種手段,那就是異或。
再來看下面一段程序,咱們在散列函數中加入一個異或過程。
# 構造散列函數 def hash(a): return (a % 8) ^ 5 # 測試散列函數功能 print(hash(233)) print(hash(234)) print(hash(235))
很明顯的,加入一層異或過程以後,計算以後的結果規律性就不是那麼明顯了。# 輸出結果
- 4
- 7
- 6
固然,你們也許會以爲這樣的算法依舊很不安全,若是用戶使用連續變化的一系列文本與計算結果相比對,就頗有可能找到算法所包含的規律。可是咱們還有其餘的辦法。好比在進行計算以前對原始文本進行修改,或是加入額外的運算過程(如移位),好比如下程序。
# 構造散列函數 def hash(a): return (a + 2 + (a << 1)) % 8 ^ 5 # 測試散列函數功能 print(hash(233)) print(hash(234)) print(hash(235))
這樣處理獲得的散列算法就很難發現其內部規律,也就是說,咱們並不能很輕易地給出一個數,讓它通過上述散列函數運算以後的結果等於4——除非咱們去窮舉測試。# 輸出結果
- 0
- 5
- 6
上面的算法是否是很簡單?事實上,下面咱們即將介紹的經常使用算法MD5和SHA1,其本質算法就是這麼簡單,只不過會加入更多的循環和計算,來增強散列函數的可靠性。
目前流行的 Hash 算法包括 MD五、SHA-1 和 SHA-2。
MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年設計的,MD 是 Message Digest 的縮寫。其輸出爲 128 位。MD4 已證實不夠安全。
MD5(RFC 1321)是 Rivest 於1991年對 MD4 的改進版本。它對輸入仍以 512 位分組,其輸出是 128 位。MD5 比 MD4 複雜,而且計算速度要慢一點,更安全一些。MD5 已被證實不具有」強抗碰撞性」。
SHA (Secure Hash Algorithm)是一個 Hash 函數族,由 NIST(National Institute of Standards and Technology)於 1993 年發佈第一個算法。目前知名的 SHA-1 在 1995 年面世,它的輸出爲長度 160 位的 hash 值,所以抗窮舉性更好。SHA-1 設計時基於和 MD4 相同原理,而且模仿了該算法。SHA-1 已被證實不具」強抗碰撞性」。
爲了提升安全性,NIST 還設計出了 SHA-22四、SHA-25六、SHA-384,和 SHA-512 算法(統稱爲 SHA-2),跟 SHA-1 算法原理相似。SHA-3 相關算法也已被提出。
能夠看出,上面這幾種流行的算法,它們最重要的一點區別就是」強抗碰撞性」。
你可能已經發現了,在實現算法章節的第一個例子,咱們嘗試的散列算法獲得的值必定是一個不大於8的天然數,所以,若是咱們隨便拿9個數去計算,確定至少會獲得兩個相同的值,咱們把這種狀況就叫作散列算法的「碰撞」(Collision)。
這很容易理解,由於做爲一種可用的散列算法,其位數必定是有限的,也就是說它能記錄的文件是有限的——而文件數量是無限的,兩個文件指紋發生碰撞的機率永遠不會是零。
但這並不意味着散列算法就不能用了,由於凡事都要考慮代價,買光全部彩票去中一次頭獎是毫無心義的。現代散列算法所存在的理由就是,它的不可逆性能在較大機率上獲得實現,也就是說,發現碰撞的機率很小,這種碰撞能被利用的機率更小。
隨意找到一組碰撞是有可能的,只要窮舉就能夠。散列算法獲得的指紋位數是有限的,好比MD5算法指紋字長爲128位,意味着只要咱們窮舉21282128次,就確定能獲得一組碰撞——固然,這個時間代價是不可思議的,而更重要的是,僅僅找到一組碰撞並無什麼實際意義。更有意義的是,若是咱們已經有了一組指紋,可否找到一個原始文件,讓它的散列計算結果等於這組指紋。若是這一點被實現,咱們就能夠很容易地篡改和僞造網絡證書、密碼等關鍵信息。
你也許已經聽過MD5已經被破解的新聞——但事實上,即使是MD5這種已通過時的散列算法,也很難實現逆向運算。咱們如今更多的仍是依賴於海量字典來進行嘗試,也就是經過已經知道的大量的文件——指紋對應關係,搜索某個指紋所對應的文件是否在數據庫裏存在。
下面讓咱們來看看一個真實的碰撞案例。咱們之因此說MD5過期,是由於它在某些時候已經很難表現出散列算法的某些優點——好比在應對文件的微小修改時,散列算法獲得的指紋結果應當有顯著的不一樣,而下面的程序說明了MD5並不能實現這一點。
import hashlib # 兩段HEX字節串,注意它們有細微差異 a = bytearray.fromhex("0e306561559aa787d00bc6f70bbdfe3404cf03659e704f8534c00ffb659c4c8740cc942feb2da115a3f4155cbb8607497386656d7d1f34a42059d78f5a8dd1ef") b = bytearray.fromhex("0e306561559aa787d00bc6f70bbdfe3404cf03659e744f8534c00ffb659c4c8740cc942feb2da115a3f415dcbb8607497386656d7d1f34a42059d78f5a8dd1ef") # 輸出MD5,它們的結果一致 print(hashlib.md5(a).hexdigest()) print(hashlib.md5(b).hexdigest()) ### a和b輸出結果都爲: cee9a457e790cf20d4bdaa6d69f01e41 cee9a457e790cf20d4bdaa6d69f01e41
而諸如此類的碰撞案例還有不少,上面只是原始文件相對較小的一個例子。事實上如今咱們用智能手機只要數秒就能找到MD5的一個碰撞案例,所以,MD5在數年前就已經不被推薦做爲應用中的散列算法方案,取代它的是SHA家族算法,也就是安全散列算法(Secure Hash Algorithm,縮寫爲SHA)。
安全散列算法與MD5算法本質上的算法是相似的,但安全性要領先不少——這種領先型更多的表如今碰撞攻擊的時間開銷更大,固然相對應的計算時間也會慢一點。
SHA家族算法的種類不少,有SHA0、SHA一、SHA25六、SHA384等等,它們的計算方式和計算速度都有差異。其中SHA1是如今用途最普遍的一種算法。包括GitHub在內的衆多版本控制工具以及各類雲同步服務都是用SHA1來區別文件,不少安全證書或是簽名也使用SHA1來保證惟一性。長期以來,人們都認爲SHA1是十分安全的,至少你們尚未找到一次碰撞案例。
但這一事實在2017年2月破滅了。CWI和Google的研究人員們成功找到了一例SHA1碰撞,並且很厲害的是,發生碰撞的是兩個真實的、可閱讀的PDF文件。這兩個PDF文件內容不相同,但SHA1值徹底同樣。(對於這件事的影響範圍及討論,可參考知乎上的討論:如何評價 2 月 23 日谷歌宣佈實現了 SHA-1 碰撞?)
因此,對於一些大的商業機構來講, MD5 和 SHA1 已經不夠安全,推薦至少使用 SHA2-256 算法。
在介紹HashMap的實現以前,先考慮一下,HashMap與ArrayList和LinkedList在數據複雜度上有什麼區別。下圖是他們的性能對比圖:
獲取 | 查找 | 添加/刪除 | 空間 | |
ArrayList | O(1) | O(1) | O(N) | O(N) |
LinkedList | O(N) | O(N) | O(1) | O(N) |
HashMap | O(N/Bucket_size) | O(N/Bucket_size) | O(N/Bucket_size) | O(N) |
能夠看出HashMap總體上性能都很是不錯,可是不穩定,爲O(N/Buckets),N就是以數組中沒有發生碰撞的元素,Buckets是因碰撞產生的鏈表。
注:發生碰撞其實是很是稀少的,因此N/Bucket_size約等於1
HashMap是對Array與Link的折衷處理,Array與Link能夠說是兩個速度方向的極端,Array注重於數據的獲取,而處理修改(添加/刪除)的效率很是低;Link因爲是每一個對象都保持着下一個對象的指針,查找某個數據須要遍歷以前全部的數據,因此效率比較低,而在修改操做中比較快。
本文以JDK8的API實現進行分析
在JDK8中,因爲使用了紅黑樹來處理大的鏈表開銷,因此hash這邊能夠更加省力了,只用計算hashCode並移動到低位就能夠了。
static final int hash(Object key) { int h; //計算hashCode,並沒有符號移動到低位 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
舉個例子: 363771819^(363771819 >>> 16)
0001 0101 1010 1110 1011 0111 1010 1011(363771819) 0000 0000 0000 0000 0001 0101 1010 1110(5550) XOR --------------------------------------- = 0001 0101 1010 1110 1010 0010 0000 0101(363766277)
這樣作能夠實現了高地位更加均勻地混到一塊兒。
下面給出在Java中幾個經常使用的哈希碼(hashCode)的算法。
Object類的hashCode. 返回對象的通過處理後的內存地址,因爲每一個對象的內存地址都不同,因此哈希碼也不同。這個是native方法,取決於JVM的內部設計,通常是某種C地址的偏移。
String類的hashCode. 根據String類包含的字符串的內容,根據一種特殊算法返回哈希碼,只要字符串的內容相同,返回的哈希碼也相同。
Integer等包裝類,返回的哈希碼就是Integer對象裏所包含的那個整數的數值,例如Integer i1=new Integer(100), i1.hashCode的值就是100 。因而可知,2個同樣大小的Integer對象,返回的哈希碼也同樣。
int,char這樣的基礎類,它們不須要hashCode,若是須要存儲時,將進行自動裝箱操做,計算方法同上。
計算了Hash,咱們如今要把它插入數組中了
i = (tab.length - 1) & hash;
經過位運算,肯定了當前的位置,由於HashMap數組的大小老是2^n,因此實際的運算就是 (0xfff…ff) & hash ,這裏的tab.length-1至關於一個mask,濾掉了大於當前長度位的hash,使每一個i都能插入到數組中。
這個對象是一個包裝類,Node
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; //getter and setter .etc. }
(1). 若是輸入當前的位置是空的,就插進去,如圖,左爲插入前,右爲插入後
0 0 | | 1 -> null 1 - > null | | 2 -> null 2 - > null | | ..-> null ..- > null | | i -> null i - > new node | | n -> null n - > null
(2). 若是當前位置已經有了node,且它們發生了碰撞,則新的放到前面,舊的放到後面,這叫作鏈地址法處理衝突。
0 0 | | 1 -> null 1 - > null | | 2 -> null 2 - > null | | ..-> null ..- > null | | i -> old i - > new - > old | | n -> null n - > null
咱們能夠發現,失敗的hashCode算法會致使HashMap的性能由數組降低爲鏈表,因此想要避免發生碰撞,就要提升hashCode結果的均勻性。
若是當表中的75%已經被佔用,即視爲須要擴容了
(threshold = capacity * load factor ) < size
它主要有兩個步驟:
左移1位,就是擴大到兩倍,用位運算取代了乘法運算
newCap = oldCap << 1; newThr = oldThr << 1;
for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //若是發現當前有Bucket if ((e = oldTab[j]) != null) { oldTab[j] = null; //若是這裏沒有碰撞 if (e.next == null) //從新計算Hash,分配位置 newTab[e.hash & (newCap - 1)] = e; //這個見下面的新特性介紹,若是是樹,就填入樹 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //若是是鏈表,就保留順序....目前就看懂這點 else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } }
由此能夠看出擴容須要遍歷並從新賦值,成本很是高,因此選擇一個好的初始容量很是重要。
解決擴容損失:若是知道大體須要的容量,把初始容量設置好以解決擴容損失;
好比我如今有1000個數據,須要 1000/0.75 = 1333 個坑位,又 1024 < 1333 < 2048,因此最好使用2048做爲初始容量。
解決碰撞損失:使用高效的HashCode與loadFactor,這個…因爲JDK8的高性能出現,這兒問題也不大了。
在不少的Java基礎書上都已經說過了,他們的主要區別其實就是Table全局加了線程同步保護
官方推薦使用SparseArray([spɑ:s][ə’reɪ],稀疏的數組)或者LongSparseArray代替HashMap。官方總結有一下幾點好處:
SparseArray使用基本類型(Primitive)中的int做爲Key,不須要Pair
能夠看到不管是密碼學、數據結構等計算機領域,仍是現實生活中的應用,處處能夠看到Hash的影子。但願這篇總結的博文,能夠幫助到你們更好的學習哈希算法。
最後要感謝一下博文的創做者,謝謝!
【時間倉促,若有錯誤,歡迎指正! || 歡迎留下您的評語! 你們一塊兒探討、學習區塊鏈!】
【轉載請註明出處!http://www.cnblogs.com/X-knight/】