Rabin-Karp 是对字符串进行hash返回一个数字(int, long, long long)的算法。
如果字符串只包含小写字符:
首先第一步,对每个字母进行编码,a-z 分别代表0-25.
int n = string s[i] - 'a'; //示例并非严谨代码
编号之后引入Rabin-Karp算法,实际上是个多进制转换算法在字符串中的应用,举例全是小写字母,所以进制a为26,再设为字符串的第i个字母对应上面的编码数字,那么有Rabin-Karp算法:
代码可以表示为:
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) :
代码:
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)的,如果比较的时候用这种方法则时间复杂度是的,用上面的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);
}