哈希(hash)
對於我來說,HASH就像一個加密軟件,你輸入一個值,他就會輸出值,並且比之前的值更優,更方便。而這個值呢,就叫做哈希值。然后字符串哈希就是輸入一個字符串,把它轉成對應的HASH值就行了。
對於每個字符串,我們通過一個固定的轉換方式,使相同字符串的哈希值一定相同,不同字符串的值盡量不同。因為很可能存在兩個不同的字符串哈希值一樣的操作,我們稱之為“哈希沖突”。
我們此處傳換的方式,就是最常見的進制哈希,它的核心是給出一個固定進制base,把字符串上面的每一個元素看成base進制的每一個數字,然后轉換成十進制,最后的結果就是HASH值。最后我們只需要比較每一個字符串的HASH值就可以知道他們是不是同一個字符串。
關於進制的選擇,還是很自由的,但是一定不要含有mod的質因子(那你還模什么模),所以我們取進制和mod時,一般都是質數。但是簡單的還是利用unsigned long long,不手動進行取模,它溢出時會自動對2^64取模
下面我利用【模板】字符串哈希 給大家介紹一下這兩種進制哈希:
題目描述
如題,給定N個字符串(第i個字符串長度為Mi,字符串內包含數字、大小寫字母,大小寫敏感),請求出N個字符串中共有多少個不同的字符串。
輸入輸出格式
輸入格式:
第一行包含一個整數N,為字符串的個數。
接下來N行每行包含一個字符串,為所提供的字符串。
輸出格式:
輸出包含一行,包含一個整數,為不同的字符串個數。
輸入樣例1
5 abc aaaa abc abcc 12345
輸出樣例1
4
1、自然溢出哈希
對於這個哈希,我們不對它取模,而是利用unsigned long long的溢出取模。
1 #include<bits/stdc++.h> 2 #define FAST std::ios::sync_with_stdio(false),std::cin.tie(0),std::cout.tie(0) 3 using namespace std; 4 typedef unsigned long long ull;//typedef專門把C++的值的類型改名字,和宏定義一個道理,如自帶的int,char或者自定義的struct 5 int n,ans=1;//種類因為不搜第一個,所以初值是一 6 ull base=131;//進制數 7 int a[10001];//記錄hash值 8 int hash(string s) 9 { 10 ull sum=0;//哈希值 11 for(int i=0;i<s.size();i++) 12 { 13 sum=sum*base+(ull)(s[i]);//乘進制數加上這一位 14 } 15 return sum;//返回hash值 16 } 17 int main() 18 { 19 FAST;//優化輸入輸出 20 cin>>n; 21 for(int i=1;i<=n;i++) 22 { 23 string s;//輸入字符串 24 cin>>s; 25 a[i]=hash(s);//給它hash值 26 } 27 sort(a+1,a+1+n);//hash值排序 28 for(int i=2;i<=n;i++) 29 { 30 if(a[i]!=a[i-1])ans++;//不一樣種類就加一 31 } 32 cout<<ans; 33 }
2、單哈希
自定義取模的值就行了。
1 #include<bits/stdc++.h> 2 #define FAST std::ios::sync_with_stdio(false),std::cin.tie(0),std::cout.tie(0) 3 using namespace std; 4 int mod=20160817;//神奇的數字(質數)但是交上去只能得80分,所以我們用一個大一點的質數(212370440130137957ll) 不要在意后面的兩個符號,交上去就對了 5 int n,ans=1; 6 long long base=131; 7 int a[100001]; 8 int hash(string s) 9 { 10 int sum=0;//哈希值 11 for(int i=0;i<s.size();i++) 12 { 13 sum=sum*base+(int)(s[i]);//乘進制數加上這一位 14 sum%=mod;//模一下 15 } 16 return sum;//返回hash值 17 } 18 int main() 19 { 20 FAST;//優化輸入輸出 21 cin>>n; 22 for(int i=1;i<=n;i++) 23 { 24 string s;//輸入字符串 25 cin>>s; 26 a[i]=hash(s);//給它hash值 27 } 28 sort(a+1,a+1+n);//hash值排序 29 for(int i=2;i<=n;i++) 30 { 31 if(a[i]!=a[i-1])ans++;//不一樣種類就加一 32 } 33 cout<<ans; 34 }
很顯然,大家看到上面的代碼,還是有可能出現哈希沖突的情況,針對這種情況,我們要不就把模的數再大一點,要不就換種方式,來解決。
3、多重哈希
這其實就是你用不同的兩種或多種方式哈希,然后分別比對每一種哈希值是否相同——顯然是增加了空間和時間,但也確實增加了其正確性。
1 #include<bits/stdc++.h> 2 #define FAST std::ios::sync_with_stdio(false),std::cin.tie(0),std::cout.tie(0) 3 using namespace std; 4 int mod1=20160817; 5 int mod2=19260817; 6 int n,ans=1; 7 int base=131; 8 struct node{ 9 int x,y; 10 }a[100001]; 11 int hash1(string s) 12 { 13 int sum=0; 14 for(int i=0;i<s.size();i++) 15 { 16 sum=base*sum+(int)(s[i]); 17 sum%=mod1; 18 } 19 return sum; 20 } 21 int hash2(string s) 22 { 23 int sum=0; 24 for(int i=0;i<s.size();i++) 25 { 26 sum=base*sum+(int)(s[i]); 27 sum%=mod2; 28 } 29 return sum;//返回hash值 30 } 31 bool sj(node x,node y) 32 { 33 return x.x<y.x; 34 } 35 int main() 36 { 37 FAST;//優化輸入輸出 38 cin>>n; 39 for(int i=1;i<=n;i++) 40 { 41 string s;//輸入字符串 42 cin>>s; 43 a[i].x=hash1(s);//給它hash值 44 a[i].y=hash2(s); 45 } 46 sort(a+1,a+1+n,sj);//hash值排序 47 for(int i=2;i<=n;i++) 48 { 49 if(a[i].x!=a[i-1].x||a[i].y!=a[i-1].y)ans++; 50 } 51 cout<<ans; 52 }
哈希表
說到底,我們哈希還是來分辨字符串的。有人說:字符串不可以用map嗎?
可以的,但是你要知道,map是O(n)的,而哈希只需要計算一下,就直接找到,O(1)的,所以明白了嗎?
這就是hash_map,也叫哈希表
哈希表呢,就是開一串數組,然后每一個字符串對應一個下標,而這個下標,就是他的哈希值(因為這里的數組開不了很大,所以一般不用自然溢出)
所以就存在哈希值重復的情況,我們這里有三種操作方式
開鏈法(鏈式前向星)
當哈希值沖突的時候,我們就可以用鏈式前向星(就和那個存圖一樣的)
void add(int x) { cnt++; int key=x%mod; sum[cnt].pre=last[key]; last[key]=cnt; sum[cnt].x=x; }
這里的key是哈希值,last[i]是最后一個哈希值為i的編號,然后pre就是上一個,連起來就行了。查找的時候就像基本的前向星一樣查找就是了,一個一個向上遍歷
這里要提醒一下,當你的mod大,數組就大,空間就大,相應的沖突就小,所以時間就比較快。而mod小了,數組就小,空間小,相應的沖突大,所以時間就會慢,有的時候會超時。
線性勘測法
這里的線性勘測法就是有沖突的,下標就加一,如果還有,就繼續加一......
1 void add(int x) 2 { 3 int key=x%mod; 4 while(sum[key].x&&sum[key].x!=x)key=(key+1)%mod; 5 sum[key].x=x; 6 return ; 7 }
找的時候一樣
bool find(int x) { int key=x%mod; while(sum[key].x&&sum[key].x!=x)key=(key+1)%mod; if(!sum[key].x)return false; return true; }
所以這個時間復雜度還是很麻煩的,因為你會發現元素都是堆在一堆的,查找很慢,所以我們要跳來跳去
二次勘測法
這個用的就是平方跳跳跳,1^2,-1^2,2^2.......(S是平方數組)
void add(int x) { int key=x%mod; int i=1; while(sum[key].x&&sum[key].x!=x) { key=((key+S[i])%mod+mod)%mod; i++; } sum[key].x=x; }
1 bool find(int x) 2 { 3 int key=x%mod; 4 int i=1; 5 while(sum[key].x&&sum[key].x!=x) 6 { 7 key=((key+S[i])%mod+mod)%mod; 8 i++; 9 } 10 if(!sum[key].x)return false; 11 return true; 12 }
因為這種方法存在一直在跳循環,所以數組空間最好開大點......