【数据结构与算法】字符串匹配 - RK算法
RK(Rabin-Karp)算法是由Rabin
和Karp
两个大佬发明,RK算法的产生是因为发明者觉得BF算法那样每个字符一一比较太慢了,他们引入了哈希算法,哈希算法对主串中N - M + 1
个字符串分别求哈希值,然后逐个与模式串的哈希值进行比较。
与暴力算法使用一致的思想,通过滑动窗口(每次滑动一位)的方式计算N - M + 1
个子串的哈希值,依然以主串=AABABCDEF
,模式串=ABCD
为例,N = 9
, M = 4
,那么N - M + 1 = 6
个字串,这6个字串分别是:
有了上面这些字串的哈希值,之后就可以与模式串的哈希值hash(ABCD)
进行比较,如果哈希值相等,那么说明字符串可能匹配了。
通过上面的分析,我们就能得到一个大概的代码,如下:
public class Solution {
/**
* RK算法
*
* @param text 文本串
* @param pattern 模式串
* @return
*/
private static int rk(String text, String pattern) {
// 文本串长度
int N = text.length();
// 模式串长度
int M = pattern.length();
if (M > N) {
return -1;
}
// 模式串的hash值
int patternHash = hash(pattern);
for (int i = 0; i < N - M + 1; i++) {
// 获取与模式串等长的子串
String subText = text.substring(i, i + M);
// 获取子串的哈希值
int subTextHash = hash(subText);
if (subTextHash == patternHash) {
// 返回匹配的开始索引
return i;
}
}
return -1;
}
private static int hash(String text) {
// TODO获取字符串的哈希值
return 0;
}
}
在上面的代码中,通过循环加上截取字符串的方式肯定能获取到文本串中所有跟模式串长度一样的字串,然后通过hash算法,计算哈希值,与模式串的哈希值比较。
hash值如何实现呢?
比如模式串是ABCD
,它的哈希值如何算呢?我们可以将每个字符的Unicode编码值相加就是hash(“ABCD”)的值。
A
的Unicode编码值是65
B
的Unicode编码值是66
C
的Unicode编码值是67
D
的Unicode编码值是68
所有ABCD
的哈希值就是65 + 66 + 67 + 68 = 266
。
所有hash方法的实现如下:
private static int hash(String text) {
int hashcode = 0;
for (int i = 0; i < text.length(); i++) {
hashcode += text.charAt(i) - 'a';
}
return hashcode;
}
由此我们得到了如下完整的代码:
public class Solution {
/**
* RK算法
*
* @param text 文本串
* @param pattern 模式串
* @return
*/
public int rk(String text, String pattern) {
// 文本串长度
int N = text.length();
// 模式串长度
int M = pattern.length();
if (M > N) {
return -1;
}
// 模式串的hash值
int patternHash = hash(pattern);
for (int i = 0; i < N - M + 1; i++) {
// 获取与模式串等长的子串
String subText = text.substring(i, i + M);
// 获取子串的哈希值
int subTextHash = hash(subText);
if (subTextHash == patternHash) {
// 返回匹配的开始索引
return i;
}
}
return -1;
}
private static int hash(String text) {
int hashcode = 0;
for (int i = 0; i < text.length(); i++) {
hashcode += text.charAt(i);
}
return hashcode;
}
}
来测试下:
public static void main(String[] args) {
String text = "AABABCDEF";
String pattern = "ABCD";
System.out.println(new Solution().rk(text, pattern));
}
结果打印3
,结果是正确的,那么这个算法就是完全没有问题的了吗?其实不是,只是我们的测试用力不够而已,看如下用例,文本串是BACDABCDEF
, 模式串是ABCD
,运行看下结果:
public static void main(String[] args) {
String text = "BACDABCDEF";
String pattern = "ABCD";
System.out.println(new Solution().rk(text, pattern));
}
结果是0
!!!,这肯定是错的,应该是4
才对,为什么是0
呢,这是因为以0
开始,长度是M
的字符串是BACD
,模式串是ABCD
,它俩的哈希值是相等的都是266
,也就是出现了哈希冲突的问题,怎么解决呢?当出现哈希冲突的时候,再比较下字串和模式串是否相等即可。
public class Solution {
/**
* RK算法
*
* @param text 文本串
* @param pattern 模式串
* @return
*/
public int rk(String text, String pattern) {
// 文本串长度
int N = text.length();
// 模式串长度
int M = pattern.length();
if (M > N) {
return -1;
}
// 模式串的hash值
int patternHash = hash(pattern);
for (int i = 0; i < N - M + 1; i++) {
// 获取与模式串等长的子串
String subText = text.substring(i, i + M);
// 获取子串的哈希值
int subTextHash = hash(subText);
if (subTextHash == patternHash && subText.equals(pattern)) {
// 返回匹配的开始索引
return i;
}
}
return -1;
}
private static int hash(String text) {
int hashcode = 0;
for (int i = 0; i < text.length(); i++) {
hashcode += text.charAt(i);
}
return hashcode;
}
}
看这段代码if (subTextHash == patternHash && subText.equals(pattern)) {
,增加了字符串比较的逻辑。
重新再测试下哈希冲突的情况:
public static void main(String[] args) {
String text = "BACDABCDEF";
String pattern = "ABCD";
System.out.println(new Solution().rk(text, pattern)); // 4
}
上述代码还有可优化的地方,我们看下我们计算文本串中字串的哈希值的时候,
结果打印4
,正确。
哈希计算的方法有可以优化的地方,上面的算法中,每次都要计算整个字串中每个字符的编码值,然后累加,这里有优化点,先看下图:
比如hash(ABAB)
,可以通过上一次hash(AABA)
推出来,我们要知道字串的由来,它是由文本串每次向后移动一位得来的,那么头部肯定少了一位,尾部多了一位,所以**本次哈希值 = 上一次的哈希值 - 头部丢弃字符的哈希值 + 增加的字符的哈希值 **如下图所示:
这是典型的动态规划思想,修改下hash方法,完整代码如下:
public class Solution {
/**
* RK算法
*
* @param text 文本串
* @param pattern 模式串
* @return
*/
public int rk(String text, String pattern) {
// 文本串长度
int N = text.length();
// 模式串长度
int M = pattern.length();
if (M > N) {
return -1;
}
// 模式串的hash值
int patternHash = hash(pattern, 0, 0, M);
// 上一次计算的哈希值
int preHash = 0;
for (int i = 0; i < N - M + 1; i++) {
// 获取子串的哈希值
int subTextHash = hash(text, preHash, i, M);
preHash = subTextHash;
// 获取与模式串等长的子串
String subText = text.substring(i, i + M);
if (subTextHash == patternHash && subText.equals(pattern)) {
// 返回匹配的开始索引
return i;
}
}
return -1;
}
private static int hash(String text, int preHash, int startIndex, int M) {
if (startIndex == 0) {
int hashcode = 0;
for (int i = 0; i < M; i++) {
hashcode += text.charAt(i);
}
return hashcode;
}
return preHash - text.charAt(startIndex - 1) + text.charAt(startIndex + M - 1);
}
}
RK算法时间复杂度最好的情况是
O(1)
,最坏的情况下是O(NM)
。