字符串哈希算法
一、字符串哈希:将一串字符串映射成一个整数,并用它来代替字符串进行比较。这样俩个字符串的比较就变成俩个整数的比较,可以将时间复杂度减少至O(1)
二、哈希函数:为了将字符串转化为整数,需要一个哈希函数hash,使得以下条件成立:如果字符串s == t 那么 hash(s) == hash(t)。一般情况下采用多项式 哈希函数构造:
对于一个长度为n的字符串s的hash值计算:
\[\begin{aligned}
hash(s[1..n])&= s[1]*p^{n-1} + s[2]*p^{n-2} + s[3]*p^{n-3} + ...+s[i]*p^{n-i} + s[n] \quad mode \quad m \\
&= \sum_{i=1}^{n}s[i]*p^{n-i} \quad mod \quad m
\end{aligned}
\]
为了减少哈希冲突,p需要选择一个素数,m需要选择一个足够大的数,因为俩个随机字符串碰撞的概率大约是1/m,这里使\(m=10^9 + 7\)
三、实现
long long compute_hash(string const& s) {
const int m = 1e9+7;
const int p = 1333; //如果出现冲突,p需要取更大的素数再测试
long long has_val = 0;
for(char c : s) {
has_val = (has_val*p + c - 'a') % m;
}
return has_val;
}
四、快速计算子字符串哈希
单次计算一个字符串的哈希值复杂度是O(n),其中n为串长度,按照多项式哈希公式,假设子字符串\(s[l, r]\),那么
\[\begin{aligned}
hash(s[l, r])&= s[l]*p^{r-l} + s[l+1]*p^{r-l-1}+...+s[r-1]*p + s[r]\\
hash(s[l,r])&=s[1]*p^{r-1} + s[2]*p^{r-2} + s[l-1]*p^{r-l+1} + s[l]*p^{r-l}+ ... + s[r] \\
& \quad -(s[1]*p^{l-1} + s[2]*p^{l-2} + ... + s[l-1])*p^{r-l+1}\\
&=\sum_{i=1}^rs[1]*p^{r-i} - \sum_{i=1}^{l-1}s[1]*p^{r-i}*p^{r-l+1}\\
&= hash[1, r] - hash[1, l-1]*p^{r-l+1}
\end{aligned}
\]
使用前缀数组\(h[i]\)记\(hash[1,i]\)的值,以及使用\(p[i]\) 记录\(p^{i}\)的值,那么上面的表达式可以由以下公式表达,计算子字符串的哈希值时间复杂度减少至O(1)
\[hash(s[l,r]) = h[r] - h[l-1]*p[r-l+1]
\]
五、应用题目
class Solution {
public:
vector<string> findRepeatedDnaSequences(string s) {
int n = s.size();
vector<long long> p(n+1);
vector<long long> h(n+1);
const int base = 123, mod = 1e+9;//实际测试base=123能过
p[0] = 1;
//构建字符串哈希前缀
for(int i = 1; i <= n; i++) {
h[i] = (h[i-1]*base + s[i-1]) % mod;
p[i] = (p[i-1]*base) % mod;
}
int len = 10;
unordered_map<long long, int> hash_count;
vector<string> ans;
for(int i = 1; i + len - 1 <= n; i++)
{
int j = i + len - 1;
int t = (h[j] - h[i-1]*p[j-i+1]) % mod;
int hash = (t + mod) % mod;
if(hash_count.count(hash) && hash_count[hash] == 1) {
ans.emplace_back(s.substr(i-1, len));
}
hash_count[hash]++;
}
return ans;
}
};
六、哈希冲突和自然溢出
- 第i次进行hash求值的时候,有\(M-i/M\)的概率不会发生碰撞,由此可以知道M的值越大,越不容易发生冲突
- 自然溢出法:利用了unsigned long long 这一基本类型在溢出的时候对\(2^{64}-1\)取模这一性质达到了取模的效果
- 187. 重复的DNA序列 自然溢出解法
class Solution {
public:
vector<string> findRepeatedDnaSequences(string s) {
int n = s.size();
vector<unsigned long long> p(n+1);
vector<unsigned long long> h(n+1);
int base = 13333; //当发生冲突时,选取更大素数尝试
p[0] = 1;
for(int i = 1; i <= n; i++) {
p[i] = p[i-1] * base;
h[i] = h[i-1] * base + s[i-1];
}
unordered_map<unsigned long long, int> hash_count;
vector<string> ans;
int len = 10;
for(int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
unsigned long long hash = h[j] - h[i-1]*p[j-i+1];
if(hash_count.count(hash) && hash_count[hash] == 1) {
ans.emplace_back(s.substr(i-1, len));
}
hash_count[hash]++;
}
return ans;
}
};
浙公网安备 33010602011771号