【字符串匹配】滚动哈希
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 的,涉及到了以下几道题:
- LeetCode 1044. Longest Duplicate Substring
- LeetCode 1062. Longest Repeating Substring【需解锁】
- LeetCode 1392. Longest Happy Prefix
- LeetCode 1316. Distinct Echo Substrings
- LeetCode 214. Shortest Palindrome
- LeetCode 718. Maximum Length of Repeated Subarray
- LeetCode 1147. Longest Chunked Palindrome Decomposition
这篇 novoland.github.io 的博客 不仅介绍了 RK 算法,还讲解了 Java 中使用的 Hash 算法,以及 RK 算法的二位扩展。
彩蛋
常用于取模的质数:1e9+7, 19260817, 19491001 ……