一、KMP解決的問題
一個字符串s1=“BBCABCDABABCDABCDABCDABDE”,和一個模式串s2=“ABCDABD”;現在要求s2於s1匹配的開端位置。如本串的位置就是 “BBCABCDABABCDABCDABCDABDE”,開端位置就是A,下標爲17
二、KMP的過程
KMP主要分爲兩個過程。一個是求next數組。另一個是根據next數組移動。
(一)求next數組
(1)求解next數組,next數組是對於s2模式串來說的。
求解next數組首先要明白前綴和後綴的概念,這邊不再重複造輪子,關於這個概念看其他的博客,其實說白了
前綴就是:ABCDABD
後綴就是:ABCDABD
當這兩部分能相等,且前綴最長不能到達最後一個字符,後綴不能到達第一個字符的最長串,就是最長前後綴。
next數組保存的就是從第一個字符到當前字符之前的這個串的最長前後綴:
比如:對於ABCDABD中的B來說,它的最長前後綴就是ABCDA這部分的值。因爲只有前後綴都爲A時才相等。所以B這個位置的最長前後綴就是1。
(2)如何快速求得next數組呢,其實求這個數組有一個規律。
首先next[0]和next[1]認爲規定爲:
next[0] = -1;
next[1] = 0;
接下來從i = 2開始都遵循這樣的一個規律:
如果s2[i-1]等於(i-1)這個位置的最長前後綴中的前綴的下一個字符;如果不相等,則以這個字符,判斷其最長前後綴的前綴字符是否等於,直到找到相等的,則最長前綴就是這個最長前綴+1,否則爲0;
所以可以求得上面這個串的next={-1,0,0,0,0,1,2}
private static int[] getNextArray(char[] str2) {
int[] next = new int[str2.length];
next[0] = -1;
next[1] = 0;//前面這兩個數是認爲規定的
int i = 2;
int cn = 0;//代表分界指標,這邊有一個隱含的條件,最長前綴和最長後綴的長度==最長前綴的最後一個字符下標
while(i < str2.length)
{
if(str2[i - 1] == str2[cn])//判斷最長前後綴的前綴的下一個字符是否等於當前判斷的位置的前一個字符
{
next[i++] = ++cn;//存在則值爲最長前後綴的前綴的長度+1
}
else if(cn > 0)//前一個字符和最長前綴的下一個字符不相等,則以最長前綴下一個字符爲切分點繼續切分
{
cn = next[cn];
}
else {
next[i++] = 0;//cn來到了沒有最長前綴,則i這個位置的next值爲0
}
}
return next;
}
(二)移動求解過程
根據我們求得的KMP就可以快速找到是否相等的匹配串。
主要由三部分組成:
(1)判斷字符是否相等,如果相等,匹配下一個字符
(2)如果不相等,從查找當前不相等字符的next值,模式串移動next值對應的最長前綴。其中的原理可以參考其他博客。
比如
s1 = “BBCABCDABABCDABCDABCDABDE”
s2 = “ABCDABD” 當前匹配到s1的A和s2的D不相等,也就是當前的指向i1來到了A位置,i2來到了D位置。則查找D這個位置的next數組中的值。根據上面生成的next數組可以知道next[D]=2,則i2指向s2的C位置,因爲前面的是最長前綴,一定跟s1的那部分相等,所以只需要從c位置開始驗證,如果next[D]==-1,也就是沒有最長前綴,從我們的規定next數組可以看出,這個時候其實已經來到了s2的首字母,則從新開始匹配。
public static int getIndexOf(String s1, String s2)
{
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
int[] next = getNextArray(str2);
int i1 = 0;//主子串下標
int i2 = 0;//匹配子串下標
while(i1 < str1.length && i2 < str2.length)
{
if(str1[i1] == str2[i2])//如果字符相等,則繼續往前匹配
{
i1++;
i2++;
}
else if(next[i2] == -1)//如果不相等,且當前字符已經沒有最長前綴和最長後綴的匹配
{
i1++;//順序匹配下一個字符
}
else {
i2 = next[i2];//存在最長前綴和最長後綴相等大於0的,則從最長前綴的下一個字符開始匹配,最長前綴那一部分不用匹配是因爲
//一定是相等的
}
}
return i2 == str2.length? i1 - i2:-1;//如果跳出循環的是因爲匹配到最後一個字符正確跳出的,則存在子串,否則不存在返回-1
}
總的代碼如下:
package lc.Top.Inte;
public class KEMAlgorithm {
/**
* 根據next移動,找到匹配串的開端下標
* @param s1
* @param s2
* @return
*/
public static int getIndexOf(String s1, String s2)
{
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
int[] next = getNextArray(str2);
int i1 = 0;//主子串下標
int i2 = 0;//匹配子串下標
while(i1 < str1.length && i2 < str2.length)
{
if(str1[i1] == str2[i2])//如果字符相等,則繼續往前匹配
{
i1++;
i2++;
}
else if(next[i2] == -1)//如果不相等,且當前字符已經沒有最長前綴和最長後綴的匹配
{
i1++;//順序匹配下一個字符
}
else {
i2 = next[i2];//存在最長前綴和最長後綴相等大於0的,則從最長前綴的下一個字符開始匹配,最長前綴那一部分不用匹配是因爲
//一定是相等的
}
}
return i2 == str2.length? i1 - i2:-1;//如果跳出循環的是因爲匹配到最後一個字符正確跳出的,則存在子串,否則不存在返回-1
}
/**
* 計算next數組
* @param str2
* @return
*/
private static int[] getNextArray(char[] str2) {
int[] next = new int[str2.length];
next[0] = -1;
next[1] = 0;//前面這兩個數是認爲規定的
int i = 2;
int cn = 0;//代表分界指標,這邊有一個隱含的條件,最長前綴和最長後綴的長度==最長前綴的最後一個字符下標
while(i < str2.length)
{
if(str2[i - 1] == str2[cn])
{
next[i++] = ++cn;
}
else if(cn > 0)//前一個字符和最長前綴的下一個字符不相等,則以最長前綴下一個字符爲切分點繼續切分
{
cn = next[cn];
}
else {
next[i++] = 0;//cn來到了沒有最長前綴,則i這個位置的next值爲0
}
}
return next;
}
public static void main(String[] args) {
String s1 = "BBCABCDABABCDABCDABCDABDE";
String s2 = "ABCDABD";
int indexOf = getIndexOf(s1, s2);
System.out.println(indexOf);
}
}