KMP算法
KMP算法的作用
KMP主要应用在字符串匹配上,在一个已知字符串中查找子串的位置。
前缀表
比如要在文本串:aabaabaaf
中查找是否出现过一个模式串:aabaaf
如果要用暴力解法,会有两个for循环分别遍历两个字符串进行匹配:
当遍历到文本串中第六个字符b和模式串的第六个字符f时发现不匹配,会将模式串整体向后移动一位开始匹配:
直到模式串移动到能够匹配上文本串的位置:
但KMP算法不会从头移动模式串,而是从上次已经匹配的内容开始匹配,会找到模式串中第三个字符b继续开始匹配:
KMP算法通过前缀表能够知道之前匹配过并跳到那个已经匹配过的内容的后面继续开始匹配。
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
前缀表会记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
前缀与后缀
要知道前缀表是如何找到下一步匹配中模式串的匹配位置,就要先了解前缀、后缀、最长相等前后缀的概念。
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
以aabaaf
为例,a、aa、aab、aaba、aabaa
是前缀,aabaaf
不是前缀(不包含尾字符)
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
以aabaaf
为例,f、af、aaf、baaf、abaaf
是后缀,aabaaf
不是后缀(不包含首字符)
最长相等的前缀和后缀的长度,即最长相等前后缀
以a
为例,没有前缀和后缀,最长相等前后缀为0;
以aa
为例,前缀为a
,后缀为a
,最长相等前后缀长度为1;
以aab
为例,前缀为a、aa
,后缀为b、ab
,没有相等的前后缀,最长相等前后缀长度为0;
以aaba
为例,前缀为a、aa、aab
,后缀为a、ba、aba
,最长相等前后缀长度为1;
以aabaa
为例,前缀为a、aa、aab、aaba
,后缀为a、aa、baa、abaa
,最长相等前后缀长度为2;
以aabaaf
为例,前缀为a、aa、aab、aabaa
,后缀为f、af、aaf、baaf、abaaf
,没有前缀和后缀,最长相等前后缀为0;
因此就得到了一个关于最长相等前后缀的序列0、1、0、1、2、0
,即为aabaaf
的前缀表。
使用前缀表进行匹配
当进行到b
和f
不匹配时,就要找f
前面的子串aabaa
的最长相等前后缀,根据前缀表可得是2。
由最长相等前后缀2可得,有一个后缀aa,前面也有一个与其相等的前缀aa,在后缀的后面发生了不匹配。
那么就要找与后缀相等的前缀的后面继续匹配,下标即为字符串aabaa的最长相等前后缀的长度
这里不理解的可以推一下:
根据最长相等前后缀2,可以找到后缀aa和前缀aa(即与后缀相等的前缀),而前缀aa的后边就是b,在字符串aabaa中下标即为2。
利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置:
next数组
next数组就可以是前缀表,但是很多实现都是把前缀表减一(或者右移一位,初始位置为-1)之后作为next数组。
前缀表整体减一和整体右移的效果是一样的,初始位置都为-1,后面要么减一要么右移。
如果前缀表不改变,发生冲突后会在发生冲突位置的前一位的前缀表所对应的值,这个值为模式串开始重新匹配的下标位置。
如果前缀表右移,则发生冲突后会在发生冲突位置的前缀表所对应的值,这个值为模式串开始重新匹配的下标位置。
如果前缀表减一,则发生冲突后会在发生冲突位置的前一位的前缀表所对应的值加1,这个值为模式串开始重新匹配的下标位置。
前缀表减一的匹配方式:
如果将前缀表右移,初始为-1,当遇到冲突时就找冲突位置所对应的前缀表位置。
构造next数组
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
1.初始化;
2.处理前后缀不相同的情况;
3.处理前后缀相同的情况。
前缀表整体减1:
void getNext(int* next, const string& s) { //函数参数为 指向next数组的指针和一个字符串(模式串)
int j = -1; //初始化,定义两个指针,j指向前缀末尾位置,i指向后缀末尾位置
next[0] = j; //next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
for (int i = 1; i < s.size(); i++) { //那么i就下标1开始(即j的右边),进行s[i] 与 s[j+1]的比较
while (j >= 0 && s[i] != s[j + 1]) { //处理前后缀不相同的情况
j = next[j]; //next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度
}
if (s[i] == s[j + 1]) { //找到相同的前后缀,说明找到了相同的前后缀
j++;
}
next[i] = j; //将j(前缀的长度)赋给next[i],记录相同前后缀的长度
}
}
前缀表不变:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for (int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; // 找前一位的对应的回退位置
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
使用next数组进行匹配
在文本串s里 找是否出现过模式串t:
定义两个下标,j 指向模式串起始位置,i指向文本串起始位置;
前缀表统一减一(右移一位,初始位置为-1):
s[i] 与 t[j+1](因为j从-1开始的)进行比较,如果相同,那么i和j同时向后移动
如果不相同,j要从next数组中寻找下一个匹配的位置
int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // i从0开始遍历文本串
// s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。
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)) { // 如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串
return (i - t.size() + 1);
}
}
前缀表:
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
题目1:28.实现 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()]; //next数组等于模式串长度
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;
}
};
前缀表(不减一)
class Solution {
public:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for (int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while (j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size()) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
题目2:459.重复的子字符串
解题思路:
在KMP算法中,next数组记录的是最长相等前后缀,
如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(next数组长度为:len)
最长相等前后缀的长度为:next[len - 1] + 1。(这里的next数组是以统一减一的方式计算的,因此需要+1)
如果len % (len - (next[len - 1] + 1)) == 0 ,则说明 (数组长度-最长相等前后缀的长度) 正好可以被 数组的长度整除,说明有该字符串有重复的子字符串。
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。
(len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。
前缀表统一减一
class Solution {
public:
void getNext (int* next, const string& s){
next[0] = -1;
int j = -1;
for(int i = 1;i < s.size(); i++){
while(j >= 0 && s[i] != s[j+1]) {
j = next[j];
}
if(s[i] == s[j+1]) {
j++;
}
next[i] = j;
}
}
bool repeatedSubstringPattern (string s) {
if (s.size() == 0) {
return false;
}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) {
return true;
}
return false;
}
};
前缀表(不减一)
class Solution {
public:
void getNext (int* next, const string& s){
next[0] = 0;
int j = 0;
for(int i = 1;i < s.size(); i++){
while(j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if(s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
bool repeatedSubstringPattern (string s) {
if (s.size() == 0) {
return false;
}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) {
return true;
}
return false;
}
};