Rabin-Karp——比KMP更简单的字符串匹配算法

Rabin Karp 算法简介

首先我们来回忆一下,关于字符串匹配的算法。
最容易理解最容易想到的算法,应该就是暴力匹配,时间复杂度是 O(n*m)。

假设存在 字符串 S, 需要在S中找到字串s,返回首次出现的下标。

  • 遍历S,时间复杂度是O(n).
  • 在S中截取s长度的字串,然后和s中的字符依次比较,时间复杂度是O(m).
  • 故 整个时间复杂度为 O(m*n).

现在我们提出一种假设,关于第二步(在S中截取s长度的字串,然后和s中的字符依次比较),时间复杂度是 O(m),我们能不能把它优化到 常数级别的时间复杂度呢?

办法是有的,假设在S中截取的字串为 tempS,那么如果我们将其进行哈希运算,将其映射成为一个数字,那么每个截取的 tempS 都可以转化为一个数字num(tempS),那么对题中要求的字串s,它自身也可以映射成为一个数字num(s),那么比较 tempS 和 s 的时候,直接比较 num(tempS) 和 num(s) 就可以,比较算法时间复杂度成功降低到 O(1)。

下面是一个具体的例子:
Rabin-Karp——比KMP更简单的字符串匹配算法_第1张图片

关于哈希函数num()

这里哈希函数的选择更多的取决于前人的经验,可以做到尽可能地减少碰撞。

Rabin-Karp——比KMP更简单的字符串匹配算法_第2张图片

  • 还有一个需要明确的点,尽管经过前人各种尝试,33是最佳的选择,但是还是可能会发生碰撞,也就是两个不相等的字符串,经过哈希函数的计算之后,两者映射到的数字是一样的,也就是说,两个相同的字符串映射到的数字一定是一样的,而两个不相同的字符串映射到的数字是有可能一样的,为了确保最后的比较结果,当两个字符串的映射数字相等,我们还需要再依次比较它们的各个字符,以确保这两个字符串是一定相等的。

关于字串映射数字的迭代

假设当前我们取了从主串 i 位置开始的子串,计算出其映射值,并不满足。
那么就需要向后移动一位,从 i + 1 的位置开始取字串,这个时候,字串的哈希映射值不应该重新计算,而是从之前的结果计算出来,具体如下图:

Rabin-Karp——比KMP更简单的字符串匹配算法_第3张图片

关于数字越界的问题

我们知道,当字符串长度过长的时候,如果采用上述的哈希函数,int类型是装不下的,可能会发生越界,这个时候我们需要采用 取余 的方式。
在 计算 哈希值 和 迭代计算 哈希值 的时候,最后我们可以通过对 10 ^6 、10 ^7 等进行取余运算,避免越界的问题。

代码示例

字符串匹配例题
Rabin-Karp——比KMP更简单的字符串匹配算法_第4张图片

class Solution {
public:
    /**
     * @param source: A source string
     * @param target: A target string
     * @return: An integer as index
     */
    int base = 1000000;
    int hashNumber = 33;
    int strStr2(string &source, string &target) {
        // write your code here
        if (target.length() > source.length()) {
            return -1;
        }
        if (target.length() == 0) {
            return 0;
        }
        // length ^ hashNumber
        int length = target.length();
        int power = 1;
        for (int i = 0; i < length; i++) {
            power = (power * hashNumber) % base;
        }

        // targetCode
        int targetCode = 0;
        for (int i = 0; i < length; i++) {
            targetCode = (targetCode * hashNumber + target[i]) % base;
        }

        // hashCode
        int hashCode = 0;
        for (int i = 0; i < source.length(); i++) {
            // abc + d
            hashCode = (hashCode * hashNumber + source[i]) % base;
            if (i < length - 1) {
                continue;
            }
            // abcd - a
            if (i >= length) {
                hashCode = hashCode - (power * source[i - length]) % base;
                if (hashCode < 0) {
                    hashCode += base;
                }
            }
            // double check the string
            if (hashCode == targetCode) {
                if (source.substr(i - length + 1, length) == target) {
                    return i - length + 1;
                }
            }
        }
        return -1;
    }
};

总结

Rabin-Karp 算法其实就是哈希表的应用,重点在于哈希运算,以及在各种情况下通过取余避免越界,通过将各个字符串映射成为数字,可以将字符串比较的时间复杂度降低O(m),整体的时间复杂度降低到O(m+n)。

你可能感兴趣的:(LeetCode刷题笔记,算法,哈希算法,散列表)