字符串哈希算法

一、字符串哈希:将一串字符串映射成一个整数,并用它来代替字符串进行比较。这样俩个字符串的比较就变成俩个整数的比较,可以将时间复杂度减少至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;
    }
};

七、其他题目练习

2156. 查找给定哈希值的子串

posted @ 2023-11-16 23:16  橙皮^-^  阅读(138)  评论(0)    收藏  举报