题目地址:
https://leetcode.com/problems/implement-strstr/
给定两个字符串 s s s和 p p p,判断 p p p是否是 s s s的子串,如果是则返回匹配的起始下标,否则返回 − 1 -1 −1。
法1:暴力法。直接枚举所有的开始位置,依次做匹配,一旦匹配成功则返回起始点下标,如果一直未成功则返回 − 1 -1 −1。代码如下:
class Solution {
public:
int strStr(string s, string p) {
if (s.empty() && p.empty()) return 0;
if (p.empty()) return 0;
// i后面要留足p的长度这么多位置
for (int i = 0; i + p.length() - 1 < s.length(); i++) {
// 开始从p[0]开始匹配
int idx = 0;
while (idx < p.length()) {
if (s[i + idx] != p[idx]) break;
idx++;
}
// 匹配完了说明找到了子串,返回起始点下标
if (idx == p.length()) return i;
}
return -1;
}
};
时间复杂度 O ( l s l p ) O(l_sl_p) O(lslp),空间 O ( 1 ) O(1) O(1)。
法2:KMP。KMP算法有一个next数组的概念,首先参考https://blog.csdn.net/qq_46105170/article/details/106168535,在未改进版本的KMP中, p p p的next数组 n [ i ] n[i] n[i]表示 p [ 0 : i − 1 ] p[0:i-1] p[0:i−1]中最长的相等前后缀的长度,而在改进版本中, n [ i ] n[i] n[i]则表示当 p [ i ] p[i] p[i]与 s [ j ] s[j] s[j]不匹配的时候, p p p里下一个要与 s [ j ] s[j] s[j]匹配的字符的下标。未改进版本的KMP中,这个下标取的是最长的相等前后缀的长度,利用这个信息去移动 p p p,可以在不错过可能解的情况下,保证移动后和 s [ j ] s[j] s[j]比较时,之前的字符已经全部相等,也就是不用再进行匹配了;但是它还可以利用一个信息,若移动后与 s [ j ] s[j] s[j]对齐的那个字符仍然与 p [ i ] p[i] p[i]相等,那么肯定会出错,还是得继续向后移,改进的KMP算法就是将这个信息加入进去。首先,若 s [ j ] ≠ p [ i ] s[j]\ne p[i] s[j]=p[i],那么下一个与 s [ j ] s[j] s[j]对齐的字符变为了 p [ n [ i ] ] p[n[i]] p[n[i]],若还不等,则对齐的是 p [ n [ n [ i ] ] ] p[n[n[i]]] p[n[n[i]]],直到相等或者变为 − 1 -1 −1为止。如果我们已经知道了 p [ i ] = p [ n [ i ] ] p[i]=p[n[i]] p[i]=p[n[i]],那再次对齐就没有意义,因为还要取一次next。所以索性在一开始就”next到底“,杜绝 p [ i ] = p [ n [ i ] ] p[i]=p[n[i]] p[i]=p[n[i]]的情况发生。代码如下:
class Solution {
public:
int strStr(string s, string p) {
if (p.empty()) return 0;
// size返回的是unsigned,这里需要转为int以防出现奇怪的错误
int n = s.size(), m = p.size();
auto buildNext = [&] {
vector<int> ne(m, 0);
for (int i = 0, j = ne[0] = -1; i < m - 1;) {
if (j == -1 || p[i] == p[j]) {
i++;
j++;
// 在未改进的KMP算法中,ne[i]直接取j;
// 在改进的算法中需要判断一下当前字符是否和p[j]相等,如果相等则还需要ne一下
ne[i] = p[i] == p[j] ? ne[j] : j;
} else
j = ne[j];
}
return ne;
};
vector<int> ne = buildNext();
int i = 0, j = 0;
while (i < n && j < m) {
if (j == -1 || s[i] == p[j]) {
i++;
j++;
} else
j = ne[j];
}
return j == m ? i - j : -1;
}
};
时间复杂度 O ( l s + l p ) O(l_s+l_p) O(ls+lp),空间 O ( l p ) O(l_p) O(lp)。
法3:字符串哈希。采用前缀哈希的方式,先算出 p p p的哈希值,然后遍历 s s s,算出每 l p l_p lp的长度的子串的哈希值,与之比较,一旦比较相等则返回匹配起始位置。代码如下:
class Solution {
public:
using ull = unsigned long long;
int strStr(string s, string p) {
int n = s.size(), m = p.size();
if (n < m) return -1;
ull hash = 0, P = 131, hashP = 0, pow = 1;
for (int i = 0; i < m; i++) {
hashP = hashP * P + p[i];
pow *= P;
}
for (int i = 0; i < n; i++) {
if (i < m)
hash = hash * P + s[i];
else {
if (hash == hashP) return i - m;
hash = hash * P + s[i];
hash -= s[i - m] * pow;
}
}
return hash == hashP ? n - m : -1;
}
};
时间复杂度 O ( l s ) O(l_s) O(ls),空间 O ( 1 ) O(1) O(1)。
下面给出一个字符串下标从 1 1 1开始的KMP代码,C++:
class Solution {
public:
int strStr(string s, string p) {
int n = s.size(), m = p.size();
s = " " + s;
p = " " + p;
vector<int> ne(m + 1);
for (int i = 2, j = 0; i <= m; i++) {
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j++;
ne[i] = j;
}
for (int i = 1, j = 0; i <= n; i++) {
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j++;
if (j == m) return i - m;
}
return -1;
}
};
时空复杂度一样。