字符串hash编码方式 Rabin-Karp

Rabin-Karp 是对字符串进行hash返回一个数字(int, long, long long)的算法。

如果字符串只包含小写字符:

首先第一步,对每个字母进行编码,a-z 分别代表0-25. 

int n = string s[i] - 'a'; //示例并非严谨代码

编号之后引入Rabin-Karp算法,实际上是个多进制转换算法在字符串中的应用,举例全是小写字母,所以进制a为26,再设c_{i}为字符串的第i个字母对应上面的编码数字,那么有Rabin-Karp算法:

h_{0} = c_{0} a^{L-1} + c_{1} a^{L-2} +... +c_{i} a^{L-i-1} +...c_{L-1} a^{1} +c_{L} a^{0}

     = \sum_{L-1}^{i=0}c_{i}a^{L-i-1}

代码可以表示为:

long long h = 0;
long long al = 1;
for(int i = 0; i < win; i++){
    h = (cur_hash * a + nums[i]) % mod;
    al = (al * a) % mod;
}

其中num[i] 为已经编码好的字符串每个字符对应的数字列表。 

应用:

滑动窗口计算字符串等长度子串的hash值时,完成 O(n)时间复杂度 对当前窗口长度的全部子串进行编码:

思路: 先计算第一个窗口的字符串的Rabin-Karp编码值,后面每次滑动一次,移除当前窗口内的最高位,加入最低位,每一次编码的时间复杂度为O(1) :

h_{1} = h_{0} a - c_{0} a^{L} +c_{L+1}

代码:

cur_hash = (cur_hash * a - nums[i - 1] * al % mod + mod) % mod; // +mod 防止负数取余
cur_hash = (cur_hash + nums[i + win - 1]) % mod;

其中 mod 为取模数,防止溢出,且此取模数要考虑 “生日悖论” 问题,不能选取小于sqrt(定义域)的值,否则会很大概率出现碰撞。所以Rabin-Karp的返回数字类型需要考虑输入字符串的最大长度,设计取模数的大小避免碰撞,从而决定接收的数据类型。

实战题:LeetCode 1044 最长重复子串

此题二分不难想到,关键复杂度在查找该长度下是否存在重复子串算法上。

#include 
#include 
#include 
#include 

using namespace std;

typedef long long ll;

int Rabin_Karp(vector& nums, int win, long long mod, int a) 
{
    unordered_set st;
    ll cur_hash = 0;
    ll al = 1;
    for (int i = 0; i < win; i++){
        cur_hash = (cur_hash * a + nums[i]) % mod;
        al = (al * a) % mod;
    }
    st.insert(cur_hash);
    for (int i = 1; i < nums.size() - win + 1; i++) {
        cur_hash = (cur_hash * a - nums[i - 1] * al % mod + mod) % mod;
        cur_hash = (cur_hash + nums[i + win - 1]) % mod;
        if (st.find(cur_hash) != st.end()) {
            return i;
        }
        st.insert(cur_hash);
    }
    return -1;

}

string longestDupSubstring(string S) 
{
    int a = 26;
//    ll mod = (long)pow(2, 32);
    ll mod = 1000000000007;
    vector nums(S.size(), 0);
    for (int i = 0; i < S.size(); i++) {
        nums[i] = S[i] - 'a';
    }

    int l = 0;
    int r = S.size();
    int start = -1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        start = Rabin_Karp(nums, mid, mod, a);
        if(start == -1){
            r = mid - 1;
        } else{
            l = mid + 1;
        }
    }
    start = Rabin_Karp(nums, r, mod, a);
    return start == -1 ? "" : S.substr(start, r);
}

学会Rabin-Karp算法就可以尝试新方法解决一下字符串问题了。

例如:1392. 最长快乐前缀

这道题的大数据量可以让你充分认识到 string == string 这样的方式其实是O(n)的,如果比较的时候用这种方法则时间复杂度是O(n^{2})的,用上面的RK算法就能让比较复杂度变为O(1)。

#include 
#include 

using namespace std;

typedef long long ll;

string longestPrefix(string s) {
    if(s.size() <= 1) return "";
    ll mod = 1000000000007;
    ll al = 1;
    int a = 26;
    ll cur_front = 0;
    ll cur_back = 0;
    int l = s.size();
    int i = 0;
    int max_len = 0;
    for(;i

 

你可能感兴趣的:(字符串,算法,c++,哈希算法)