一、最长公共子缀
公共子缀分为前缀和后缀
前缀:总是包含第一个字符的子串(不包括父串本身)
后缀:总是包含最后一个字符的子串(不包括父串本身)
————————————————————————————
举个例子:
父串ABCDEAB
前缀:A、AB、ABC、ABCD、ABCDE、ABCDEA
后缀:B、AB、EAB、DEAB、CDEAB、BCDEAB
最长公共子缀就是指完全相同的最长前缀和后缀
在上述的例子中最长公共子缀是AB,长度为2。
如下图所示,最长公共子缀就是看前、后有多少完全相同的字符。
二、next数组
next数组的值其实就是【当前位置之前的字符串】的最长公共子缀长度
————————————————————————————
举个例子:
父串ABAABACA
当我们在计算C字符对应的next数组值时,我们其实就是要计算ABAABA这个字符串的最长公共子缀。
ABAABA的前缀有A、AB、ABA、ABAA、ABAAB
后缀有A、BA、ABA、AABA、BAABA
最长的公共子缀是ABA,长度为3,所以C这个位置的next数组值就是3。
由于第一个字符之前没有任何字符,所以我们规定第一个字符处的next数组值为-1;第二个字符之前只有1个字符,但是我们规定子缀是不能包含父串本身的,所以最长公共子缀为0;其他位置的按照标准计算即可。
根据定义,父串ABAABACA的next数组值为:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
str[i] | A | B | A | A | B | A | C | A |
next[i] | -1 | 0 | 0 | 1 | 1 | 2 | 3 | 0 |
三、基于next数组的文本匹配
KMP算法解决的问题是在父串中查找子串是否存在。
————————————————————————————
我们举例来说明KMP的工作过程
父串txt=BBCABCDABEABCDABCDABDE
子串str=ABCDABD
(1)求出子串str的next数组
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
str[i] | A | B | C | D | A | B | D |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 |
(2)指针定位到父串的开头和子串的开头,匹配第一个字符
(3)子串第一个字符就不匹配,子串不动,父串移动,继续和子串第一个字符匹配
(4)子串第一个字符仍然不匹配,子串不动,父串移动,继续和子串第一个字符匹配
(5)子串第一个字符仍然不匹配,子串不动,父串移动,继续和子串第一个字符匹配
(6)子串第一个字符匹配了,则子串和父串同时向后移动,继续匹配,直到匹配到子、父串不相等(或者子串匹配完成,则查找结束)
(7)匹配到不相等的位置,该位置在子串中的下标是i,则父串不动,子串移动到next[i]的位置,比如图中位置D在子串中下标是6,next[6]=2,则子串移动2这个位置。
(8)继续比较当前位置的子串是否和父串相等,如果等则子串、父串一起后移;如果不等则重复上面的过程,也就是移动到next[i]这个位置
(9)左移完之后继续比较子串是否和父串相等,发现不等,这时发现子串已经回到了头部,再左移没有位置移动了,所以此时只移动父串,继续和子串第一个位置比较
(10)发现父串和子串第一个位置相同,则父串、子串一起向后移动,直到匹配到不相同的字符(或者子串匹配完成,则查找结束)
(11)发现了不相等的字符,则子串移动到next[i]的位置
(12)移动之后继续匹配,如果相等就父串、子串一起往后动;如果不等就子串移到next[i]的位置;如果已经回退到子串头部了,因为next[0]=-1,所以那就可以父串、子串一起移动一下就好
(13)子串已经匹配完了,查找结束。
所以KMP的基本工作原理就是:
- 若子串、父串相等,则同时往后移动
- 若子串、父串不等,则子串移动到next[i]这个位置
- 若i=-1时子串、父串同时往后移动
四、KMP优化原理
你应该发现,KMP的核心要义其实就是左移到next[i]这个部分了,那为什么这个部分可以帮助我们减少比较的工作量呢?
传统的匹配算法如下图:
在匹配到不相同的字符时,父串回退到刚才开始匹配位置的下一个位置,子串回退到头部,从头开始匹配。
我们明显觉得很麻烦,甚至要喊:你要退也退到一个跟子串开头字符相等的位置吧,干嘛非得一位一位动?
就比如下图,我们在父串E位置匹配失败了,如果要重新匹配,那肯定也是从刚才匹配过的字符中找一个跟子串头位置相等的位置,比如标红的A处开始重新匹配吧,那为什么移动next[i]就正好是这个位置呢?
回顾一下next数组的含义:next[i]指的是【i之前的字符串】的最长公共子缀长度。
next数值其实告诉了我们两个信息,一是子串从哪个位置开始和头部相等;二是从那个位置开始,到当前位置结束,一共有多少个相同的字符和头部是匹配的。
比如上图位置D,next[6]=2,就代表str[6-2]=str[4]这个位置是跟头部字符是相同的;而从str[4]这个位置开始,有2个字符和从头部开始有2个字符是相同的。也就是,从子串开头开始有X0X1两个字符和D前面两个字符X4X5相等,既然X0X1=X4X5,现在X6和父串不相等,那我们下一步就应该移到X3,看它和父串对应位置是否相等。
左移到next[i]可以让头部的A占据D之前的A的位置,让头部的B占据B之前的B的位置,然后开始匹配后面的数据。
通过这样的方法,我们可以节省大量的时间,做到父串不回退。
五、求next数组代码
next数组的原理上面已经讲了,现在说说怎么写next数组的求解代码。
首先能想到的一个暴力方法是,每一位在求解时,都把它之前的子串的前后子缀由长到短逐渐比较。
————————————————————————————
比如AAABBADA,我们在求解D所对应的next数组值时,将前面的子串AAABBA提出来:
- 先比较AAABB和AABBA
- 不等,则比较AAAB和ABBA
- 不等,则比较AAA和BBA
- 不等,则比较AA和BA
- 不等,则比较A和A
- 发现等,所以next数组值是1
想也知道这是一个多么繁琐的过程,所以我们势必要进行优化,我们再看一个例子:
————————————————————————————
比如要求下面数组的next数组内容,首先0和1的位置固定是-1和0不会变。
不知道你看出规律了没有!
每一项都正好比前一项多出了一条新的匹配出来,也就是说前一项的next值可以给后面一项提供参考。
比如说:
如果i=3时,计算出next[3]=1,也就是X0=X2,那在计算next[4]时,我们可以直接去匹配X1和X3,而不用再重新匹配X0和X2了,因为next[3]=1其实就已经告诉我们这两相等了,如果X1=X3,那next[4]=next[3]+1=2。
你会奇怪,计算next[4]时,不是应该先看[0-2]和[1-3]是否相等吗?为啥直接跳过了然后就去算第二种情况了?这是因为next[3]=1≠2,不等于2其实就告诉我们X0X1≠X1X2,所以对于next[4]来说就不用计算X0X1X2?X1X2X3了,因为肯定不等。
而且你会发现,当我们在计算i+1这一项的next数组值时,比i多出来的这一条对比项其实是看Xi?Xnext[i]的关系,比如i=4时next[i]=next[4]=2,那计算i+1,也就是next[5]时,我们就可以直接匹配X4?Xnext[4],也就是X4和X2,发现相等,所以next[5]=next[4]+1=3。
所以next计算的第一条规律就是:
在已经知道next[i]的情况下,如果要计算next[i+1],我们令m=next[i],那我们可以直接去匹配Xi?Xm的关系,如果相等,则next[i+1]=next[i]+1。
那问题来了,如果Xi和Xnext[i]不等怎么办?
我们先说结论:如果Xi≠Xnext[i],那就去比较Xi和Xnext[next[i]]
比如:假设next[9]=3,那计算next[10]时应该匹配看X9和X3是否相等,如果不等,则去匹配X9和Xnext[3]是否相等,如果不等,则继续匹配X9和Xnext[next[3]]是否相等,直到X9和X0都匹配过了,没得退了,说明next[10]=0。
如上图所示,我们假设next[i]=m,那就表示从0开始有m个字符和从i-1开始往前数m个字符是相同的,也就是X0X1…Xm-1=Xi–mXi-m+1…Xi-1。
那在计算next[i+1]时,应该先匹配Xi和Xm,看是否相等,如果不等,如下图所示,那我们就应该缩减红色框的范围,看更小的范围内能不能找到一个位置k,在k这个位置X0X1…Xk-1=Xi–kXi-k+1…Xi-1,那这样我们就可以直接匹配Xk和Xi,看是否相等,如果相等那就说明next[i+1]=next[k]+1。
那为什么这个位置k=next[m],而不是直接m-1呢?
如下图所示,我们需要找一个位置k,让【从0开始数k个字符】和【从i-1往前k个字符】相等,也就是X0X1…Xk-1=Xi–kXi-k+1…Xi-1,那因为next[i]=m,所以根据对称原则,从【m-1往前数k个字符】应该和【从i-1往前数k个字符】是一模一样的,也就是说【从0开始数k个字符】和从【m-1往前数k个字符】应该是一模一样的,那这个k不就是next[m]吗?!
所以next计算的第二条规律就是:
在已知next[i]的前提下要计算next[i+1],我们令m=next[i],那我们可以直接去匹配Xi?Xm的关系,如果不等,则我们令m=next[m],接着去匹配Xi?Xm的关系,以此类推直到两个字符相等,这时我们知道next[i+1]=m+1;或者m已经退到头部了还不等,这时m=next[0]=-1,next[i+1]=m+1=0正好。
写成代码就是:
public void calNext(int[] next,String str){
next[0]=-1;
if(next.length==1){
return;
}
next[1]=0;
int i=2;
int m=next[i-1];
while(i<str.length()){
if(m==-1 || str.charAt(i-1)==str.charAt(m)){
next[i]=m+1;
m++;
i++;
}else{
m=next[m];
}
}
}
六、KMP整体代码
有了求next数组的代码,KMP的代码就简单多了,我们直接按照上面的标准来写:
所以KMP的基本工作原理就是:
- 若子串、父串相等,则同时往后移动
- 若子串、父串不等,则子串移动到next[i]这个位置
- 若i=-1时子串、父串同时往后移动
public int KMP(String txt,String str){
if(null==txt || null==str){
return -1;
}
if(str.equals("")){
return 0;
}
int[] next=new int[str.length()];
calNext(next,str);
int i=0,j=0;
while(i<str.length() && j<txt.length()){
if(i==-1 || str.charAt(i)==txt.charAt(j)){
i++;
j++;
}else{
i=next[i];
}
}
if(i==str.length()){
return j-i;
}else{
return -1;
}
}