名称来由
RK 算法的全称叫 Rabin-Karp 算法. 它是由两位发明者 Rabin 和 Karp 的名字来命名的算法.
实现思路
BF算法的实现思路是对主串n
中的每一个连续子串n1
,都与模式串m
进行比较,该算法的复杂度为O(m*n)
RK算法是在BF算法的基础上做了一些改进:通过字符串hash值比较代替字符串的遍历比较
。这样能提交字符串的比较效率,但是并没有提高算法的时间复杂度,因为计算每一个连续子串n1的hash值,也是有时间消耗的,跟遍历比较的时间复杂度一样都是O(m),所有子串的hash值的计算时间复杂度为O(mn),那么这样整体的RK算法的时间复杂度还是O(mn),进一步的优化点可以放在hash值的计算上
hash值计算优化思路
如果子串n1
的hash值的计算,是通过子串中每个字符计算出的val
值做的累加,例如主串abcd
,那么子串有abc
、bcd
,假设abc
串的hash值为hash(abc)
,那么bcd
串的hash值的计算方式就是hash(bcd) = hash(abc) - val(a) + val(d)
,这样就可以减少没必要的hash值的重复计算,所有子串的hash值的计算时间复杂度为O(m + (n-m)) = O(n)
还有一个问题就是hash冲突,遇到hash冲突(不同的子串得到相同的hash值
)怎么办?
遇到hash冲突,则返回到字符串的遍历比较,来最终确定n1与m是否相同
时间复杂度
O(m+n)
伪代码实现
// ln : 主串的长度,lm:模式串的长度
// hn1 : 子串的hash值,hm:模式串的hash值
// val() : 字符hash值的计算方法
for (i = 0 ; i < lm ; i++) {
hn1 += val(n[i]);
hm += val(m[i]);
}
for (i = 0 ; i <= ln - lm ; i++) {
// 如果hash值相同,则返回去比较字符串
if (hn1 == hm) {
if (两个子串相同) {
return i;
}
}
// 计算下一个子串的hash值
hn1 += val(n[i + lm +1]);
hn1 -= val(n[i]);
}
golang实现
func strStr(haystack string, needle string) int {
needleLen := len(needle)
if needleLen == 0 {
return 0
}
hlen := len(haystack)
if hlen < needleLen {
return -1
}
// 计算第一个子串和模式串的hash值
subHaystackHash, needleHash, step, stepMax := 0, 0, 26, 1
for i := 0; i < needleLen; i++ {
subHaystackHash = (step*subHaystackHash + int(haystack[i]-'a'))
needleHash = (step*needleHash + int(needle[i]-'a'))
if i > 0 {
stepMax *= step
}
}
idx := -1
for i, _ := range haystack {
if i+needleLen > hlen {
break
}
// 如何hash值相同而且子串与模式串相同
if subHaystackHash == needleHash && isMatch(haystack, needle, i) {
idx = i
break
} else {
// 计算下一个子串hash值
subHaystackHash = (subHaystackHash-int(haystack[i]-'a')*stepMax)*step + int(haystack[i+needleLen]-'a')
continue
}
}
return idx
}
// 字符串匹配
func isMatch(haystack string, needle string, idx int) bool {
for i2, _ := range needle {
if idx+i2 >= len(haystack) || haystack[idx+i2] != needle[i2] {
return false
}
}
return true
}
总结
作为一个字符串匹配的算法,通过hash值预先匹配,从而加快匹配的效率,这确实是一个不错的解决思路,但是该算法的弊端也很明显,那就是需要设计一个可以匹配各类字符串的算法,这个并不容易实现,如果hash算法实现不好,算法的时间复杂度就会进行退化