天地广场KMP算法详解
1. 学好KMP算法,关键点有三点,⼀是知道什么是前缀和后缀最长相等长度;⼆是理解KMP算法流程,三是求解next数组。
⼀、什么是前缀和后缀最长相等长度
⾸先给定⼀个字符串abstkabs。下⾯介绍前缀和后缀
前缀就是以第⼀个字符开始,⼀共能找出多少种和本⾝不⼀样的字符串(必须连续)。意思就是你不能破坏原来字符串的顺序来找,⽐如abstkabs,abtk就不⾏,因为它破坏了原来的顺序结构,即从b直接跳到t。所以abstkabs的前缀有a、ab、abs、abst、abstk、abstka、abstkab;不计算本⾝,所以共7种。同理,后缀是以最后⼀个字符结尾,⼀共有多少种和本⾝不⼀样的字符串。此处后缀有s、bs、abs、kabs、tkabs、stkabs、bstkabs 共7种。⽽前缀和后缀中只有abs这个字符串是相等的,所以前缀和后缀中最长的长度就是当前缀和后缀都为abs的时候,即最长相等长度为3。理解这个概念很重要,这也是next数组的含义。
⼆、KMP算法的流程
1、简单概念介绍:
KMP算法简单的说就是匹配字符串S1,S2,是否S2为S1的⼦串,当然你要满⾜s2的长度是⼩于等于S1。⽐如absabs和bsa,明显bsa是包含于absabs中,并且连续,所以bsa为absabs的⼦串。⽽kmp算法的返回值是如果S2是S1⼦串,则返回第⼀个匹配相等的字符的下标,所以这个例⼦,就是返回1,如果不是⼦串直接返回-1。
2、从暴⼒解法引⼊KMP算法
注意下图中的数字或者字母代表字符串中某个字符的下标(或者理解成指针),不要和字符串的内容混淆
如果针对两个字符串匹配的问题,暴⼒解法必然是如下所⽰:拿⼀个⽐较通⽤的情况来举例
即当S2和S1⼀直开始匹配,直到某个匹配过程的时候,匹配到不相等的地⽅,也就是S1[X]和S2[Y](绿⾊表⽰字符串的内容匹配⼀致)。这时候暴⼒解法就是S2向右移动⼀位,也就是拿S2重新跟S1从i+1处开始进⾏匹配。然后依次类推。这样⼀来时间复杂度是o(N*M)级别的,因为假设S1有N个,S2有M个(M<=N),考虑最坏情况:
我需要从S1第⼀个字符开始看,如果和S2⼀致,则要往下⼀直要看M个字符,直到发现最后⼀次⽐较S1和S2对应的值不相等,那么我得重新开始从第⼆个字符开始继续看。对于第⼀个字符就是o(1*M),S1⼀共N个字符,所以需要看N*M遍,所以复杂度为o(N*M)。复杂度还是很⾼的。⽽KMP算法其实在暴⼒的基础上跳过了⼀些字符的⽐较,所以加速了⽐较进程。
KMP算法是怎么样流程呢?还是针对上⾯那幅图
当S1和S2匹配到不相等的时候,即S1[X]!=S2[Y],KMP是根据S2中的前缀和后缀的最长相等长度(后⾯⼀致称为最长前缀,因为两者相等),将指针Y往前指向到Y处的最长前缀(不含Y,只看Y之前字符串
的最长前缀)的下⼀个,再跟S1中X处开始⽐较。可能你听不太懂啥意思,看下⾯的图应该⽐较好理解:
假设此时Y处之前的最长前缀和后缀是红框中所⽰,那么Y就指到到最长前缀的下⼀个,也就是:
然后再和S1的X处进⾏匹配,如下图虚线对应的那样:(j是最长后缀的第⼀个字符,在下⾯讲解有⽤)
其实就相当于把S1往左移动,也就相当于把S2往右移动,反正只要保证X和Y对应就⾏。但这⾥我习惯性的是理解成S2右移动(因为S1是你的参照物,最好不动,然后S2是你要匹配的,所以尽量让S2移动,我觉得这样⽐较好理解)。所以就相当于Y向右移动五格,所以X和Y对齐,0和j对齐:
⾄此,其实KMP算法的性质就变了,也就是变成问你,S1从j位置开始,可不可以实现S2的匹配?并且,我只需要从X和Y处进⾏⽐较即可。
你可能会问,为什么可以直接跳到j位置开始匹配呢?这是因为j的位置是最长后缀的第⼀个字符,⽽0的位置是最长前缀的第⼀个字符,将他俩对齐我可以保证j~x-1这段内容和0~y-1这段内容是⼀致的。因此我就可以直接从S1[X]和S[Y]处进⾏⽐较。但是问题⼜来了,你怎么保证j之前的任意位置开始,S1配不出S2?我们可以⽤反证法来证明:
假设i~j中间有⼀个位置k,且从k位置开始可以实现S2的匹配(图中红框的部分表⽰S2的最长前缀和后缀)即:
如果在假设成⽴的前提下,那么必然有什么?因为假设告诉你,从k开始可以实现s2的匹配,所以必然有k~x-1的长度的内容必然和S2中从0开始等长度的内容是⼀致的(如果这⼀段都不⼀致,后⾯怎么可能是匹配的嘛),也就是黄⾊框内的内容。即S1黄=S2黄。这样会出现什么问题?因为绿⾊的内容是表⽰匹配,所以会出现:
S2紫⾊框和S1黄⾊框的内容相等(都是绿⾊),S1黄= S2紫。⽽S1黄=S2黄。所以有S2的黄⾊框=S2的紫⾊框,S2黄= S2紫。那这代表什么?不就代表S2的最长前缀和最长后缀变成了S2黄和S2紫了吗?⽽我们之前明明给定了最长前缀和最长后缀是红框了呀,所以出现就前后⽭盾,所以假设不成⽴。这就说明i~j中不可能出现某个位置实现S1对S2的匹配,那么⾃然为什么可以直接从j位置开始匹配的原因。
上⾯就是KMP算法的核⼼思想。接下来讲⼀下流程:假设两个指针i1和i2分别指向S1和S2的第⼀个字符。分为三种情况
拉伸真的能长高吗
1.当S1[i1] = S2[i2]的时候,两个指针同时往后⾛,i1++,i2++;
2.当S1[i1] != S2[i2]的时候,其实也就是我们上⾯讲述的⼀⼤堆所代表的某种中间情况,即在某⼀个位置,两者不匹配了。
2.1 如果i2可以往前跳:即next[i2]!=-1或者i2!=0(因为next[0]=-1),则i2跳到最长前缀的下⼀ 个,即i2=next[i2];再跟可能x
处对⽐(其实就是找j位置)
2.2.当i2不能往前跳的时候,i1++;这步是什么意思?其实就是i2⼀直往前跳,跳到头, 还没找 到s1中可以和我对⽐的位置,也就
是j位置(上述已讲)。怎么说?因为第2步,它就是往前 跳,跳到⼀个位置可以去跟x处对⽐(相当于可以找到⼀个j位置,让s1从j位置开始,对s2进⾏匹配)。⽽此处i2跳到头还没找到⼀个位置可以和x位置对⽐,那么说明从s1的x处开始⼀直往前,根本没有⼀个j位置可以匹配s2,所以这时候,s1需要往下移动⼀个位置作为新的X处,然后再重复过程即可。
下⾯给出KMP的主代码。
int KMP(string&s1, string&s2) {
中国人性格if (s1.empty() || s2.empty() || s1.size() < s2.size()) return -1;
vector<int> next = getNextArray(s2);//构建⼩串s2的next数组
//开始匹配,⾸先定义s1和s2的指针都从0开始
春天英文int i1 = 0;//指向s1的⾸元素
int i2 = 0;//指向s2的⾸元素
while (i1 < s1.size() && i2 < s2.size()) {
if (s1[i1] == s2[i2]) {//当两者指向的值相等,两个继续往下指,看后续是否⼀致
i1++;
i2++;
}
el if (i2!=0) {//当两者指向的值不相等的时候,并且i2不是指向第0个位置(因为next[0]=-1)i2往前跳到最长前缀的下⼀个
//el if(i2!=0)也可以这样写
i2 = next[i2];
}
el {//当两者指向的值不相等,并且指向第0个位置,那么i1要往后⾯移动⼀个位置跟我再⽐
i1++;
}
小学生读书笔记10篇
}
return i2 == s2.length() ? i1 - i2 : -1;
/*
i2如果越界,则表⽰匹配完成,所以,返回的第⼀个匹配的下标就是i1 - i2,否则返回 - 1
注意,为什么是i1-i2?因为我们要知道,i2是不停跳跃变化的,但是唯⼀关⼼的就是当匹配完成的时候,
它⼀定会到最⼤下标的下⼀个(越界),这样才能保证s2⽐完,所以i2==s2.size(),就是看它越界没,
社会化媒体营销
如果越界,则第⼀个匹配的下标,是啥,是不是i1当前的位置,减去i2的长度(也就是s2.size())?对吧
*/
}
三、next数组
next的数组的含义⼀定要搞清楚,⾸先next数组是针对较⼩的字符串⽽⾔的,换句话说就是需要匹配的字符串,⽐如abbs和bbs,那么next数组就是针对bbs⽽⾔。其次,该数组的每个位置的值(或者叫信息)其实是跟该位置没有关系,它其实表⽰的是它前⼀位置的信息,这个信息是什么呢?就是该位置的前⼀位置的最长前缀(最长后缀)的长度。
我们⼈为的规定,next[0]=-1,next[1]=0;所以next数组要从i=2开始计算。⽐如bbs这个字符串,它的next数组就是[-1,0,1],因为i=2,是s,它保存的信息是s之前(跟S⽆关)的字符串的最长前缀和最长后缀的长度,所以就是1。
了解完next数组的含义,我们来看看next数组怎么求解。假设我们现在要求字符串str 的i位置的值,那
么根据之前说的,我们需要找的是i 之前的字符串的最⼩前缀的长度。⽽next[i]必然跟next[i-1]相关。我们设i-1前的最长前后缀为红框所⽰,且最长前缀的下⼀位的位置⽤cn 表⽰
其实也分为三种情况:
1.str[i-1]==str[cn],那么很显然,i之前的最长前后缀长度就变成了3+1=4;即:next[i+1]=cn+1
2.当str[i-1]!=str[cn]:
2.1:且cn并不处于第0个位置,也就是cn可以往前跳,那么cn就跳到next[cn]的位置,
即cn=next[cn];next[cn]的值是什么?它表⽰cn处的信息,也就是cn之前的字符串的最长前后缀长度。假设上图红框中还有最长前后缀,即:
这时继续⽐较cn和i-1处是不是相等,相等则next[i+1]=cn+1,即next[i]=1+1 = 2;
2.2 cn处于0位置,说明cn不能往前跳了,因此i之前就没有最长前缀和后缀,所以next[i+1]=0;
next数组实现:
vector<int> getNextArray(string& str) {
if (str.size() == 1) return { -1 };
//定义⼀个next数组
年轻的女教师
vector<int> next;
next[0] = -1;
next[1] = 0;
int i = 2;//从i=2开始计算
int cn = 0;
/*注意:为什么这⾥cn=0?
cn表⽰拿什么位置跟i-1位置对⽐,也就是⼀直拿cn位置(前缀的下⼀个)跟i-1位置对⽐。
因为i从2开始计算,⽽i-1也就是1,所以就是拿什么位置跟i=1对⽐?i=1之前就⼀个字符,也就是说没有前缀,所以cn=0
此外,cn还表⽰i-1的信息,因为cn = next[i-1],⽐如next[1] = 0,这就是为什么初始值设置为i=2,cn=0
*/
while (i < next.size()) {
if (str[i - 1] == str[cn]) {//如果str的i-1处字符和cn处相等,那么next[i]=next[i-1]+1,继续往下⽐
next[i++] = ++cn;//cn也是当前next[i-1]的值,所以next[i]=cn+1。因为要继续看下⼀位是否同样满⾜,所以同时⾃增
}
el if (cn > 0) {//并不是0位置
cn = next[cn];
}
el {
next[i++] = 0;
}
}
return next;
}
最后写个main函数测试⼀下:
int main()
{
string big;
string small;
cout << "请输⼊⼤⼩字符串" << endl; cin >> big;
cin >> small;
int ans = KMP(big, small);
cout<< ans<<endl;
system("pau");
return 0;
}遗爱湖