字符串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 <unordered_set>
#include <vector>
#include <math.h>
#include <iostream>

using namespace std;

typedef long long ll;

int Rabin_Karp(vector<int>& nums, int win, long long mod, int a) 
{
    unordered_set<ll> 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<int> 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 <string>
#include <iostream>

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<s.size() - 1; i++) {
        cur_front = (cur_front * a + (s[i] - 'a')) % mod;
        cur_back = (cur_back + ((s[l - i - 1] - 'a') * al) % mod) % mod;
        if(cur_back == cur_front) {
            max_len = i + 1;
        }
        al = (al * a) % mod;
    }
    if(max_len == 0) return "";
    return s.substr(0, max_len);
}

 

Rabin-Karp算法是一种用于字符串模式匹配的快速搜索算法,它基于哈希函数的思想,可以在文本串中查找指定的模式串。在Java中实现Rabin-Karp算法,你需要做以下几个步骤: 1. **计算哈希值**: - 对模式串(pattern)和文本串(text)计算各自的哈希值。通常会取每个字符的ASCII值(或其他合适的方式,如取模运算),然后乘以一定的质数并加上前一个字符的hash值。 ```java int[] patternHash = new int[pattern.length() + 1]; for (int i = 0; i < pattern.length(); i++) { patternHash[i] = (pattern.charAt(i) & 0xff) % prime; if (i > 0) patternHash[i] += prime * patternHash[i - 1]; } ``` 2. **构造文本哈希表**: - 对于文本串的每一个滑动窗口,计算其哈希值,并存储在一个数组或HashMap中。 3. **比较哈希值**: - 比较模式串的当前哈希值和文本哈希表中的对应值,如果相等,则进一步进行精确匹配;如果不相等,则继续移动模式串到下一个位置,更新哈希值。 4. **精确匹配**: - 如果哈希值相等,在模式串长度内逐字符比对,检查是否完全匹配。 以下是简单的Java代码示例: ```java public static boolean search(String text, String pattern, int prime) { int patternLen = pattern.length(); int hashText = 0, hashPattern = 0, w = 0; for (int i = 0; i < patternLen; i++) { hashPattern = (hashPattern * prime + pattern.charAt(i)) % prime; w = (w * prime + text.charAt(i)) % prime; } for (int i = 0; i <= text.length() - patternLen; i++) { if (hashPattern == w && text.substring(i, i + patternLen).equals(pattern)) { return true; } if (i < text.length() - patternLen) w = (w * prime + text.charAt(i + patternLen)) % prime; else w = hashText; hashText = (hashText * prime + text.charAt(i)) % prime; } return false; } ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值