【字符串匹配】滚动哈希

LeetCode 28. Implement strStr()

题目描述

Implement strStr().

Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Clarification:

What should we return when needle is an empty string? This is a great question to ask during an interview.

For the purpose of this problem, we will return 0 when needle is an empty string. This is consistent to C's strstr() and Java's indexOf().

Example 1:

Input: haystack = "hello", needle = "ll"
Output: 2

Example 2:

Input: haystack = "aaaaa", needle = "bba"
Output: -1

Example 3:

Input: haystack = "", needle = ""
Output: 0

Constraints:

  • 0 <= haystack.length, needle.length <= 5 * 104
  • haystack and needle consist of only lower-case English characters.

解题思路

一道基础的字符串匹配的题目。

这道题设定的难度是 Easy,所以简单的两层循环暴力算法也能通过,事件复杂度 O(M*N)

但是这种算法显然时间复杂度比较高,如果设定为 Hard 的话就无法通过了。

我们希望能用的是线性时间的字符串匹配算法,常见的有 KMP、BM(Boyer Moore)、Sunday 算法等。KMP 算法是教科书上的经典算法,但是比较晦涩,手写记忆都比较麻烦,面试中几乎不会用到这一算法;BM 算法对 KMP 进行了改进,性能有数倍提升,文本编辑器和IDE中常用的查找功能就是基于BM算法。

有一种简单好记好理解的算法,是基于哈希对暴力算法的改进,这种算法叫 Rabin-Karp 算法,主要用于检测文章抄袭。两层循环的低效是因为每次移动一位,都需要从头重新比较两个串,所以开销是 M*N。这里我们通过一种策略,复用上一次的比较结果来进行这一次比较。这种办法的本质是把字符串看作一个 k 进制数,然后在滑动窗口里计算这个 k 进制数在窗口内部分的数值是否与待匹配值相等。这样每次比较的时间复杂度降低到 O(1),总时间降低到 O(M+N)

这里有一篇博客对以上5种字符串匹配算法进行了介绍和比较 字符串匹配常见算法(BF,RK,KMP,BM,Sunday)

参考代码

需要注意的点:

  • 乘法的溢出问题,步步取模
  • 哈希值用有符号整数,因为减法会导致出现负数
/*
 * @lc app=leetcode id=28 lang=cpp
 *
 * [28] Implement strStr()
 */

// @lc code=start
class Solution {
public:
    int64_t R_n(int R, int nl, int MOD) {
        int64_t Rn = 1;
        int64_t base = R;
        while(nl) {
            if (nl & 1) {
                Rn = (Rn * base) % MOD;
            }
            nl >>= 1;
            base = (base * base) % MOD;
        }
        return Rn;
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0) return 0; // !!
        int hl = haystack.size();
        int nl = needle.size();
        constexpr int MOD = 1e9+7;
        constexpr int R = 26;
        // const int64_t Rn = (int64_t)pow(R, nl) % MOD;
        const int64_t Rn = R_n(R, nl, MOD);
        int64_t ns = 0;
        int64_t hs = 0; // must be signed, for hs may < 0
        for (int i=0; i<nl; i++) {
            ns = (ns * R + (needle[i] - 'a')) % MOD;
            hs = (hs * R + (haystack[i] - 'a')) % MOD;
        }
        if (hs == ns) return 0;
        for (int i=nl; i<hl; i++) {
            hs = (hs * R + (haystack[i] - 'a') - (haystack[i-nl] - 'a') * Rn) % MOD;
            hs = (hs + MOD) % MOD;
            if (hs == ns) return i-nl+1;
        }
        return -1;
    }
};
// @lc code=end

扩展解读 Rabin-Karp 算法

实际上不只是右移一位,对于左移一位、左端或右端加长一位或是缩短一位的情况,RK 算法也能进行类似的处理。有 一个B站视频 专门讲 Rolling Hash 的,涉及到了以下几道题:

这篇 novoland.github.io 的博客 不仅介绍了 RK 算法,还讲解了 Java 中使用的 Hash 算法,以及 RK 算法的二位扩展。

彩蛋

常用于取模的质数:1e9+7, 19260817, 19491001 ……

posted @ 2021-04-24 11:56  与MPI做斗争  阅读(249)  评论(0编辑  收藏  举报