Skip to content

Commit 297ae18

Browse files
committed
feat: add KMP strStr implemention
1 parent 3ea6874 commit 297ae18

File tree

3 files changed

+186
-2
lines changed

3 files changed

+186
-2
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"editor.formatOnSave": true,
8484
"editor.tabSize": 4,
8585
"cSpell.words": [
86+
"aabaabaafa",
8687
"binarytree",
8788
"editorconfig",
8889
"Heapify",

leetcode/string/implement_strstr

54.2 KB
Binary file not shown.

leetcode/string/implement_strstr.cpp

+185-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @Author: Chacha
33
* @Date: 2022-03-20 14:32:32
44
* @Last Modified by: Chacha
5-
* @Last Modified time: 2022-03-20 23:02:34
5+
* @Last Modified time: 2022-03-26 01:16:22
66
*/
77

88
/**
@@ -44,6 +44,8 @@ class Solution
4444
/* data */
4545
public:
4646
int strStr(string haystack, string needle);
47+
48+
void getNext(int *next, const string &s);
4749
};
4850

4951
/**
@@ -79,15 +81,196 @@ class Solution
7981
*
8082
* 为什么一定要用前缀表?
8183
*
84+
* 文本串: a a b a a b a a f a
85+
* 模式串: a a b a a f
86+
* 下标: 0 1 2 3 4 5
87+
*
88+
* 为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配?
89+
* 下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串 aa,
90+
* 因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面(即字符 f 之后),
91+
* 那么我们找到与其相同的前缀的后面从新匹配就可以了。所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
92+
*
93+
* 如何计算前缀表?
94+
* 注意:字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
95+
*
96+
* 如下:
97+
* 模式串: a a b a a f
98+
* 下标: 0 1 2 3 4 5
99+
* 前缀表: 0 1 0 1 2 0
100+
*
101+
* 长度为前1个字符的子串a,最长相同前后缀的长度为0
102+
* 长度为前2个字符的子串aa,最长相同前后缀的长度为1
103+
* 长度为前3个字符的子串aab,最长相同前后缀的长度为0
104+
* 长度为前4个字符的子串aaba,最长相同前后缀的长度为1
105+
* 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2
106+
* 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0
107+
*
108+
* 可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
109+
*
110+
* 文本串: a a b a a b a a f a
111+
* 模式串: a a b a a f
112+
* 下标: 0 1 2 3 4 5
113+
* 前缀表: 0 1 0 1 2 0
114+
*
115+
* 找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
116+
* 上述图中,前一个字符的前缀表的数值是2,所有把下标移动到下标2的位置继续匹配。
117+
*
118+
* 前缀表与next数组
119+
* 很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?
120+
* next数组可以就是前缀表,也可以是前缀表统一减1(右移一位,初始位置为-1)。
121+
*
122+
* 使用next数组来匹配
123+
* 以下我们以前缀表统一减一之后的next数组来做演示。有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。
124+
* 注意next数组是新前缀表(旧前缀表统一减1得来)
125+
*
126+
* 匹配过程如下:
127+
* i: 0 1 2 3 4 5 6 7 8 9
128+
* 文本串: a a b a a b a a f a
129+
* 模式串: a a b a a f
130+
* j+1: 0 1 2 3 4 5
131+
* 前缀表: 0 1 0 1 2 0
132+
* next[j]: -1 0 -1 0 1 0
133+
*
134+
* 时间复杂度分析
135+
* 其中 n 为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,
136+
* 可以看出匹配的过程是 O(n),之前还要单独生成next数组,时间复杂度是 O(m)。
137+
* 所以整个KMP算法的时间复杂度是 O(n+m) 的。暴力的解法显而易见是 O(n × m),
138+
* 所以KMP在字符串匹配中极大的提高的搜索的效率。
139+
*
140+
* 构造next数组
141+
* 我们定义一个 getNext 来构建 next 数组,函数参数为指向next数组的指针,和一个字符串。代码如下:
142+
* void getNext(int* next, const string& s)
143+
*
144+
* 构造next数组其实就是计算模式串s,前缀表的过程。主要有如下三个步骤:
145+
* 1. 初始化
146+
* 2. 处理前后缀不相同的情况
147+
* 3. 处理前后缀相同的情况
148+
*
149+
* 详解:
150+
* 1. 初始化:
151+
* 定义两个指针 i 和 j,j 指向前缀起始位置,i 指向后缀起始位置。然后还要对next数组进行初始化赋值,如下:
152+
* int j = -1; next[0] = j;
153+
* next[i] 表示 i(包括i)之前的最长相等的前后缀长度(其实就是 j)。
154+
*
155+
* next[i]: -1 0 -1 0 1 0
156+
* 模式串: a a b a a f
157+
* 下标: 0 1 2 3 4 5
158+
* ^ ^
159+
* j+1 i
160+
*
161+
* 2. 处理前后缀不相同的情况
162+
* 因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。所以遍历模式串s的循环下标 i 要从 1 开始,代码如下:
163+
* for (int i = 1; i < s.size(); i++) {}
164+
*
165+
* 如果 s[i] 与 s[j+1]不相同,也就是遇到前后缀末尾不相同的情况,就要向前回退。怎么回退呢?
166+
* next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。那么 s[i] 与 s[j+1] 不相同,
167+
* 就要找 j+1前一个元素在next数组里的值(就是next[j])。所以,处理前后缀不相同的情况代码如下:
168+
* while (j >= 0 && s[i] != s[j+1]) {
169+
* j = next[j]; // 向前回退
170+
* }
171+
*
172+
* 3. 处理前后缀相同的情况
173+
* 如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动 i 和 j 说明找到了相同的前后缀,
174+
* 同时还要将 j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。代码如下:
175+
* if (s[i] == s[j + 1]) { // 找到相同的前后缀
176+
* j++;
177+
* }
178+
* next[i] = j;
179+
*
180+
* 最后整体构建next数组的函数代码如下 void Solution::getNext(int *next, const string &s)
181+
*
182+
* 使用next数组来做匹配
183+
* 在文本串s里找是否出现过模式串 t。定义两个下标 j 指向模式串起始位置,i 指向文本串起始位置。
184+
* 那么 j 初始值依然为-1,为什么呢? 依然因为next数组里记录的起始位置为-1。i 就从 0 开始,遍历文本串,代码如下:
185+
* for (int i = 0; i < s.size(); i++)
186+
*
187+
* 接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。如果 s[i] 与 t[j + 1] 不相同,
188+
* j 就要从next数组里寻找下一个匹配的位置。代码如下:
189+
* while(j >= 0 && s[i] != t[j + 1]) {
190+
* j = next[j];
191+
* }
192+
*
193+
* 如果 s[i] 与 t[j + 1] 相同,那么 i 和 j 同时向后移动, 代码如下:
194+
* if (s[i] == t[j + 1]) {
195+
* j++; // i的增加在for循环里
196+
* }
197+
*
198+
* 如何判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。
199+
* 本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,
200+
* 就是文本串字符串中出现模式串的第一个位置。代码如下:
201+
* if (j == (t.size() - 1) ) {
202+
* return (i - t.size() + 1);
203+
* }
204+
*
205+
* 那么使用next数组,用模式串匹配文本串的整体代码如下:
82206
*
83207
*/
84208
int Solution::strStr(string haystack, string needle)
85209
{
210+
if (needle.size() == 0)
211+
{
212+
return 0;
213+
}
214+
215+
int next[needle.size()];
216+
217+
getNext(next, needle);
218+
219+
int j = -1;
220+
221+
for (int i = 0; i < haystack.size(); i++)
222+
{
223+
while (j >= 0 && haystack[i] != needle[j + 1])
224+
{
225+
// 如果不匹配,j 寻找之前匹配的位置
226+
j = next[j];
227+
}
228+
229+
if (haystack[i] == needle[j + 1])
230+
{
231+
// 如果匹配,j和i同时向后移动
232+
j++;
233+
}
234+
235+
if (j == needle.size() - 1)
236+
{
237+
// 文本串 haystack 里出现了模式串 needle
238+
return (i - needle.size() + 1);
239+
}
240+
}
241+
86242
return 0;
87243
}
88244

245+
void Solution::getNext(int *next, const string &s)
246+
{
247+
int j = -1;
248+
next[0] = j;
249+
250+
for (int i = 1; i < s.size(); i++)
251+
{
252+
while (j >= 0 && s[i] != s[j + 1])
253+
{
254+
// 如果前后缀不相同的,那么就向前回退
255+
j = next[j];
256+
}
257+
258+
if (s[i] == s[j + 1]) // 找到相同的前后缀
259+
{
260+
j++;
261+
}
262+
263+
next[i] = j; // 将 j(前缀的长度)赋给next[i]
264+
}
265+
}
266+
89267
int main(int argc, char const *argv[])
90268
{
91-
/* code */
269+
Solution s;
270+
string haystack = "aabaabaafa";
271+
string needle = "aabaaf";
272+
273+
cout << s.strStr(haystack, needle) << endl; // 输出 3
274+
92275
return 0;
93276
}

0 commit comments

Comments
 (0)