散列(哈希 hash)
目錄
- 前言
- hash介紹
- 字符串hash初步
- 練習(xí)題
前言
散列(hash)是常用的算法之一。
為了了解hash我們先看一個(gè)簡(jiǎn)單的題。
題目:
最簡(jiǎn)單的思路當(dāng)輸入M中的數(shù)時(shí)就遍歷一次N。這樣算法的復(fù)雜度為O(MN)
當(dāng)M和N很大的時(shí)候,算法顯然太慢了。
那么如何做呢? 不妨用空間換時(shí)間
設(shè)定一個(gè)bool型數(shù)組hashTable[100010] , 其中hashTable[x]=true表示正整數(shù)x在N中出現(xiàn)過,
而hashTable[x]=false表示正整數(shù)x在N中沒有出現(xiàn)過。
這樣就可以在一開始讀入N個(gè)正整數(shù)時(shí)就進(jìn)行預(yù)處理,即當(dāng)讀入的數(shù)為x時(shí),就令hashTable[x]=true
(說明:hashTable數(shù)組需要初始化為false,表示初始化狀態(tài)下所有數(shù)均未出現(xiàn)過)。
于是,對(duì)M個(gè)欲查詢的數(shù),就能直接通過hashTable數(shù)組判斷出每個(gè)數(shù)是否出現(xiàn)過。
這種算法的復(fù)雜度為O(N+M)。
代碼如下:
如果問的是求M中的數(shù)字,在N中的數(shù)字中出現(xiàn)的次數(shù)。
只需要把數(shù)組定義為int型,找到一次就hashTable[x]++就可以了
代碼如下:
上面的兩個(gè)問題都有一個(gè)特點(diǎn),那就是直接把輸入的數(shù)作為數(shù)組的下標(biāo)來對(duì)這個(gè)數(shù)的性質(zhì)進(jìn)行統(tǒng)計(jì)(這種做法非常實(shí)用,請(qǐng)務(wù)必牢記)。
hash介紹
接著上面的習(xí)題,你會(huì)發(fā)現(xiàn)一個(gè)問題就是輸入的數(shù)太大(109)或者輸入的是一個(gè)一個(gè)字符串,就不能直接作為數(shù)組的下標(biāo)了。
要是有一種方法可以把這些亂七八糟的元素轉(zhuǎn)換為一個(gè)在能接受范圍內(nèi)的整數(shù),那該多好啊。
于是散列(hash) 就誕生了。一般來說,散列可以濃縮成一句話"將元素通過一個(gè)函數(shù)轉(zhuǎn)換為整數(shù),使得該整數(shù)可以盡量唯一地代表這個(gè)元素“。其中把這個(gè)轉(zhuǎn)換函數(shù)稱為散列函數(shù)H ,也就是說,如果元素在轉(zhuǎn)換前為key,那么轉(zhuǎn)換后就是一個(gè)整數(shù)H(key)。
那么對(duì)key是整數(shù)的情況來說,有哪些常用的散列函數(shù)呢?
一般來說,常用的有:直接定址法、平方取中法、除留余數(shù)法等,
其中直接定址法是指恒等變換(即H(key)=key,本節(jié)開始的問題就是直接把key作為數(shù)組下標(biāo),是最常見最實(shí)用的散列應(yīng)用)或是線性變換(即,H(key)=a*key+b);而平方取中法是指key的平方的中間若干位作為hash值(很少用)。
除留余數(shù)法是比較實(shí)用的。
陳留余數(shù)法是指把key除以一個(gè)數(shù)mod得到的余數(shù)作為hash值的方法,即 H(key)=key%mod
通過這個(gè)散列函數(shù),可以把很大的數(shù)轉(zhuǎn)換為不超過mod的整數(shù),這樣就可以將它作為可行的數(shù)組下標(biāo)(注意 : 表長TSize必須不小于mod,否則會(huì)產(chǎn)生越界)。顯然,當(dāng)mod是一個(gè)素?cái)?shù)時(shí),H(key)能盡可能覆蓋 [0,mod) 范圍內(nèi)的每一個(gè)數(shù).因此一般為了方便起見,下文中取TSize是一個(gè)素?cái)?shù),而mod直接取成與TSize相等。
其實(shí)你會(huì)發(fā)現(xiàn)一個(gè)問題,通過除留余數(shù)法可能會(huì)有兩個(gè)不同的數(shù)key1和key2,它們的hash值H(key1)與H(key2)是相同的,這樣當(dāng)key1已經(jīng)把表中位置為H(key1)的單元占據(jù)時(shí),key2便不能再使用這個(gè)位置了。我們把這種情況叫做 “沖突”。
既然沖突不可避免,那就要想辦法解決沖突。下面以三種方法來解決沖突為例,其中第一種和第二種都計(jì)算了新的hash值,又稱為開放定址法
當(dāng)?shù)玫絢ey的hash值H(key),但是表中下標(biāo)為H(key)的位置已經(jīng)被某個(gè)其他元素使用了,那么就檢查下一個(gè)位置H(key) +1是否被占,如果沒有,就使用這個(gè)位置;否則就繼續(xù)檢查下一個(gè)位置(也就是將hash值不斷加1),如果檢查過程中超過了表長,那么就回到表的首位繼續(xù)循環(huán),直到找到一個(gè)可以使用的位置,或者是發(fā)現(xiàn)表中所有位置都已被使用。顯然,這個(gè)做法容易導(dǎo)致扎堆,即表中連續(xù)若干個(gè)位置都被使用,這在一定程度上會(huì)降低效率。
在平方探查法中,為了盡可能避免扎堆現(xiàn)象,當(dāng)表中下標(biāo)為H(key)的位置被占時(shí),將按下面的順序檢查表中的位置: H(key)+ 12、H(key)-12、H(key)+22、H(key)-22、H(key) + 32、…如果檢查過程中H(key)+k2超過了表長TSize,那么就把H(key)+ k2對(duì)表長TSize取模;如果檢查過程中出現(xiàn)H(key)- k2<0的情況(假設(shè)表的首位為0),那么將(H(key) -k2%TSize +TSize)% TSize作為結(jié)果(等價(jià)于將H(key)- k2不斷加上TSize直到出現(xiàn)第一個(gè)非負(fù)數(shù))。如果想避免負(fù)數(shù)的麻煩,可以只進(jìn)行正向的平方探查。可以證明,如果k在[0, TSize)范圍內(nèi)都無法找到位置,那么當(dāng)k≥TSize時(shí),也一定無法找到位置。
和上面兩種方法不同,鏈地址法不計(jì)算新的hash值,而是把所有H(key)相同的key連接成一條單鏈表。這樣可以設(shè)定一個(gè)數(shù)組 Link,范圍是Link[0] ~ Link[mod- 1], 其中Link[h]存放H(key)= h的一條單鏈表,于是當(dāng)多個(gè)關(guān)鍵字key 的hash值都是h時(shí),就可以直接把這些沖突的key直接用單鏈表連接起來,此時(shí)就可以遍歷這條單鏈表來尋找所有H(key)=h的key。
當(dāng)然,一般來說,可以使用標(biāo)準(zhǔn)庫模板庫中的map 來直接使用hash的功能(C++11以后可以用unordered_map, 速度更快),因此除非必須模擬這些方法或是對(duì)算法的效粹要求比較高,一般不需要自己實(shí)現(xiàn)上面解決沖突的方法。
字符串hash初步
如果key不是整數(shù),那么又應(yīng)當(dāng)如何設(shè)計(jì)散列函數(shù)呢?
一個(gè)例子是: 如何將一個(gè)二維整點(diǎn)P的坐標(biāo)映射為一個(gè)整數(shù),使得整點(diǎn)P可以由該整數(shù)唯一地代表。假設(shè)一個(gè)整點(diǎn)P的坐標(biāo)是(x,y),其中0≤x, y≤Range,那么可以令hash函數(shù)為H§=X* Range+ y,這樣對(duì)數(shù)據(jù)范圍內(nèi)的任意兩個(gè)整點(diǎn)P與P2, H(P1)都不會(huì) 等于H(P2),就可以用H( P )來唯一地代表該整點(diǎn) P,接著便可以通過整數(shù)hash的方法來進(jìn)一步映射到較小的范圍。
字符串hash是指將一個(gè)字符串 S映射為一個(gè)整數(shù),使得該整數(shù)可以盡可能唯一地代表字符串 S。
為了討論問題方便,先假設(shè)字符串均由大寫字母A-Z構(gòu)成。在這個(gè)基礎(chǔ)上,不妨把A~ Z 視為0~25,這樣就把26個(gè)大寫字母對(duì)應(yīng)到了二十六進(jìn)制中。接著,按照將二十六進(jìn)制轉(zhuǎn)換為十進(jìn)制的思路,由進(jìn)制轉(zhuǎn)換的結(jié)論可知,在進(jìn)制轉(zhuǎn)換過程中,得到的十進(jìn)制肯定是唯一的,由此便可實(shí)現(xiàn)將字符串映射為整數(shù)的需求(注意:轉(zhuǎn)換成的整數(shù)最大為是26len-1, 其中l(wèi)en 為字符串長度)。
代碼如下:
顯然,如果字符串S的長度較長,那么轉(zhuǎn)換成的整數(shù)也會(huì)很大,因此需要注意使用時(shí)len不能太長。如果字符串中出現(xiàn)了小寫字母,那么可以把 A~Z作為0-25,而把a(bǔ)-z作為26-51,這樣就變成了52進(jìn)制轉(zhuǎn)換為10進(jìn)制的問題,做法也是相同的;
int hushFunc(char s[]),int len) {int id=0;for(int i;i<len;i++){if(s[i]>='a'&&s[i]<='z')id=id*52+s[i]-'A'+26;else if(s[i]>='A'&&s[i]<='Z')id=id*52+s[i]-'A';}return id; }而如果出現(xiàn)了數(shù)字,一般有兩種處理方法:
下面的代碼體現(xiàn)了這個(gè)例子:
int hushFunc(char s[]),int len) {int id=0;for(int i;i<len-1;i++)//末位為數(shù)字,所以排除末位{if(s[i]>='a'&&s[i]<='z')id=id*52+s[i]-'A'+26;else if(s[i]>='A'&&s[i]<='Z')id=id*52+s[i]-'A';}id=id*10+s[len-1]-'0';return id; }練習(xí)題
給出N個(gè)字符串(由恰好三位大寫字母組成),再給出M個(gè)查詢字符串,問每個(gè)查詢字符串在N個(gè)字符串中出現(xiàn)的次數(shù)。
#include<cstdio> int hushF(char a[],int len) {int id=0;for(int i=0;i<len;i++){id=id*26+a[i]-'A';}return id; } int hush[26*26*26+10]; char s[100][5],temp[5]; int main(void) {int M,N;int i;scanf("%d%d",&N,&M);for(i=0;i<N;i++){scanf("%s",s[i]);hush[hushF(s[i],3)]++;}for(i=0;i<M;i++){scanf("%s",temp);printf("%d\n",hush[hushF(temp,3)]);} }總結(jié)
以上是生活随笔為你收集整理的散列(哈希 hash)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C/C++】排序算法
- 下一篇: 散列(hash)练习题