Rabin-Karp算法
解决的问题
Rabin-Karp算法,它是字符串快速查找的一种算法,为了检测s是否为t的子串及子串s在t中第一次出现的位置。
例如:s = “adc”; t = “agadcef”; s在t中第一次出现的位置就是2
如果用暴力匹配法的话时间复杂度是O(n∗m)
Rabin-Karp算法可以将字符串对比花费的时间O(m) 转化为O(1)。
算法思想
假设字符在是0-9的数字组成的,那么比较字符串 “123” 和字符串“456” 只需要比较两者的值是否相同即可知道字符串是否相同。
如果字符是小写字母, 相当于0-9的数字变成了0-25, 如果是ASCII 就是0-127
但是当数字过大,肯定会溢出,解决这个问题的办法就是取余,而取余了以后,余数不相等可以确定两个字符串一定不相等,如果余数相等则不一定相等。总结算法思想如下:
- 计算子串的hash值。(hash:散列函数,取模运算即为常用hash函数之一)
- 计算目标字符串中每个长度为【子串长度】的子串的hash值
- 比较hash值,如果相同这再次枚举对比每个字符, 如果不相同则必然不同直接跳过。
算法代码
例如: 在"abcde"中找”bcd“
- 计算”abc“的hash函数: 为:x = a ∗ 312 + b ∗ 311 + c ∗ 310
- x ∗ 31 + e - b ∗ 313 就计算出”bcd“的hash函数
- 比较hash值和目标子串的hash值,hash值相同就再比较一次字符串。
public int rabin_karp(String src, String target) {
if(src == null || target == null) return -1;
int sLen = src.length(), tLen = target.length();
if(tLen == 0) return 0;
int Mod = 1000000009; //比较大的质数,不容易出现冲突
int base = 31; //26个字母至少需要26进制映射,为了降低偶然性,选取质数31
//得到最高位要乘的值
long power = 1;
for(int i = 0; i < tLen; i++) {
power = (power * base) % Mod;
}
//计算目标子串hash值
long targetHash = 0;
for(int i = 0; i < tLen; i++) {
targetHash = (targetHash * base + target.charAt(i) - 'a') % Mod;
}
//滑动窗口求每个长度和target相同的子串hash
long curHash = 0;
for(int i = 0; i < sLen; i++) {
curHash = (curHash * base + src.charAt(i) - 'a') % Mod;
if(i < tLen - 1) continue;
if(i > tLen - 1){ //从第二个开始由第一个加后面的再减去最前面的
curHash = curHash - ((src.charAt(i - tLen) - 'a') * power) % Mod;
}
if(curHash < 0) curHash += Mod; //可能减成负数,就把之前mod掉的借回来
if(curHash == targetHash) {//hash相同
if(src.substring(i - tLen + 1, i + 1).equals(target)){
return i - tLen + 1;
}
}
}
return -1;
}
Rabin-Karp算法相关题目及题解
力扣28. 实现 strStr()
题目:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置。如果不存在,则返回 -1 。
思路: 直接用Rabin-Karp算法, 时间复杂度为O(n + m);
代码:
class Solution {
public int strStr(String haystack, String needle) {
int tLen = needle.length(), sLen = haystack.length();
if(tLen == 0) return 0;
int MOD = 1000000009, base = 31;
long power = 1;//最高位
for(int i = 0; i < tLen; i++) {
power = (power * base) % MOD;
}
//needle 的hash值
long needleCode = 0;
for(int i = 0; i < tLen; i++) {
needleCode = (needleCode * base + (needle.charAt(i) - 'a')) % MOD;
}
long curHashCode = 0;
for(int i = 0; i < sLen; i++) {
curHashCode = (curHashCode * base + (haystack.charAt(i) - 'a')) % MOD;
if(i < tLen - 1) continue;
if(i > tLen - 1) {
curHashCode = (curHashCode - (haystack.charAt(i - tLen) - 'a') * power) % MOD;
if(curHashCode < 0) curHashCode += MOD;
}
if(curHashCode == needleCode){
if(haystack.substring(i - tLen + 1, i + 1).equals(needle)) {
return i - tLen + 1;
}
}
}
return -1;
}
}
力扣686. 重复叠加字符串匹配
题目:给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。
思路: 同样用Rabin-Karp算法, 算当前hash值的时候循环算就好了,hash相等时,进行比较也循环比较。
需要注意的是:
- 要确定一个最大的重复叠加次数,用于终止循环,最大的叠加次数为:bLen / aLen + 2,即这样的情况:abc cabca
- hash相等,且子串相等时,返回值 (i + 1) / aLen + ((i + 1) % aLen==0?0:1)
由于i是从0开始的。 所有当前用到的长度是 i + 1, (i + 1) / aLen刚好为整数的时候,说明叠加这么多次刚刚好在最后一个匹配完成,如果有余数,则要多一次。
class Solution {
public int repeatedStringMatch(String a, String b) {
if(b.equals("")) return 0;
int aLen = a.length(), bLen = b.length();
long power = 1;
int MOD = 1000000009;
int base = 31;
long bHash = 0;
for(int i = 0; i < bLen; i++) {
power = (power * base) % MOD;
bHash = (bHash * base + b.charAt(i) - 'a')%MOD;
}
long curHash = 0;
int max = bLen / aLen + 2;
for(int i = 0; i < max * aLen; i++) {
curHash = (curHash * base + a.charAt(i % aLen) - 'a') % MOD;
if(i < bLen - 1) continue;
if(i > bLen - 1) {
curHash = (curHash - (a.charAt((i - bLen) % aLen)- 'a') * power) % MOD;
}
if(curHash < 0) curHash += MOD;
if(curHash == bHash) {
if(isMatch(a, b, i - bLen + 1)) return (i + 1) / aLen + ((i + 1) % aLen==0?0:1);
}
}
return -1;
}
public boolean isMatch(String a,String b,int k){
//判断a从下标k开始循环是否和b匹配
for(int i=0;i<b.length();i++){if(b.charAt(i)!=a.charAt((i+k)%a.length())){return false;}}
return true;
}
}
力扣.1044. 最长重复子串
题目:给你一个字符串 s ,考虑其所有 重复子串 :即:s 的连续子串,在 s 中出现 2 次或更多次。返回任意一个可能具有最长长度的重复子串
思路:
Rabin-Karp算法算出子串hash,并存起来,找到相同的hash值的子串再进行比较。确认子串相同。
①找最长的,最简单的方法是从n-1长度的子串开始找,然后找n-2长度的,但是这会很慢。
②如果长的子串满足,那么短的子串也肯定满足,即左侧都满足,右侧都不满足,所以我们可以用二分查找来找到最长满足情况的子串。
注意: hash相同再一一比较子串,比较麻烦不好实现,所以直接搞双hash进一步减少碰撞概率
代码:
class Solution {
int MOD1 = 1000000007,MOD2 = 1000000009;
int base1 = 31, base2 = 37;
int end = -1;//重复子串终止位标志。
public String longestDupSubstring(String s) {
int l = 0, r = s.length() - 1;
while(l < r) {
int mid = (l + r + 1) / 2; //向上取整,左边不动自动向右靠拢
if(hasDupString(s, mid)) {
l = mid;
} else {
r = mid - 1;
}
}
return end == -1?"":s.substring(end - l + 1,end + 1);
}
/**
* 判断长度为len的子串有没有重复的
*/
public boolean hasDupString(String s, int len) {
Set<Long> set1 = new HashSet<>();
Set<Long> set2 = new HashSet<>();
long power1 = 1, power2 = 1;//最高位权值
for(int i = 0; i < len; i++) {
power1 = (power1 * base1) % MOD1;
power2 = (power2 * base2) % MOD2;
}
//滑动窗口算hash值
long curHash1 = 0,curHash2 = 0;
for(int i = 0; i < s.length(); i++) {
curHash1 = (curHash1 * base1 + (s.charAt(i) - 'a')) % MOD1 ;
curHash2 = (curHash2 * base2 + (s.charAt(i) - 'a')) % MOD2;
if(i < len - 1) continue;
if(i > len - 1) {
curHash1 = (curHash1 - (s.charAt(i - len) - 'a') * power1) % MOD1;
if(curHash1 < 0) curHash1 += MOD1;
curHash2 = (curHash2 - (s.charAt(i - len) - 'a') * power2) % MOD2;
if(curHash2 < 0) curHash2 += MOD2;
}
if(set1.contains(curHash1) && set2.contains(curHash2)){
//start = i - len + 1;//起始位;
end = i;//终止位
return true;
}
set1.add(curHash1);
set2.add(curHash2);
}
return false;
}
}