哈希和哈希表(超詳細!!!)


介紹

哈希算法是通過一個哈希函數,將一段數據(也包括字符串、較大的數字等)轉化為能夠用變量表示或是直接就可作為數組下標的數字,這樣轉化后的數值我們稱之為哈希值, 也就是算出一個數來代表一個字符串

我們通過哈希值從而實現很快地查找和匹配,

常用:字符串Hash和哈希表。

字符串Hash流程 

如果我們用O(m)的時間來計算長度為m的字符串的哈希值,則總的時間復雜度並沒有改觀,這里就需要用到一個叫做滾動哈希的優化技巧。

我們選取兩個合適的互素常數b(進制)h(模數)(b < h),假設字符串C =c1c2...cm,那么我們定義哈希函數:

正常的數字是十進制的,這里b是基數,相當於把字符串看做是b進制數。

這一過程是遞推計算的,設H(c, k)為前k個字符的構成的字符串的哈希值,則:(以下均不考慮取模的情況)

如字符串C=“ACDA”(為方便處理,我們令‘A’表示1,‘B’表示2,以此類推),則:

通常題目要求的判斷字符串C 從位置k+1開始的長度為n的子串C'=ck+1ck+2...ck+n的哈希值與另一匹配串S = s1s2...sn的哈希值是否相等,則:

於是只要預處理出bn,就能在O(1)時間內得到任意的字符串子串哈希值,從而完成字符串匹配,那么上述字符串匹配問題的總復雜度就為O(n + m)。

如字符串C=“ACDA”,S=”CD”,當k=1, n=2時:

因此子串C'與匹配串S匹配。

在實現時,可以利用64位無符號整數計算哈希值,即取h=2^64,通過自然溢出省去求模運算。

字符串Hash正確性

字符串Hash對於任意不同的字符串所產生的哈希值必然是互不相同的嗎?顯然不是的,但概率很低在競賽中我們常常認為這種情況不會發生

即便如此,我們還可以再用“雙哈希”降低出現相同哈希值的概率,即取不同的模數,把不同模數算出的哈希值都記下來,只有幾個哈希值都一樣,我們才能判定匹配。我們通常用雙哈希就可以將沖突的概率降到很低,如果分別取h=10^9+7h=10^9+9,就幾乎不可能發生沖突,因為他們是一對“孿生素數”。


 【例題1】Oulipo(信息學奧賽一本通 1455)

【題目描述】

給出兩個字符串s1,s2((只有大寫字母),求s1在s2中出現多少次。 例如:s1="ABA",s2="ABAABA",答案為2。

【輸入】

輸入T組數據,每組數據輸出結果。

【輸出】

如題述。

【輸入樣例】

3

BAPC

BAPC

AZA

AZAAZAAZA

VEEDI AVERDXIVYERDLAN

【輸出樣例】

1

3

0

 

 


 

哈希表流程

現在要存儲和使用下面的線性表:A(1,75,324,43,1353,91,40)。

定義一個一維數組A[1...n],此時n=7,將表中元素按大小順序存儲在A[i]中,但這樣就算使用二分查找,我們仍需要用O(log n)的時間去查找某個元素。

為了用O(1)的時間實現查找,可以開一個一維數組A[1...1353],使得A[key]=key,但顯然造成了空間上的很大浪費,尤其是數據范圍很大時。

為了使空間開銷減少,我們可以對第二種方法加以優化,設計一個哈希函數H(key) = key mod 13,然后令A[H(key)]=key,這樣一來定義一個一維數組A[0...12]就已足夠,這種方法就是哈希表。

但剛才那樣的存儲是有問題的,如H(1)=H(40)=1,在存儲40時又把1給覆蓋掉了,那么查詢就會出現錯誤,這種不同的數據產生相同哈希值的情況我們稱之為沖突

這里與字符串Hash有所不同,可能不論我們怎樣選用哈希函數,還是很難避免產生沖突。

因此我們考慮對每一個哈希值記一個鏈表(其實也就相當於鄰接表),把所有等於同一個哈希值的數字都存儲下來,而查詢只要遍歷對應的鏈表即可,這樣實際復雜度取決於查詢的鏈表長度,也可以看做是常數級。

 

例如我們定義哈希函數H(x) = x mod 16,插入一些數據的效果如下圖。

哈希函數的構造

通常情況下,我們用除余法來構造哈希函數。 ·即選擇一個適當的正整數b,用其對取模的余數作為哈希值

其關鍵是b的選取,為了盡量避免沖突,一般選為能夠存儲下並且盡量大的素數(一般情況下我們根據空間取10^6左右的素數)。一般地說,如果b的約數越多,那么沖突的幾率就越大。

 

using namespace std;    const int N = 50000;             //定義總共存入哈希表的數字個數         
const int b = 999979;             //定義哈希函數中的模數    
int tot, adj[H], nxt[N], num[N]];                  void insert(int key)  //將一個數字插入哈希表
{                 int h = key % b;                  //除余法 
   for (int e = adj[h]; e; e = nxt[e]) {    if (num[e] == key) return ;      }                           //如果鏈表中已經出現過當前的數字就不用再存儲一遍
    nxt[++tot] = adj[h], adj[h] = tot;               num[tot] = key;  //建立鏈表,存儲下所有哈希值等於h的數字   
 }            bool query(int key)  //查詢數字x是否存在於哈希表中  
{                int h = key % b;                  //同樣先進行取余,求其哈希值  
   for (int e = adj[h]; e; e = nxt[e])               if (num[e] == key) return true//查詢對應鏈表,查詢到則表示存在 
    return false;                       //不存在返回 false  
  }   } 

 


【例題2】圖書管理(信息學奧賽一本通 1456)

【題目描述】

圖書管理是一件十分繁雜的工作,在一個圖書館中每天都會有許多新書加入。為了更方便的管理圖書(以便於幫助想要借書的客人快速查找他們是否有他們所需要的書),我們需要設計一個圖書查找系統。 該系統需要支持 2 種操作: add(s) 表示新加入一本書名為 s 的圖書。 find(s) 表示查詢是否存在一本書名為 s 的圖書。

【輸入】

第一行包括一個正整數 n,表示操作數。 以下 n 行,每行給出 2 種操作中的某一個指令條,指令格式為:add s 或 find s 在書名 s 與指令(add,find)之間有一個隔開,我們保證所有書名的長度都不超過 200。可以假設讀入數據是准確無誤的。

【輸出】

對於每個 find(s) 指令,我們必須對應的輸出一行 yes 或 no,表示當前所查詢的書是否存在於圖書館內。 注意:一開始時圖書館內是沒有一本圖書的。並且,對於相同字母不同大小寫的書名,我們認為它們是不同的。

【輸入樣例】

4

add Inside C#

find Effective Java

add Effective Java

find Effective Java

【輸出樣例】

no

yes

 

 

 

 


免責聲明!

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



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