一,什麼是哈希
哈希是將任意長度的數據轉換爲一個數字的過程。這個數字是在一個固定的範圍之內的。
轉換的方法稱爲哈希函數,原值經過哈希函數計算後得到的值稱爲哈希值。
1.哈希特點
(1)一致性:同一個值每次經過同一個哈希函數計算後得到的哈希值是一致的。
F(x)=rand() :每次返回一個隨機值,是不好的哈希
(2)散列性:不同的值的哈希值儘量不同,理想情況下每個值對應於不同的數字。
F(x)=1 : 不管輸入什麼都返回1,是不好的哈希
2.衝突怎麼解決
把一個大的集合映射到一個固定大小的集合中,肯定是存在衝突的。這個是抽屜原理或者叫鴿巢理論。
桌上有十個蘋果,要把這十個蘋果放到九個抽屜裏,無論怎樣放,我們會發現至少會有一個抽屜裏面放不少於兩個蘋果。這一現象就是我們所說的“抽屜原理”。 抽屜原理的一般含義爲:“如果每個抽屜代表一個集合,每一個蘋果就可以代表一個元素,假如有n+1個元素放到n個集合中去,其中必定有一個集合裏至少有兩個元素。” 抽屜原理有時也被稱爲鴿巢原理。它是組合數學中一個重要的原理。
(1)拉鍊法:
鏈表地址法是使用一個鏈表數組來存儲相應數據,當hash遇到衝突的時候依次添加到鏈表的後面進行處理。Java裏的HashMap是拉鍊法解決衝突的典型應用場景。
Java8的HashMap中,使用一個鏈表數組來存儲數據,根據元素的哈希值確定存儲的數組索引位置,當衝突時,就鏈接到元素後面形成一個鏈表,Java8中當鏈表長度超過8的時候就變成紅黑樹以優化性能,紅黑樹也可以視爲拉鍊法的一種變形。
(2)開放地址法
開放地址法是指大小爲 M 的數組保存 N 個鍵值對,其中 M >N。我們需要依靠數組中的空位解決碰撞衝突。基於這種策略的所有方法被統稱爲“開放地址”哈希表。
線性探測法,就是比較常用的一種“開放地址”哈希表的一種實現方式。線性探測法的核心思想是當衝突發生時,順序查看錶中下一單元,直到找出一個空單元或查遍全表。簡單來說就是:一旦發生衝突,就去尋找下 一個空的散列表地址,只要散列表足夠大,空的散列地址總能找到。
Java8中的HashTable就是用線性探測法來解決衝突的。
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
(2)衝突解決示例
舉個例子,假如散列長度爲8,哈希函數是:y=x%7。兩種解決衝突的方式如下:
拉鍊法解決衝突
線性探測法解決衝突
二,幾個常見哈希算法
1.MD5
MD5哈希算法是將任意字符散列到一個長度爲128位的Bit數組中,得出的結果表示爲一個32位的十六進制數字。
MD5哈希算法有以下幾個特點:
- 正像快速:原始數據可以快速計算出哈希值
- 逆向困難:通過哈希值基本不可能推導出原始數據
- 輸入敏感:原始數據只要有一點變動,得到的哈希值差別很大
- 衝突避免:很難找到不同的原始數據得到相同的哈希值
算法過程:
- 數據填充:
將原數據的二進制值進行補齊。
(1)填充數據:使得長度模除512後得到448,留出64個bit來存儲原信息的長度。填充規則是填充一個1,後面全部是0。
(2)填充長度數據:計算原數據的長度數據,填充到最後的64個bit上,如果消息長度數據大於64bit就使用低64位的數據。
- 迭代計算:
將填充好的數據按照每份512的長度進行切分,對每一份依次進行處理,每份的處理方式是使用四個函數進行依次進行計算,每個函數都有四個輸入參數,輸出也是四個數字,輸出的數字作爲下一份數據的輸入,所有份數的數據處理完畢,得到的四個數字連接起來就是最終的MD5值。
以下圖片是整個迭代計算的過程示意圖,其中四個初始參數和四個函數定義如下:
//四個初始參數值
A=0x67452301;
B=0xefcdab89;
C=0x98badcfe;
D=0x10325476;
//四個函數的定義
// a、b、c、d是每次計算時候的四個參數
F=(b&c)|((~b)&d);
F=(d&b)|((~d)&c);
F=b^c^d;
F=c^(b|(~d));
- md5的java實現
package com.chybin.algorithm.chapter2;
/**
* Create By 鳴宇淳 on 2019/12/26
**/
public class MD5{
/*
*四個鏈接變量
*/
private final int A=0x67452301;
private final int B=0xefcdab89;
private final int C=0x98badcfe;
private final int D=0x10325476;
/*
*ABCD的臨時變量
*/
private int Atemp,Btemp,Ctemp,Dtemp;
/*
*常量ti
*公式:floor(abs(sin(i+1))×(2pow32)
*/
private final int K[]={
0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,
0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,0x698098d8,
0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,
0xa679438e,0x49b40821,0xf61e2562,0xc040b340,0x265e5a51,
0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,
0xfcefa3f8,0x676f02d9,0x8d2a4c8a,0xfffa3942,0x8771f681,
0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,
0xbebfbc70,0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,
0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,0xf4292244,
0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,
0xffeff47d,0x85845dd1,0x6fa87e4f,0xfe2ce6e0,0xa3014314,
0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391};
/*
*向左位移數,計算方法未知
*/
private final int s[]={7,12,17,22,7,12,17,22,7,12,17,22,7,
12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,
4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,
15,21,6,10,15,21,6,10,15,21,6,10,15,21};
/*
*初始化函數
*/
private void init(){
Atemp=A;
Btemp=B;
Ctemp=C;
Dtemp=D;
}
/*
*移動一定位數
*/
private int shift(int a,int s){
return(a<<s)|(a>>>(32-s));//右移的時候,高位一定要補零,而不是補充符號位
}
/*
*主循環
*/
private void MainLoop(int M[]){
int F,g;
int a=Atemp;
int b=Btemp;
int c=Ctemp;
int d=Dtemp;
for(int i = 0; i < 64; i ++){
if(i<16){
F=(b&c)|((~b)&d);
g=i;
}else if(i<32){
F=(d&b)|((~d)&c);
g=(5*i+1)%16;
}else if(i<48){
F=b^c^d;
g=(3*i+5)%16;
}else{
F=c^(b|(~d));
g=(7*i)%16;
}
int tmp=d;
d=c;
c=b;
b=b+shift(a+F+K[i]+M[g],s[i]);
a=tmp;
}
Atemp=a+Atemp;
Btemp=b+Btemp;
Ctemp=c+Ctemp;
Dtemp=d+Dtemp;
}
/*
*填充函數
*處理後應滿足bits≡448(mod512),字節就是bytes≡56(mode64)
*填充方式爲先加一個0,其它位補零
*最後加上64位的原來長度
*/
private int[] add(String str){
int num=((str.length()+8)/64)+1;//以512位,64個字節爲一組
int strByte[]=new int[num*16];//64/4=16,所以有16個整數
for(int i=0;i<num*16;i++){//全部初始化0
strByte[i]=0;
}
int i;
for(i=0;i<str.length();i++){
strByte[i>>2]|=str.charAt(i)<<((i%4)*8);//一個整數存儲四個字節,小端序
}
strByte[i>>2]|=0x80<<((i%4)*8);//尾部添加1
/*
*添加原長度,長度指位的長度,所以要乘8,然後是小端序,所以放在倒數第二個,這裏長度只用了32位
*/
strByte[num*16-2]=str.length()*8;
return strByte;
}
/*
*調用函數
*/
public String getMD5(String source){
init();
int strByte[]=add(source);
for(int i=0;i<strByte.length/16;i++){
int num[]=new int[16];
for(int j=0;j<16;j++){
num[j]=strByte[i*16+j];
}
MainLoop(num);
}
return changeHex(Atemp)+changeHex(Btemp)+changeHex(Ctemp)+changeHex(Dtemp);
}
/*
*整數變成16進制字符串
*/
private String changeHex(int a){
String str="";
for(int i=0;i<4;i++){
str+=String.format("%2s", Integer.toHexString(((a>>i*8)%(1<<8))&0xff)).replace(' ', '0');
}
return str;
}
/*
*單例
*/
private static MD5 instance;
public static MD5 getInstance(){
if(instance==null){
instance=new MD5();
}
return instance;
}
private MD5(){};
public static void main(String[] args){
String str=MD5.getInstance().getMD5("123");
System.out.println(str);
}
}
2.SHA
SHA類似MD5,也是一種信息摘要算法,也是將任意長度的字符串轉換爲固定長度的數字的算法。SHA算法是一個家族,有五個算法:SHA-1、SHA-224、SHA-256、SHA-384,和SHA-512。這些變體除了生成摘要的長度、循環運行的次數等一些微小差異外,算法的基本結構是一致的。
SHA-1算法的結果是一個160個bit的數字,比MD5的128個bit要長32位,碰撞機率要低了2^32倍。可是SHA-1和MD5一樣已經被人破解,已經不安全了。
SHA-256從名字上看就表明了它的值存儲在長度爲256的bit數組中的,SHA-512信息摘要長度是512個bit。
SHA-224是SHA256的精簡版本,SHA-384是SHA-512的精簡版本,精簡版本主要用在安全等級要求不太高的場景,比如只是驗證下文件的完整性。使用什麼版本的SHA取決於安全要求和算法速度,畢竟長度越長算法計算時間約長,但是安全等級高。
SHA算法過程:
SHA算法的底層原理和MD5很相似,只是在摘要分段和處理細節上有少許差別,他們都是第一步將原數據進行填充,填充到512的整數倍,填充的信息包括10數據填充和長度填充,第二步切分爲相同大小的塊,第三步進行對每一塊迭代,每塊進行N輪運算,最終得到的值拼接起來就是最終的哈希值。
以下是MD5、SHA-1、SHA-2系列的算法過程比較:
MD5算法過程示意圖:
MD5是對每一塊數據分爲四個部分,用四個函數進行運算。最終生成128位的哈希值。
SHA-1算法過程示意圖:
SHA-1是將每一塊數據分爲五個部分。
SHA-2算法過程示意圖:
SHA-2是分爲八個部分,算法也更加複雜。
3.SimHash
SimHash是Google提出的一種判斷文檔是否重複的哈希算法,他是將文本轉換爲一個64位的哈希值,然後計算兩個哈希值的距離,如果小於n(n一般是3)就認爲這兩個文本是相似的。
之所以能夠這樣判斷是否相似是因爲SimHash算法不同於MD5之類的算法,SimHash算法是局部敏感的哈希算法,MD5算法是全局敏感的哈希算法。在MD5中原數據只要有一個字符的變化,哈希值就會變化很大,而在SimHash算法中,原數據變化一小部分,哈希值也只有很小一部分的變化,所以只要哈希值很類似,就意味着原數據就很類似。
算法實現:
參考這個博客【[Algorithm] 使用SimHash進行海量文本去重】
(1)第一步:哈希
- 分詞: 將文本進行分詞,並給單詞分配權重。
- hash: 對每個次進行hash計算,得到哈希值。
- 加權: 對每個單詞的has進行加權。
- 合併: 把上一步加權hash值合併累計起來。
- 降維: 把上一步累加起來的值變爲01。如果每一位大於0 記爲 1,小於0 記爲 0。
(2)第二步:計算海明距離
兩個simhash對應二進制(01串)取值不同的數量稱爲這兩個simhash的海明距離。
舉例如下: 10101 和 00110 從第一位開始依次有第一位、第四、第五位不同,則海明距離爲3。
異或就是如果a、b兩個值不相同,則異或結果爲1。如果a、b兩個值相同,異或結果爲0。兩個simhash值進行異或,得出的結果中1的個數就是海明距離。
判斷兩個文本是否相似,就計算兩個simhash哈希值的海明距離,根據經驗,如果海明距離小於3就可以判定兩個文本是相似的。
4.GeoHash
GeoHash 算法將經緯度哈希爲一個數字,然後將數字base32編碼爲一個字符串。
比如:北海公園的經緯度是:(39.928167,116.389550),對應的GeoHash值可以爲wx4g、wx4g0、wx4g0s、wx4g0s8、wx4g0s8q。GeoHash值代表的是這個經緯度點所在的一個矩形區域,長度越長矩形面積約小,表示的越精確。
兩個位置的GeoHash值前部分一樣的位數越多,說明兩個位置離得越近,百度地圖的查找附近、滴滴打車查找附近的車輛功能就可以使用這個算法。
GeoHash算法過程
下面對於北海公園的經緯度(39.928167,116.389550)進行編碼,瞭解下算法過程。
(1)第一步:緯度編碼
將整個地球從水平方向上進行逐步切分,確定緯度39.928167在哪個區域中。
緯度範圍是-90到90,每次平均分爲兩份,進行逐步細化地迭代。
- 第一次迭代:處於-90到0的標記爲0,0到90的標記爲1,39.928167處於1的區間,所以最終結果的第一位是1。
- 第二次迭代:對上一步標記爲1的部分平分,0到45標記爲0,45到90標記爲1,39.928167標記爲1處於0的區間,所以最終結果的第二位是0。
- 第三次迭代:對上一步標記爲0的部分平分,0到22.5標記爲0,22.5到45標記爲1,39.928167標記爲1處於0的區間,所以最終結果的第三位是0
- 第四次迭代:對上一步標記爲0的部分平分,22.5到33.75標記爲0,33.75到45標記爲1,39.928167標記爲1處於1的區間,所以最終結果的第三位是1。
經過N次迭代後,得到一個長度爲N的二進制值,比如得到的值爲1011100011,這個就是對緯度進行的編碼最終值。
(2)第二步:經度編碼
對經度的編碼過程跟對緯度編碼過程十分類似,不同點是經度範圍是-180到180,對經度116.389550經過N次迭代後得到編碼值。比如得到1101001011。這個就是對經度編碼的最終值。
(3)第三步:合併經緯度
對緯度編碼值、經度編碼值進行合併,合併規則是奇數位放緯度、偶數位放經度,合併爲一個新的二進制串。
(4)第四步:轉換爲字符串
將上一步合併的二進制11100 11101 00100 01111每5位一段轉換爲十進制,結果是28、29、4、15,Base32編碼後爲wx4g。這個就是北海公園的經緯度(39.928167,116.389550)最終的GeoHash編碼值。
以下圖表是二進制數字、base32字符對應表:
Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Base | 32 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | b | c | d | e | f |
Decimal | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Base | 32 | h | j | k | m | n | p | q | r | s | t | u | v | w | x | y |