哈希原理與常見哈希函數

一,什麼是哈希

哈希是將任意長度的數據轉換爲一個數字的過程。這個數字是在一個固定的範圍之內的。
轉換的方法稱爲哈希函數,原值經過哈希函數計算後得到的值稱爲哈希值。

哈希函數

1.哈希特點

(1)一致性:同一個值每次經過同一個哈希函數計算後得到的哈希值是一致的。

F(x)=rand() :每次返回一個隨機值,是不好的哈希

(2)散列性:不同的值的哈希值儘量不同,理想情況下每個值對應於不同的數字。

F(x)=1 : 不管輸入什麼都返回1,是不好的哈希
2.衝突怎麼解決

把一個大的集合映射到一個固定大小的集合中,肯定是存在衝突的。這個是抽屜原理或者叫鴿巢理論。

桌上有十個蘋果,要把這十個蘋果放到九個抽屜裏,無論怎樣放,我們會發現至少會有一個抽屜裏面放不少於兩個蘋果。這一現象就是我們所說的“抽屜原理”。 抽屜原理的一般含義爲:“如果每個抽屜代表一個集合,每一個蘋果就可以代表一個元素,假如有n+1個元素放到n個集合中去,其中必定有一個集合裏至少有兩個元素。” 抽屜原理有時也被稱爲鴿巢原理。它是組合數學中一個重要的原理。

(1)拉鍊法:

鏈表地址法是使用一個鏈表數組來存儲相應數據,當hash遇到衝突的時候依次添加到鏈表的後面進行處理。Java裏的HashMap是拉鍊法解決衝突的典型應用場景。

Java8 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. 正像快速:原始數據可以快速計算出哈希值
  2. 逆向困難:通過哈希值基本不可能推導出原始數據
  3. 輸入敏感:原始數據只要有一點變動,得到的哈希值差別很大
  4. 衝突避免:很難找到不同的原始數據得到相同的哈希值

算法過程:

  1. 數據填充:

將原數據的二進制值進行補齊。

(1)填充數據:使得長度模除512後得到448,留出64個bit來存儲原信息的長度。填充規則是填充一個1,後面全部是0。

(2)填充長度數據:計算原數據的長度數據,填充到最後的64個bit上,如果消息長度數據大於64bit就使用低64位的數據。

第一步:填充數據

  1. 迭代計算:

將填充好的數據按照每份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));

第二步:數據計算

  1. 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位的哈希值。

MD5算法過程

SHA-1算法過程示意圖:

SHA-1是將每一塊數據分爲五個部分。

SHA-1算法過程

SHA-2算法過程示意圖:

SHA-2是分爲八個部分,算法也更加複雜。

SHA-2算法過程

3.SimHash

SimHash是Google提出的一種判斷文檔是否重複的哈希算法,他是將文本轉換爲一個64位的哈希值,然後計算兩個哈希值的距離,如果小於n(n一般是3)就認爲這兩個文本是相似的。

之所以能夠這樣判斷是否相似是因爲SimHash算法不同於MD5之類的算法,SimHash算法是局部敏感的哈希算法,MD5算法是全局敏感的哈希算法。在MD5中原數據只要有一個字符的變化,哈希值就會變化很大,而在SimHash算法中,原數據變化一小部分,哈希值也只有很小一部分的變化,所以只要哈希值很類似,就意味着原數據就很類似。

算法實現:

參考這個博客【[Algorithm] 使用SimHash進行海量文本去重】

(1)第一步:哈希

  1. 分詞: 將文本進行分詞,並給單詞分配權重。
  2. hash: 對每個次進行hash計算,得到哈希值。
  3. 加權: 對每個單詞的has進行加權。
  4. 合併: 把上一步加權hash值合併累計起來。
  5. 降維: 把上一步累加起來的值變爲01。如果每一位大於0 記爲 1,小於0 記爲 0。

(2)第二步:計算海明距離

兩個simhash對應二進制(01串)取值不同的數量稱爲這兩個simhash的海明距離。

舉例如下: 10101 和 00110 從第一位開始依次有第一位、第四、第五位不同,則海明距離爲3。

異或就是如果a、b兩個值不相同,則異或結果爲1。如果a、b兩個值相同,異或結果爲0。兩個simhash值進行異或,得出的結果中1的個數就是海明距離。

simhash計算過程

判斷兩個文本是否相似,就計算兩個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,每次平均分爲兩份,進行逐步細化地迭代。

  1. 第一次迭代:處於-90到0的標記爲0,0到90的標記爲1,39.928167處於1的區間,所以最終結果的第一位是1。
  2. 第二次迭代:對上一步標記爲1的部分平分,0到45標記爲0,45到90標記爲1,39.928167標記爲1處於0的區間,所以最終結果的第二位是0。
  3. 第三次迭代:對上一步標記爲0的部分平分,0到22.5標記爲0,22.5到45標記爲1,39.928167標記爲1處於0的區間,所以最終結果的第三位是0
  4. 第四次迭代:對上一步標記爲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
發佈了74 篇原創文章 · 獲贊 77 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章