哈希(hash)理解


轉載自https://www.cnblogs.com/mingaixin/p/4318837.html

一、什么是哈希?(一種更復雜的映射)

Hash,一般翻譯做“散列”,也有直接音譯為“哈希”的,就是把任意長度的輸入,通過散列算法(哈希函數),變換成固定長度的輸出,該輸出就是散列值(哈希值)。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出(沖突),所以不可能從散列值來唯一的確定輸入值。

映射是一種對應關系,而且集合A的某個元素只能對應集合B中的一個元素。但反過來,集合B中的一個元素可能對應多個集合A中的元素。如果B中的元素只能對應A中的一個元素,這樣的映射被稱為一一映射。這樣的對應關系在現實生活中很常見,比如:

           A  -> B

          人 -> 身份證號

          日期 -> 星座

這里的映射都是直接的映射,沒有經過函數處理,而哈希就是經過哈希函數處理后的映射

先說明幾個概念:

  1. 鍵值(函數自變量)、哈希值(因變量):

上面兩個映射中,人 -> 身份證號是一一映射的關系。在哈希表中,上述對應過程稱為hashing。A中元素a對應B中元素b,a被稱為鍵值(key),b被稱為a的hash值(hash value)

 2.哈希表(存儲映射關系)、哈希函數(映射法則f(x)):

哈希表是從一個集合A到另一個集合B的映射哈希表的核心是一個哈希函數(hash function),這個函數規定了集合A中的元素如何對應到集合B中的元素

假設集合A中的元素的元素都是三位以上的整數,哈希函數f(x)的法則為取這個數的個位數作為哈希值,即f(x)=x%10;那么在哈希表中對應的關系就是a[x]=x*%10

 3.沖突(碰撞):

兩個不同的鍵值,根據同一哈希函數計算出的哈希值相同的現象叫做沖突(碰撞)。

例如:a[122]=2,   a[123]=3,a[124]=4,   a[222]=2和a[122]=2的哈希值相同,產生沖突

 

二、常見的哈希函數

1.直接定址法:直接以關鍵字k或者k加上某個常數(k+c)作為哈希地址(哈希值)。

f(k)=k*a+b,其中a,b為合理的常數;

 

2.數字分析法:提取關鍵字中取值比較均勻的數字作為哈希地址。

例如有某些人的生日數據如下:

          年. 月. 日

          75.10.03

          85.11.23

          86.03.02

          86.07.12

          85.04.21

          96.02.15

經分析,第一位,第二位,第三位重復的可能性大,取這三位造成沖突的機會增加,所以盡量不取前三位,取后三位比較好

 

3.除留余數法:如果知道Hash表的最大長度為m,可以取不大於m的最大質數p,然后對關鍵字進行取余運算,將所得余數作為哈希表地址。

f(k)=k%p,在這里p的選取非常關鍵,p選擇的好的話,能夠最大程度地減少沖突,p一般取不大於m的最大質數。

 

4.分段疊加法:按照哈希表地址位數將關鍵字分成位數相等的幾部分,其中最后一部分可以比較短。然后將這幾部分相加,舍棄最高進位后的結果就是該關鍵字的哈希地址。

假設知道圖書的ISBN號為8903-241-23,可以將address(key)=89+03+24+12+3作為Hash地址。

 

5.平方取中法:如果關鍵字各個部分分布都不均勻的話,可以先求出它的平方值,然后按照需求取中間的幾位作為哈希地址。

假如有以下關鍵字序列{421,423,436},平方之后的結果為{177241,178929,190096},那么可以分別取{72,89,00}作為{421,423,436}的Hash地址

 

6.偽隨機數法:采用一個偽隨機數當作哈希函數。

f(key)=random(key) ,其中random為隨機函數。通常用於關鍵字長度不等時采用此法。

 

三、解決碰撞的方法

通過構造性能良好的哈希函數,可以減少沖突,但一般不可能完全避免沖突,因此解決沖突是哈希法的另一個關鍵問題。創建哈希表和查找哈希表都會遇到沖突,兩種情況下解決沖突的方法應該一致。下面以創建哈希表為例,說明解決沖突的方法。常用的解決沖突方法有以下四種:

  • 開放定址法:

開放定址法就是一旦發生了沖突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。

這種方法也稱再散列法,其基本思想是:當關鍵字key的哈希地址p=f(key)出現沖突時,以p為基礎,產生另一個哈希地址p1,如果p1仍然沖突,再以p為基礎,產生另一個哈希地址p2,…,直到找出一個不沖突的哈希地址pi ,將相應元素存入其中。這種方法有一個通用的再散列函數形式:i=(f(key)+di)%m   i=1,2,…,n,其中f(key)為哈希函數,m 為表長,di稱為增量序列。增量序列的取值方式不同,相應的再散列方式也不同。主要有以下三種:

1.線性探測再散列

di=1,2,3,…,m-1

這種方法的特點是:沖突發生時,順序查看表中下一單元,直到找出一個空單元或查遍全表。

2.二次探測再散列

 di=12,-12,22,-22,…,k2,-k2    ( k<=m/2)

這種方法的特點是:沖突發生時,在表的左右進行跳躍式探測,比較靈活。

3.偽隨機探測再散列處理

 di=偽隨機數序列。

具體實現時,應建立一個偽隨機數發生器,(如i=(i+p) % m),並給定一個隨機數做起點。

下面舉例說明上述三種處理方法

假設已知哈希表長度m=11,哈希函數為:f(key)= key  %  11,則f(47)=3,f(26)=4,f(60)=5,假設下一個關鍵字為69,則f(69)=3,與47沖突。

如果用線性探測再散列處理沖突:

下一個哈希地址為f1=(3 + 1)% 11 = 4,仍然沖突,再找下一個哈希地址為f2=(3 + 2)% 11 = 5,還是沖突,繼續找下一個哈希地址為f3=(3 + 3)% 11 = 6,此時不再沖突,將69填入5號單元。

如果用二次探測再散列處理沖突:

下一個哈希地址為f1=(3 + 12)% 11 = 4,仍然沖突,再找下一個哈希地址為f2=(3 - 12)% 11 = 2,此時不再沖突,將69填入2號單元。

如果用偽隨機探測再散列處理沖突:

假設偽隨機數序列為:2,5,9,……..,則下一個哈希地址為f1=(3 + 2)% 11 = 5,仍然沖突,再找下一個哈希地址為f2=(3 + 5)% 11 = 8,此時不再沖突,將69填入8號單元。

從上述例子可以看出,線性探測再散列容易產生“二次聚集”,即在處理同義詞的沖突時又導致非同義詞的沖突。例如,當表中i, i+1 ,i+2三個單元已滿時,下一個哈希地址為i, 或i+1 ,或i+2,或i+3的元素,都將填入i+3這同一個單元,而這四個元素並非同義詞。線性探測再散列的優點是:只要哈希表不滿,就一定能找到一個不沖突的哈希地址,而二次探測再散列和偽隨機探測再散列則不一定。

  • 鏈地址法
    • 將哈希表的每個單元作為鏈表的頭結點,所有哈希地址為i的元素構成一個同義詞鏈表。即發生沖突時就把該關鍵字鏈在以該單元為頭結點的鏈表的尾部。

這種方法的基本思想是將所有哈希地址為i的元素構成一個稱為同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進行。若選定的散列表長度為m,則可將散列表定義為一個由m個頭指針組成的指針數組T[0..m-1]。凡是散列地址為i的結點,均插入到以T[i]為頭指針的單鏈表中。T中各分量的初值均應為空指針。鏈地址法適用於經常進行插入和刪除的情況。

  • 再哈希法
    • 當哈希地址發生沖突用其他的函數計算另一個哈希函數地址,直到沖突不再產生為止。

再散列法其實很簡單,就是再使用哈希函數去散列一個輸入的時候,輸出是同一個位置就再次散列,直至不發生沖突位置

缺點:每次沖突都要重新散列,計算時間增加。

  • 建立公共溢出區
    • 將哈希表分為基本表和溢出表兩部分,發生沖突的元素都放入溢出表中。

這種方法的基本思想是:將哈希表分為基本表和溢出表兩部分,凡是和基本表發生沖突的元素,一律填入溢出表.(注意:在這個方法里面是把元素分開兩個表來存儲)

四、簡單應用

一、字符串哈希

介紹:關於字符串hash,一句話概括,就是把字符串有效的轉化為一個整數,下面介紹一種轉化方法:

進制哈希:

 

首先讓我們先回想一下二進制數。

 

對於任意一個二進制數,我們將它化為10進制的數的方法如下(以二進制數1101101為例):

 

 

進制hash用的也是一樣的原理

 

 

假設兩個較大的素數p和mod,把字符串內的每一個字母按前綴和的方式處理,每一次都乘以素數p,再加上當前字符str[i](轉化成整數),最后把求得的和對mod取模的值,就是轉化后這個字符串對應的哈希值。

假設sum[0]=str[0]

sum[i]=( sum[i-1]*p+str[i] )%mod   (i>=1);

 

for example:

取p=13, mod=101,求字符串str=“abc"對應的整數

 

sum[0]=str[0]-'a'+1; 表示a映射1。

 

sum[1]=(sum[0]*13+(int)b)%101=15;表示ab映射15。

 

sum[2]=(sum[1]*13+(int)c)%101=97; 表示abc映射97。

這樣,我們就可以記錄下每一個字符串所對應的整數,若下一次出現一個字符串,查詢整數是否出現過,就可以完成驗證。

但是也有可能出現兩個字符串對應一個整數。

調整方法(減少沖突):

調整p和mod,取p和mod都為較大的素數。  p取6~8位素數,一般可取131或13331;mod一般取1e9+7或者1e9+9;

為什么要要用unsigned long long?

如果字符串很多呢?那樣不是會溢出了嗎?

因此我們把hash值儲存在unsigned long long里面, 那樣溢出時,會自動取余2的64次方,but這樣可能會使2個不同串的哈希值相同,但這樣的概率極低(不排除你的運氣不好)。

注意:unsigned long long 數據精度很高,對格式的要求也更高,因此在做取模運算時,要注意使用一致的數據類型

 

 簡單應用題

/*
給定N個單詞(每個單詞長度不超過100,單詞字符串內僅包含小寫字母)。
請求出N個單詞中共有多少個不同的單詞。

輸入
第1行包含1個正整數N。
接下來N行每行包含一個字符串。

輸出
一個整數,代表不同單詞的個數

樣例輸入
5
lalala
hahaha
haha
lalala
haha

樣例輸出
3

提示* N <= 10000000

    不同單詞個數不超過100000

*/
#include<iostream>
#include<algorithm>
#include<string.h>
#define ull unsigned long long
using namespace std;
ull num[1000005];
ull n,p=1331,mod=1e9+7;
ull hs(string str)
{
    ull temp=str[0];
    for(int i=1;i<str.length();i++)
        temp=(temp*p+str[i])%mod;
    return temp;
}
int main()
{
    string str;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>str;
        num[i]=hs(str);
    }
    sort(num,num+n);
    int cnt=1;
    for(int i=1;i<n;i++)
    {
        if(num[i-1]!=num[i])
            cnt++;
    }
    cout<<cnt<<endl;
    return 0;
}
View Code

 

那么,對於一個字符串,怎么提取它[l,r]中的哈希值呢? 

我們已經知道這個字符串每一個位置的哈希值,類似與前綴和,即sum[r] - sum[l],但是對於 r 位置的哈希值sum[r],包含它的前一部分([0,l-1]部分),這一部分多乘了(r-l+1)個p

sum[r]=sum[l,r]+sum[l-1]*p(r-l+1)

因此區間[l,r]的哈希值可以表示為

sum[l,r]=sum[r]-sum[l-1]*p(r-l+1)

 

 

 模板題:poj4300   https://www.cnblogs.com/-citywall123/p/10841081.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM