1. KMP算法
代码随想录: 原文
力扣题目: 实现 strStr()
1.1 KMP算法介绍
1.1.1 算法名
算法取了三位学者发明的:Knuth,Morris和Pratt名字的首字母。所以叫做KMP
1.1.2 KMP算法用途
- KMP主要应用在字符串匹配上
- 当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了
1.1.3 前缀表
- next数组就是一个前缀表(prefix table)
- 前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配
- 前缀表记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀
1.1.4 最长相等前后缀
- 字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串
- 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
- 前缀表要求的是相同前后缀的长度
- 例如:字符串a的最长相等前后缀为0。 字符串aa的最长相等前后缀为1。 字符串aaa的最长相等前后缀为2
1.1.5 前缀表作用
- 前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力
1.1.6 计算前缀表
模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀
1.1.7 前缀表与next数组
next数组就可以是前缀表
很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组
这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)
1.1.8 时间复杂度分析
- n为文本串长度,m为模式串长度
- 匹配的过程是,之前还要单独生成next数组,时间复杂度是
- KMP算法的时间复杂度是的
- 暴力的解法显而易见是
1.2 构造next数组
定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串
void getNext(int* next, const string& s)
构造next数组(前缀表)步骤:
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
1.2.1 初始化
- 定义两个指针i和j,指向前缀末尾位置,指向后缀末尾位置
- 对next数组进行初始化赋值
int j = -1;
next[0] = j;
- j 初始化为 -1:前缀表要统一减一的操作的实现
- next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
- 所以初始化next[0] = j
1.2.2 处理前后缀不相同的情况
因为 j 初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较
遍历模式串s的循环下标i 要从 1开始
for (int i = 1; i < s.size(); i++) {
- 如果s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退
- next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。
- 那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
1.2.3 处理前后缀相同的情况
- 如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀
- 将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j;
1.2.4 整体构建next数组的函数代码
void getNext(int* next, const string& s){
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
1.3 使用next数组来做匹配
- 在文本串s里 找是否出现过模式串t
- 定义两个下标:j 指向模式串起始位置,i指向文本串起始位置
- j初始值为-1
i从0开始,遍历文本串
for (int i = 0; i < s.size(); i++)
s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较,s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置
while(j >= 0 && s[i] != t[j + 1]) {
j = next[j];
}
如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动
if (s[i] == t[j + 1]) {
j++; // i的增加在for循环里
}
如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了
那么使用next数组,用模式串匹配文本串的整体代码如下:
int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
return (i - t.size() + 1);
}
}
1.4 实现 strStr()题目的整体代码
前缀表统一减一
class Solution {
public:
void getNext(int* next, const string& s) {
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
};
2. 字符串总结
- 字符串是若干字符组成的有限序列
- 在C语言中,把一个字符串存入一个数组时,也把结束符 '\0’存入数组,并以此作为该字符串是否结束的标志
- 在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用’\0’来判断是否结束
- 打基础的时候,不要太迷恋于库函数
3. 双指针总结
- 双指针法在数组,链表和字符串中很常用
- 很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
- 当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章
- 双指针法是字符串处理的常客
学习时间:130min
评论区