今天又学到了一个新的算法,KMP算法,是用来求子串的,即判断字符串str2,是否为字符串str1的子串。学过以后,自己总结一下,方便加深理解。
KMP使用场景
判断字符串str2,是否为字符串str1的连续子串。例如:str2 = “1234”,为str1=“ABC1234DE”的子串,但str3=“1234E”不为str1的子串。
一般的暴力方法
如下图:
str1从i位置开始遍历,str2从0位置开始遍历。字符串str1从下标为i的位置一直遍历到下标为X-1位置的字符,均与str2从下标为0位置一直遍历到下标为Y-1位置的字符相同,但是str[X]与str[Y]不同。此时str1跳回到从i+1位置开始遍历,str2跳回到从0位置开始遍历,直至将str2遍历完,说明str2是str1的子串,否则说明不是str1的子串。
举个栗子,如下图:
<1> 红色箭头从0开始遍历,a不等于e,则红色指针后移,直至指向元素等于e。记录此时红色下标为index。接着绿色指针与红色指针同时后移直至绿色指针指向h红色指针指向i。如下图:
<2> 此时满足上面说的情况,因此红色指针回到index+1的位置,绿色指针回到0位置。如下图:
然后红色箭头接着后移,直至红色箭头指向的元素为e,此时记录下当前红色箭头的位置index,接着红色绿色箭头同时后移,直至绿色遍历完毕。说明str2为str1的子串。
KMP算法
KMP算法与暴力方法的区别在于,优化了<2>过程,当满足图1的条件后,红色箭头不是逐个后移,而且根据算法跳着后移。实现了优化。在了解KMP算法之前。首先引入几个概念
前缀,后缀,最大相等长度信息
举个栗子:如下图
对于字符k前面的字符串,前缀表示从头开始,后缀表示从后面开始。那么长度为1的前缀为a,长度为1的后缀为b,前缀不等于后缀。长度为2的前缀为ab,长度为2的后缀为bb,前缀不等于后缀。长度为3的前缀为abb,长度为3的后缀为abb,前缀等于后缀。长度为4的前缀为abba,长度为4的后缀为babb,前缀不等于后缀。长度为5的前缀为abbab,长度为5的后缀为bbabb,前缀不等于后缀。长度为6时大于等于k前字符串abbabb的总长度,故不考虑。那么k的最大相等长度信息为3。
KMP算法
那么kmp算法就是在满足图1的情况时,已知下标为Y的str2[Y]的最大相等长度信息为len,那么此时X不必跳转到index+1位置,只需让Y跳转到len+1的位置,接着让X位置开始的字符依次与从Y位置开始的字符进行比较,若还是不等,就再看当前Y位置的最大相等长度信息,重复上面的过程,直至相等或者str2[Y]只有一个元素了,就让X后移一位与str2从0开始比。如图:
举个栗子:如下图:
<1> 显然,红绿色两个箭头指向的字符之前的字符都相同,只有当前指向的字符不同。而且蓝色圈圈出了最大相等长度len,那么此时将绿色箭头指向蓝色圈刚结束的那个字符str[len+1]。这就相当于将str2向后整体挪了len+1到Y的距离,如下图:
<2> 此时看str2绿色箭头与str1红色箭头指向的字符,不等,则根据str1中绿色箭头指向的字符t的最大相等长度信息,可以知道len=3,此时再将绿色箭头移动到len+1处,如下图:
重复<2>过程,得到下图:
显然,绿色箭头指向的字符依旧不等于红色箭头指向的字符那么红色箭头向后移一位,接着重复整个过程,知道绿色箭头遍历完str2返回红色箭头减去绿色箭头位置(找到子串的首位置),否则当红色箭头遍历完str1,但是依旧没找到时,返回false。
KMP算法c++代码实现
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
int* getNextArray(char *str,int len);//将某一个字符串的每个字符的最大相等长度信息存起来
int getSubStringIndex(string str1, string str2)//得到子串在父串中出现的第一个位置下标
{
if (str1.length() < 1 || str2.length() < 1)
return -1;//当字符串没有字符时,返回-1
char*s1 = (char*)str1.c_str();
char*s2 = (char*)str2.c_str();
int index1 = 0;
int index2 = 0;//分别指向s1与s2的下标指针
int *next = getNextArray(s2,str2.length());//获得s2中每个字符的最大相等长度信息
while (index1 < str1.length() && index2 < str2.length())//保证下标不越界
{
if (s1[index1] == str2[index2])
{
//若字符相等,两个指针均后移
index1++;
index2++;
}
else if (next[index2] == -1)
{
//否则,当next[index2] == -1时,说明s2到了第一个元素,此时要才将index1后移,从新的位置开始找子串
index1++;
}
else
{
//剩下的情况就是index1位置的元素,与当前index2位置的元素不等,则将index2移动到当前元素的最大相等长度的位置
index2 = next[index2];
}
}
if (next != NULL)
delete[]next;
next = NULL;
return index2 == str2.length() ? index1 - index2: -1;//当s1或s2遍历完后,判断index2是否将s2遍历完,若完了,返回index1-index2(表示找到的位置),否则返回-1表示未找到
}
int* getNextArray(char *str,int len)
{
if (len == 1)
{
//若只有一个元素,则返回-1数组
int *next = new int(-1);
return next;
}
int *next = new int[len];//首先开辟一个数组来保存
next[0] = -1;//当只有一个元素是,这个元素的最大相等长度信息显然不存在,设置为-1
next[1] = 0;//当只有两个元素是,第二个元素的最大相等长度信息显然为0
int i = 2;//从第三个元素(下标为2)开始
int cn = 0;//用cn位置的元素与i-1位置元素相比,也表示当前第i-1个值的最大长度信息
while (i<len)
{
if (str[i - 1] == str[cn])
{
//当i-1位置的元素与cn位置元素相等时,将cn+1记录下来
next[i++] = ++cn;
}
else if (cn>0)//当跳到cn位置,且和i-1位置字符配不上
{
cn = next[cn];
}
else
{
next[i++] = 0;
}
}
return next;
}
int main(void)
{
cout << getSubStringIndex("abcabcababaccc", "ababa") << endl;
system("pause");
return 0;
}