java实现字符串匹配之Rabin-Karp算法
前言
字符串匹配就是求一个子串在给定字符串的起始位置。我们先用暴力解法实现,然后在此基础上优化成Rabin-Karp算法。
暴力解法
public interface StringMatcher {
int indexOf(String source, String target);
}
/**
* 暴力解法
*/
public class BruteForceStringMatcher implements StringMatcher {
@Override
public int indexOf(String source, String target) {
if (target.length() == 0) {
return 0;
}
if (source.length() < target.length()) {
return -1;
}
char[] sourceArr = source.toCharArray();
char[] targetArr = target.toCharArray();
for (int i = 0; i < source.length(); i++) {
if (equals(sourceArr, i, i + targetArr.length - 1, targetArr)) {
return i;
}
}
return -1;
}
private boolean equals(char[] source, int start, int end, char[] target) {
if (end >= source.length) {
return false;
}
for (int i = 0; i <= end - start; i++) {
if (source[i + start] != target[i]) {
return false;
}
}
return true;
}
}
在主串中,检查起始位置分别是 0、1、2…n-m 且长度为 m 的 n-m+1 个子串,看有没有跟模式串匹配的。
Rabin-Karp算法
/**
* Rabin-Karp算法,将子串的比较转换成子串哈希值的比较
*/
public class RabinKarpStringMatcher implements StringMatcher {
/**
* 表示进制,参考go语言中Rabin-Karp算法实现中的值
*/
private static final int R = 16777619;
/**
* 哈希值可能太大,取模,随机值BigInteger.probablePrime(31, new Random())
*/
private static final long Q = 1538824213;
@Override
public int indexOf(String source, String target) {
int targetLen = target.length();
int sourceLen = source.length();
if (targetLen == 0) {
return 0;
}
if (sourceLen < targetLen) {
return -1;
}
long RM = initRM(target);
long targetHash = hash(target, 0, targetLen - 1);
int index = 0;
long sourceHash = 0;
while (index <= sourceLen - targetLen) {
// 开始比较
sourceHash = nextHash(source, target, index, sourceHash, RM);
if (sourceHash == targetHash) {
if (equals(source, index, index + targetLen - 1, target)) {
return index;
}
}
index++;
}
return -1;
}
private long nextHash(String source, String target, int index, long preHash, long RM) {
int targetLen = target.length();
if (index == 0) {
return hash(source, 0, targetLen - 1);
}
long hash = preHash;
// 去掉第一个字符的hash值
hash = mod(hash - mod(RM * source.charAt(index - 1)));
// 加上下一个字符的hash值
hash = mod(mod(hash * R) + source.charAt(index + targetLen - 1));
return hash;
}
private long hash(String str, int start, int end) {
long hash = 0;
for (int i = start; i <= end; i++) {
hash = mod(hash * R + str.charAt(i));
}
return hash;
}
private long mod(long hash) {
if (hash < 0) {
return hash + Q;
}
return hash % Q;
}
private long initRM(String target) {
long RM = 1;
for (int i = 1; i < target.length(); i++) {
RM = (R * RM) % Q;
}
return RM;
}
private boolean equals(String source, int start, int end, String target) {
if (end >= source.length()) {
return false;
}
for (int i = 0; i <= end - start; i++) {
if (source.charAt(i + start) != target.charAt(i)) {
return false;
}
}
return true;
}
}
暴力解法的瓶颈在于子串的比较,需要每个字符依次比较,Rabin-Karp算法将其转换成哈希值的比较,当然哈希值可能有冲突,在哈希值相等的情况下,还需要再进行一次字符串的朴素比较。关于Rabin-Karp算法的详细介绍,可以看 基础知识 - Rabin-Karp 算法 这篇博客,【go源码分析】strings.go 里的那些骚操作 这篇文章解释了为什么go语言没有显式的对hash值取模。
参考
基础知识 - Rabin-Karp 算法
子字符串查找----Rabin-Karp算法(基于散列)
Rabin-Karp算法github开源实现
Rabin-Karp在go语言中的实现
【go源码分析】strings.go 里的那些骚操作