什么是Rabin-Karp算法
Rabin-Karp算法用来解决一个"简单"的问题: 在一个字符串中定位子串的位置.
同样的算法还有: KMP, BM. 关于他两可以再看看这位老哥写的文章:白话分析字符串匹配算法——KMP算法
乍一看这个问题一个for循环就能搞定, 但Rabin-Karp这个神奇的算法能让问题解决得更高效 有趣.
在了解Rabin-Karp之前,我们需要了解for暴力算法的缺点。
暴力算法的缺点
首先从头部开始匹配,如果没匹配到则向后移动一步再次匹配,如此循环。这样有什么缺点呢?
举个例子
父串是 bbbbc
子串是 bbc
如果是暴力匹配,需要移动2次才能找到子串,而在这个过程中,第2个b和第3个b都将会被对比3次。当子串更长的情况下, 重复对比问题将更为严重.
这样的重复对比肯定将造成浪费,所以有人就在想怎么不让它重复计算呢?
改善重复计算-滚动哈希
Michael O. Rabin和Richard M. Karp在1987年提出一个想法,即可以对模式串进行哈希运算并将其哈希值与文本中子串的哈希值进行比对。
而这个哈希需要能支持滚动哈希. 什么是滚动哈希呢? 后面再说.
在这里我们将哈希简单当做128进制的字符串转换为10进制表示.
先将bbc计算出一个哈希, 就像16进制一样, 只不过这次是128"进制"才能表示asc字符的哈希 : b * 128^2 + b * 128^1 + c = 1605632 + 12544 + 99 = 1618275
然后用这个数值与父串比较, 如何比较呢? 我们来依次模拟这个过程
-
先从头计算bbb的哈希: b * 128^2 + b * 128^1 + b = 1618274 与 bbc是不等的,那么就能得到bbb与bbc一定不等.
-
接下来往后移动一步: 还是bbb, 那是不是还得向上一步一样每个字符都去计算呢?这里就有一个巧妙的地方了: 我们可以利用上一步已经计算好的值来简化本次计算 。
首先将第一个b减去: - b * 128^2,
然后向后移动一位: * 128 ,
再加上最后一个b ,
即: (1618275 - b * 128^2) * 128 + b =1618274,
再与6447715(bbc)相比. 这种计算方式也叫"滚动哈希". 这样的比较方式与每个字符串比较的做 法相比, 计算量固定为-*+三个操作, 而不是子串长度的操作量. 所以当子串越长前者越有优势. -
我们在移动一步: (1618274 - b *128^2) * 128 + c = 1618275, 这时候就与1618274(bbc)相等了,就能判定找到了bbc.
不过该算法到这还没有结束, 因为最后一步的判断可能会产生误差.
我们假设一个字符π的asc码为0, 字符Δ的asc码为12643, 那么字符串 bπΔ 的计算值就为 b * 128^2 + 0 + 12643 = 1618275 与bbc的值相等. 而明显bπΔ与bbc是不同的字符串, 所以为了避免这个错误, 所以当计算到两个字符串值相等的时候, 还需要再判断一次.
这种情况被称为哈希冲突, 不过这个情况很少见, 只要保证"进制"是足够大的素数. 在Golang中这个取值是16777619.