目录
1.代码实例
废话不多说先上题目和代码
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。(这是力扣上的第28道题,想要了解的,可以去看一下)(next数组创建j=next(j-1)这一步我写错成j=next(j)了,记得改一下
//给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项
//的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
int strStr(char* haystack, char* needle)
{
int n = strlen(haystack), m = strlen(needle);
int next[m];//创建一个next数组用来记录needle每一个元素的最大公共前后缀
next[0]=0;//因为needle的第一个元素前面没有任何数,所以直接把它相对应的next[0]赋值为0
int j=0,i=1;//初始化一个变量j,表示已匹配的前缀后缀的长度。i代表需要比较的元素
while(i<m)
{
//如果当前位置i和j上的字符相等,说明找到了更长的前缀后缀匹配,
//将j+1赋给next[i],然后将i和j都向后移动一位。
if(needle[i]==needle[j])
{
next[i]=j+1;
j++;
i++;
}
else{
if(j==0){
next[i]=0;//如果模式串在位置i和j的字符不相等,并且j等于0,将next[i]设置为0。
i++;
}else{
j=next[j];//如果模式串在位置i和j的字符不相等,并且j不等于0,将j更新为next[j],直到找到一个匹配或者j等于0。
}
}
}
//我知道你们难受的是如何创建next数组,所以下面的使用我就不过多介绍了
i=0;j=0;
while(i<n&&j<m)
{
if(needle[j]==haystack[i]){
i++;
j++;
}
else{
j=next[j-1];
}
}
if(j==m)return i-j;
else return -1;
}
2.KMP算法的大致思路
这里需要提到一下BF算法(暴力求解),能更容易让你们理解KMP算法的优势在哪里
我们用i来代表文本串的比较位置,j来代表模式串的比较位置。
首先先来看一下BF的设计思路(当然你们都会,但我们再了解一下,方便之后与KMP比较):
逐个比较文本串和模式串的字符,如果不匹配,则将模式串右移一位,再重新开始比较。
简单写一下代码就是
while(i<n&&j<m)
{
if(haystack[i]==needle[j]){i++;j++}//若匹配成功,则把i和j移向下一位
else{//此处为两个算法最大的不同
i=i-j+1;//需要回溯到字串的下一个字符,j代表移动的次数,i-(j)代表i原来的位置,最后加1,移向下一个位置
j=0;//j回溯到最开始的位置
}
}
而kmp算法改变的就是其中i和j回溯时的位置,而核心创建next数组就是j(除去可以跳过的)回溯的位置:j=0+next[j-1];
i这是有两种改变:要么移向下一个字符(第一种选择),要么保持不动(第二种)
while(i<n&&j<m)
{
if(haystack[i]==needle[j]){i++;j++;}
else{
j=next[j-1];
}
}
现在是不是感觉next超神奇,他怎么就能记录j需要回溯的位置呢?既然看到这里了,说明你并不满足上面的代码,这里只介绍大致思路,往下看吧。
3.KMP算法的实现
对我来说(你们也可以这样理解)KMP算法的实现我们可以分为两种:
1.构建next数组(根据模式串,也就是需要比较的那个小的字符串)
2.如何使用next数组(使用并理解j=next(j)这一关键步骤)
(0)理解next数组和最大公共前后缀
我们先来介绍最大公共前后缀:
最大公共前后缀指的是在一个字符串中同时作为前缀和后缀的最长子串。换句话说,最大公共前后缀是字符串中既是前缀又是后缀的最长连续子串。
1.前缀的开始必须是第一个字符,后缀的结尾必须是最后一个字符
2.比较的前缀和后缀和必须相同
3.比较的前后缀不能是本身
举几个例子来说明最大公共前后缀:
-
字符串:"abcdabc"
无最大公共前后缀 -
字符串:"ababab"
最大公共前后缀:"abab" -
字符串:"abcdeabc"
最大公共前后缀:"abc" -
字符串:"aaaaa"
最大公共前后缀:"aaaa"
(a也是公共前后缀,但不是最大的,aa,aaa同理)
next数组的每个元素都代表了模式串中相应位置的最长前缀后缀匹配长度。
举个例子(我们确定第一个next[0]=0;因为字符串是以下标0开始存储的)
经过这三个例子,相信你已经能“强硬的”算出next的值了,接下来教你如何让系统自己求出next
接下来就是算法理解的核心:我们现在已经知道了j回溯的位置是其对应的最大公共前后缀,那么为什么就能让j从0跳过这个数字呢?(现在不考虑next数组怎么生成,直接看用途)
我们先举一个例子:
当kmp算法匹配失败的时候,会去看模式串最后一个匹配的字符它对应的next值是多少,我们就把j原本回溯的位置加上几,即j跳过了几个元素(j原本回溯的位置是最开始0,kmp算法就是让他跳过了一些不必要回溯的位置)
就比如:这里的a与c不匹配时,我们要看前一个字符b对应的next数组的值,next值是2,我们就是j=0+2=2(j最开始的位置一直就是0)
所以就变成了这样
注意:现在j的位置在字串下标为2的位置,就是第一个黄色部分(a),i不回溯,所以第一个黄色部分
(当我们判断出当前匹配不成功时,我们才会用next数组)现在就代表i与j前面的每一个字符都相等,而i与j对应的字符不相等。
所以主串(i之前的串)的公共前后缀不就是字串(j之前)的公共后缀嘛,(如果你还没理解,就跳出来看“abab”和“abab”这两个字符串一模一样,所以有一样的公共前后缀)
1.主串(i之前的串)的公共前缀=其公共后缀=字串(j之前的)的公共前缀,就代表已有信息:主串(i之前)的后缀=字串(j之前)的前缀 图中绿色的部分
2.而这个信息又可以等价于:主串(跳过后)的前缀(图中的ab)=字串的前缀(ab) 这就相当于j没必要从0开始,而是从已经匹配过(红字部分)的下一位置开始
而我们要找的当然是j跳过的最大的位置,那么就是找最大公共前后缀,现在再把这个词替换掉上面讲述中的每一个公共前后缀,试着理解一下。
(1)next数组的创建
你当然可以自己暴力求解出next的每个元素,但那样不就本末倒置了,关于next的创建我们一般有两种算法:迭代和递归。这里我们只讲述如何递归
我们创建next数组是根据模式串来创建的
1.第一个字符绝对没有最长公共前后缀(不能是本身),所以直接赋值next[0]=0
2.初始化两个变量i
和j
,分别表示主串当前计算的位置和模式串前缀的结束位置,初始值为1。
3.进入循环,直到i
等于模式串的长度:
(1)如果j
为0或者当前位置的字符与前缀的最后一个字符相等(即pattern[i] ==pattern[j]
),则将i
和j
分别增加1,并将next[i]
的值设置为j
。
(2) 如果当前位置的字符与前缀的最后一个字符不相等,并且j
不为0,则将j
更新为next[j]
。这一步(因为是while循环)实际上是在模式串中寻找更短的相同前缀后缀(可以参考(0)中的讲解)。直到找到短的公共前后缀:比较前缀的后一个字符和后缀的即将比较的字符时是否相等,相等的话,next[i]=这个短的公共前后缀+1;如果直到=0还没有找到,再分为两种分别对应(1)(3)。
(3)如果j
为0并且当前位置的字符与前缀的最后一个字符不相等,则将i
增加1(因为以i指向的这个字符的作为前缀的字符串没有与j中的任意一个前缀匹配,i移向下一位),并将next[i]
的值设置为0,表示当前位置没有相同的前缀后缀。
(2)如何使用next完成匹配
如果要详细的步骤去看3(0)中(这里也只说思路):
当kmp算法匹配失败的时候,会去看模式串最后一个匹配的字符它对应的next值是多少,我们就把j原本回溯的位置加上几,即j跳过了几个元素(j原本回溯的位置是最开始0,kmp算法就是让他跳过了一些不必要回溯的位置),变现成公式就是j=next[j-1].
4.一些题外话
1.
本人在理解kmp时的特殊想法(方便理解,如果接受不了,请抛弃)
在构建next数组时
(1)j代表的不仅仅是模式串,也代表了已经匹配的成功的字符数量,所以才能在赋值时:(字符串i位置==字符串j)next[i]=j+1;(已经匹配的数量加上这次匹配成功的1次)
(2)在每记录一次next数组的之后,i都会向后移一位;换言之i向后移动一位的条件就是next[i]已经被赋值了。
2.
本人感觉kmp算法最难的地方就是next的创建,如果你只是想使用这个算法,那么其实就只用将2、3(0)(1)看完就行了,因为它的其他部分与bf(你们都会)类似
3.
模式串就是子串,主串就是文本串。
4.
因为关于next创建我没有给出例子,主要是这里画表格太难了,我现在给你们文章描述:
现在模式串是AABAABAAA
- 初始化
next
数组。直接定义next[0]=0; - 设置
i = 1
,j = 0
。 - 比较needle
[j]
和needle[i]
:因为相等,且j
为0,所以next[i]
的值设置为1,将i
增加1,j也增加1,即next = [0, 1]
。i:2,j:1. -
- 比较needle
[i]
和neelde[j]
:因为不相等,且j不为0,所以j=next[j]=1
i:2,j:1 - .比较needle
[i]
和neelde[j]
:因为不相等,且j不为0,所以j=next[j]=0 i:2;j:0
- 比较needle
[i]
和neelde[j]
:因为不相等,且j为0,所以next[i]=0,i++. 即next=[0,1,0] i:3;j:0
- 比较needle
- 比较needle
[i]
和needle[j]
:因为相等,next[i]
的值设置为j+1
,将i
和j
分别增加1,即next[3] = 1
,即next = [0, 1, 0, 1]
。i:4;j:1 - 比较needel
[i]
和needle[j]:
因为相等,next[i]
的值设置为j
,将i
和j
分别增加1,即next[4] = 2
,即next = [0, 1, 0, 1, 2 ]
。i:5;j:2 - 比较
needel[i]
和needle[j]
:因为相等,next[i]
的值设置为j
,将i
和j
分别增加1,即next[5] = 3
,即next = [0, 1, 0, 1, 2, 3]
。i:6;j:3 - 比较
needle[i]
和neelde[j]
:因为相等,next[i]
的值设置为j
,将i
和j
分别增加1,即next[6] = 4
,即next = [0, 1, 0, 1, 2, 3, 4]
。i:7;j:4 - 比较
needle[i]
和neelde[j]
:因为相等,next[i]
的值设置为j
,将i
和j
分别增加1,即next[7] = 5
,即next = [0, 1, 0, 1, 2, 3, 4, 5]
。i:8;j:5 - 以上所有比较的过程全是在循环里进行的:循环结束的条件是i>=m(m是needle的长度)
5.我知道你们有的人挺疑惑,为什么看的有的视频next[0]赋值为-1,next[1]为0;有的确实直接从next[0]为0开始,这主要是他们模式串对应的公共前后缀整体前移了一位(不再是本身了),这两种区别就是在用的时候j=next[j-1]另一种则是j=next[j],当然在构建next数组中也是这样。本人感觉这种比较好理解才选则这种的,另一种用的时候好理解,但是构建数组的时候不容易理解
6.理解透彻真的好累!你们看都这篇文章说明也是刚刚学到,如果不能理解:掌握next数组算法后先放弃,说不定以后的某个时刻你突然理解了,而不是像我浪费了这么长时间。