思想
- 设: n是主串长度,m是模式串长度
- RK算法如果想要实现,必须满足以下几个条件:
- 单个字符计算hash值的函数为
hc(c)
,复杂度为O(1) - 多个字符(c1,c2,c3,…,cn)计算hash值的函数设为
hstr(c1,c2,c3,...,cn) = hashhc(hc(c1),hc(c2),hc(c3),....,hc(cn))
,hashhc是通过各个字符的hash值来计算整个字符串的哈希值的函数,复杂度为O(n),且不太容易发生hash冲突 - 计算(c2,c2,c3,…,c(n+1))的hash值的函数为
hnext(hstr(c1,c2,c3,...,cn),hc(n+1))
,若hstr(c1,c2,c3,…,cn)已知则复杂度为O(1)
- 当设计一个hash算法,可以满足以上条件,则RK算法时间复杂度为O(n+m)
- 如果hash冲突异常频繁,最差会下降到O(n*m)
简易的hash值实现
hc(c)
= c的utf-8编码值hstr(c1,c2,c3,...,cn)
= hashhc(hc(c1),hc(c2),hc(c3),....,hc(cn))
= hc(c1)+hc(c2)+hc(c3)+...+hc(cn)
hnext(hstr(c1,c2,c3,...,cn),hc(n+1))
= hstr(c1,c2,c3,...,cn) - hc(c1) + hc(n+1)
java实现
/**
* RK算法:
*
* @author 18118224_周亿进
* @since 2020/4/1
*/
public class RabinKarpDemo {
public static void main(String[] args) {
System.out.println(indexOf("1234567890", "6789"));
}
static int indexOf(String str, String regex) {
return indexOf(str.toCharArray(), regex.toCharArray());
}
/**
* RK算法:
* 匹配模式串在主串中的位置
* <p>
* 这个算法的难度在于设计hash算法,
* 假如我们的字符集是自己定义的,只会26位 0-25各代表一个字母
* 那么我们完全可以将hash值作为一个26进制的数字,每一位代表一个字母,非常高效
* 但是如果我们字符集是utf-8那么就不能这么简单的去设计了.
* <p>
* 所以我们是无法完全避免hash冲突的问题,那么我们就需要将hash值计算得足够分散,这就要靠大佬了,我是不行
* <p>
* 这里要设计一个好的hash算法才能提升此算法的效率
* 好的hash算法会让hash冲突减少,如果hash冲突非常多,复杂度还是会上升到O(n*m)
* 如果没有hash冲突,则复杂度是O(n),因为只对每个字符(char)计算了一次hash就知道所有匹配串的hash值
*
* @param str 主串
* @param regex 模式串
* @return
*/
static int indexOf(char[] str, char[] regex) {
int n = str.length;//主串的长度设为n
int m = regex.length;//模式串的长度设为m
//1. 计算主串和模式串各个字符的hash值
int[] strCharsHashArray = hashArray(str);
int[] regexCharsHashArray = hashArray(regex);
//2. 计算模式串hash值
int regexHash = hashhc(regexCharsHashArray);
//3. 计算str第一个匹配位置的hash值 todo hash计算的算法会影响hash冲突的概率
int compareStrHash = hashhc(strCharsHashArray,0,m);
//4. 比较hash,i是str的下标
for (int i = 0; i < (n - m); i++) {
if (compareStrHash == regexHash) {
//5. 检查是否匹配上,排除hash冲突的情况
if (compare(str, regex, i)) {
//匹配上直接返回下标
return i;
}
}
//如果是最后一次,则不用计算了
if (i < n - m) {
//6. 计算后移一位的hash值 todo hash计算的算法会影响hash冲突的概率
compareStrHash = hnext(strCharsHashArray, compareStrHash, i, m);
}
}
return -1;
}
//计算下一位hash的方法
//strCharsHash主串上各个位置的hash值
//compareStrHash现在正在匹配的hash值
//i现在正在匹配的str,第一位字符的下标
//m模式串的长度
//返回的是模式串在主串上后移一位的候选字符串hash值
//时间复杂度O(1)
private static int hnext(int[] strCharsHash, int compareStrHash, int i, int m) {
return compareStrHash - strCharsHash[i] + strCharsHash[i + m];
}
//比较str从下标i开始和regex是否匹配上了,解决hash冲突的问题
private static boolean compare(char[] str, char[] regex, int i) {
for (int j = 0; j < regex.length; j++) {
if (str[i + j] != regex[j]) {
return false;
}
}
return true;
}
//start include
//end exclude
//已知主串上各个字符的hash值(charsHash)
//返回start~end位置字符串的hash值
static int hashhc(int[] hashArray, int start, int end) {
int hash = 0;
for (int i = start; i < end; i++) {
hash += hashArray[i];
}
return hash;
}
static int hashhc(int[] charsHash) {
return hashhc(charsHash, 0, charsHash.length);
}
//计算出每个字符的hash值
static int[] hashArray(char[] str) {
int length = str.length;
//先计算每个字符的hash值,先搞个简单设计
int[] charHash = new int[length];
for (int i = 0; i < length; i++) {
charHash[i] = hc(str[i]);
}
return charHash;
}
//字符计算hash值的简单实现
static int hc(char c) {
return c;
}
}