目录
KMP算法理论
说在前面
先定义几个标识:
- 文本串:对应力扣中的haystack,长的那个字符串,遍历文本串本文使用指针 i
- 模式串:对应力扣中的needle,短的那个字符串,遍历模式串本文使用指针 j
- a needle in a haystack 在草垛中的针
首先得明白几个问题,带着这几个问题去理解KMP算法
- 什么是KMP算法?
- 用于字符串匹配的一种算法
- 为什么要用KMP算法?
- 一般来说,如果要看两个字符串匹配,使用的是暴力循环,从第一个字符开始匹配,如果不匹配就从第二个字符进行匹配。时间复杂度是O(n*m)
- 但是KMP算法可以不回退文本串的指针进行匹配,时间复杂度是O(n)
- KMP算法可以当出现字符串不匹配的情况的时候,可以知道一部分之前已经匹配的文本内容,利用这些信息避免"模式串"从头再去做匹配
- 怎么知道之前文本匹配过的内容,并准确找到位置呢?
- next数组
- next数组是什么? ——》是一个前缀表
- next数组用来干什么的? ——》 当出现字符匹配失败的时候,模式串可以跳过的匹配的字符个数
- KMP算法的基本思路?
- 当我们发现某个字符不匹配的时候,由于已经知道之前遍历过的字符,利用这些信息避免暴力算法中的“回退”步骤
- 最长相同前后缀?
-
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。 -
前缀表要求的就是相同前后缀的长度
-
KMP算法的思路
我们复盘暴力算法:是怎么进行匹配的?
从文本串的第一个字符开始进行比较,如果存在不符合的字符,就打断匹配,文本串指针回退到<本次开始匹配的后一位>(因为这一轮开始的已经不行了),重新模式串从模式串第一个字符进行匹配,直到模式串每个字符都可以匹配上了,再返回结果,时间复杂度O(n*m)
比如,下图中,i 指针指向文本串的4时,j 指向模式串的 4时,发现不匹配,i 就回退到 1 ,j就回退到 0,重新开始进行比较
那KMP算法会怎么做呢?
KMP算法在匹配失败的时候,会去看最后一个匹配的字符所对应的next数组,查看这个数,模式串的前几位(next数组对应的数)就不用再进行匹配了
以下图举例:也是 i 从0 开始,j 从 0 开始两个字符串进行第一轮匹配,两个指针都走到4的时候,字符不匹配,此时:模式串去查找最后一个匹配的字符对应的next数组,是2,那这代表着从文本串当前的位置开始,不再和模式串的前两个字符进行对比了,让指针 j 指向跳过模式串前两个字符的位置,而这个位置刚好是模式串的索引为2的位置,开始继续进行匹配
整个过程中 i 指针没有发生过回退,只有指针 j 进行回退,按照next数组进行回退
文本串的 i 永远不递减,这是KMP算法的精髓
所以接下来要做的就是:
- 了解为什么next数组可以做到这一点
- 怎么得到“模式串”的next数组
- 怎么使用next数组与“文本串”进行匹配
next数组(前缀表)
next数组代表了在匹配失败时,模式串中可以跳过匹配的字符个数,也是模式串的指针要回退到的索引位置
next数组具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力
为什么next数组可以做到这点?让模式串准确得跳过这么多字符串呢?
以下图为例:next数组的本质其实就是寻找模式串中“相同的前后缀的长度”,并且一定是最长的前后缀,而且前后缀不能是字符串本身。所以说不匹配的时候,跳过的字符是最长的前缀,因为上一轮最长后缀已经给你试过水了。也可以说这一轮跳过匹配的这几个字符在上一轮匹配过了。
模式串下标4之前的这部分的字符串(ABAB)的最长相等的前缀和后缀字符串 是子字符串AB,因为找到了最长相等的前后缀,匹配失败的位置就是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了(也就是从模式串的ABAB的最长前缀AB后面,下标2的地方开始就可以了)
模式串与next数组对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。代码随想录
指针 j 根据模式串已经有的next数组来进行回退,避免重复的运算,再想一下KMP算法的原理: KMP算法在匹配失败的时候,会去看最后一个匹配的字符所对应的next数组,查看这个数(保存了最长前后缀的长度),然后让模式串的前几位跳过匹配,就是说:下一轮,我看了最后这几位匹配的有这么几位(最长后缀),你再来匹配这几位(对应的最长前缀)就不用走啦
知道了为什么next数组可以做到精准回退,接下来就要看看怎么获得每个字符对应的最长相同前后缀?
需要的前提:
1、模式串s
2、next[]数组 -----> 用来存储对应的前后缀
要做三件事:
1、初始化
2、前后缀不相同,j指针回退
3、前后缀相同,更新next数组的值
首先:定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置(如下图所示),同时,j也代表最长前缀的长度
对指针和next数组进行初始化:
int j = 0;
next[0] = j; // 字符串第一个字符对应的最长前后缀长度为0
for (int i = 0; i < s.length(); i++) {
}
遇到前后缀不同的情况:
遇到前后缀不相同的时候,j 指针就找前一位需要回退的位置进行回退,而且回退到下一个目标的时候,如果不符合要求,要继续进行回退,所以这里应该使用while
while (j > 0 && s.charAt(j) != s.charAt(i)) {
j = next[j - 1];
}
遇到前后缀相同的情况:
遇到前后缀相同的情况,此时 i 对字符串的元素下标和对next数组的元素下标指针是对应的,所以先对 j 进行++, 赋值给 next[i] , 然后 i 再向后走(for循环自然++)
if (s.charAt(j) == s.charAt(i)) {
j++;
}
next[i] = j;
总结在一起:
public void getNext(int[] next,String s) {
// 初始化
int j = 0;
next[0] = 0;
for (int i = 1; i < s.length(); i++) {
// 前后缀不相同的情况
while (j > 0 && s.charAt(j) != s.charAt(i)) {
j = next[j - 1];
}
// 前后缀相同的情况
if (s.charAt(j) == s.charAt(i)) {
j++;
}
// 对next数组进行赋值
next[i] = j;
}
}
使用next数据进行匹配
上面得到了next数组,那怎么使用next数组进行匹配呢?
定义两个下标,j 指向模式串起始位置;i 指向文本串起始位置,开始遍历文本串
int j = 0; // 因为next数组里记录的起始位置是0
for (int i = 0; i < haystack.length(); i++) {
}
开始对needle.charAt(j)和haystack.charAt(i)进行比较,如果needle.charAt(j) != haystack.charAt(i),j就要从next数组里寻找下一个匹配的位置
while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
j = next[j - 1];
}
如果needle.charAt(j) == haystack.charAt(i),i 和 j 同时向后移动
if (needle.charAt(j) == haystack.charAt(i)) {
j++; // i在for循环里面增加
}
怎么判断文本串中出现了字符串呢?
if (j == needle.length()) {
return i - needle.length() + 1;
}
总结在一起:
public int strStr(String haystack, String needle) {
// 使用next数据进行匹配
if (needle.length() == 0) {
return 0;
}
// 创建next数组
int[] next = new int[needle.length()];
// 调用方法填充next数组
getNext(next,needle);
int j = 0; // 因为next数组里记录的起始位置为0
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
j = next[j - 1];
}
if (needle.charAt(j) == haystack.charAt(i)) {
j++;
}
if (j == needle.length()) {
return i - needle.length() + 1;
}
}
return -1;
}
参考资料:
28. 实现 strStr()
题目链接:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/
思路
详见KMP算法理论
实现 strStr()
暴力算法
可以让字符串needle每次从haystack中得第i个字符进行匹配,所以遍历haystack,限制条件是从haystack中得第i个字符开始后面得长度还要够字符串needle的长度,要不然就没有意义了
然后开始对needle进行匹配,如果出现不匹配的情况,就将flag设置成false,再打断循环,因为出了循环要看看匹配的这个字符串有没有出现不匹配的情况
class Solution {
public int strStr(String haystack, String needle) {
int x = haystack.length();
int y = needle.length();
// 遍历haystack字符串
for (int i = 0; i + y <= x; i++) {
boolean flag = true;
for (int j = 0; j < y; j++) {
if (haystack.charAt(i + j) != needle.charAt(j)) {
flag = false;
break;
}
}
if (flag) {
return i;
}
}
return -1;
}
}
KMP算法
class Solution {
public int strStr(String haystack, String needle) {
// 使用next数据进行匹配
if (needle.length() == 0) {
return 0;
}
// 创建next数组
int[] next = new int[needle.length()];
// 调用方法填充next数组
getNext(next,needle);
int j = 0; // 因为next数组里记录的起始位置为0
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
j = next[j - 1];
}
if (needle.charAt(j) == haystack.charAt(i)) {
j++;
}
if (j == needle.length()) {
return i - needle.length() + 1;
}
}
return -1;
}
// 构造needle的前缀表(next数组)
public void getNext(int[] next,String s) {
// 初始化
int j = 0;
next[0] = 0;
for (int i = 1; i < s.length(); i++) {
// 前后缀不相同的情况
while (j > 0 && s.charAt(j) != s.charAt(i)) {
j = next[j - 1];
}
// 前后缀相同的情况
if (s.charAt(j) == s.charAt(i)) {
j++;
}
// 对next数组进行赋值
next[i] = j;
}
}
}
459.重复的子字符串
题目链接:https://leetcode.cn/problems/repeated-substring-pattern/
思路
看了一下题解,还没详细拆解
重复的子字符串
移动匹配
KMP算法
字符串匹配算法
常见的字符串匹配算法包括
- 暴力匹配
- Knuth-Morris-Pratt 算法
- Boyer-Moore 算法
- Sunday 算法