KMP算法
问题求解:给你两个字符串 文本串 和 模式串,请你在 文本串 haystack 中找出 模式串 needle 出现的第一个位置。
实现 strStr() 函数:
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。
如果不存在,则返回 -1 。
说明: 当 needle 是空字符串时应当返回 0 ,与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符
实现strStr()方法1 – 暴力解决
j 指向模式串的当前匹配字符下标needle[j] ,i 指向文本串的当前匹配下标haystack[i],遇到不匹配就重新开始,即当needle[j] != haystack[i] 时,将 i 置为 i - j + 1,j 置为 0。
public int strStr(String haystack, String needle) {
if(needle.length() == 0) return 0;//当 needle 是空字符串时返回 0
int j = 0;
int i = 0;
while(i < haystack.length()){
if(haystack.charAt(i) == needle.charAt(j)){
j++;
i++;
}else{//回退
i = i - j + 1;
j = 0;
}
if( j == needle.length())//说明模式串完全匹配
return i - j;
}
return -1;//遍历到文本串的末尾 ,模式串不是文本串的子串 即题意的不存在
}
实现strStr()方法2 – KMP算法
KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数以达到快速匹配的目的。
具体实现: 通过一个getNext()函数得到next数组,数组包含了模式串的局部匹配信息。
1. next 数组
next[i]: 求解 i 下标的前面字符串的最长相等前后缀长度。
以 aaabaaa 为例 求解最长相等前后缀长度
前缀: 不包含尾字符 – > 小于字符串长度的从首字符开始且不包含尾字符的相邻字符的组合
- a
- aa
- aaa
- aaab
- aaaba
- aaabaa
后缀:不包含首字符 小于字符串长度的从尾字符开始且不包含首字符的相邻字符的组合
- a
- aa
- aaa
- baaa
- abaaa
- aabaaa
即可得 最长公共相等前后缀长度 == 3 (aaa)
最长相等前后缀:
对于模式串而言,此部分与文本串匹配,且前后相等,那么不需要重新开始,直接退到最长相等前缀长度的后一个字符进行重新匹配即可。
假设 文本串 为 aabaabaafaab 模式串 为aabaaf
当 字符不匹配时 即
模式串 不匹配字符 f 前的部分与文本串匹配,即 aabaa 部分;aabaa 部分的 b字符前的aa 与 b字符后的aa 相等,说明b字符前的aa 与 文本串的中 原等于 b字符后的aa字符 的那部分字符 相等;
应用数学公式表示 1部分 = 3 部分 , 2 部分 = 4 部分 ;又 3 部分 = 4 部分 所以 3 部分 = 2 部分
因此我们只需回退到模式串的b字符,并且文本串匹配位置保持不变,继续进行匹配即可
2. 求解next数组代码
即求解最长相等前后缀长度 :j 指向前缀末尾,i 指向后缀末尾
- 如匹配则分别 +1;
- 如不匹配 则使得 j 回退到next[ j - 1],直至对应的下标的字符匹配 或 到达 0 下标(说明最长相等前后缀长度为0)循环停止。
//getNext函数代码
private void getNext(int[] next, String needle) {
int j = 0;
next[0] = 0;//第一个字符不含有前后缀,故初始化为0
for(int i = 1; i < needle.length(); i++){
while(j > 0 && needle.charAt(j) != needle.charAt(i))
j = next[j-1];//回退
if(needle.charAt(j) == needle.charAt(i))
j++;
next[i] = j;
}
}
为什么是回退到next[ j - 1],其实这也类似kmp算法回退过程,只不过这里是前缀在跟后缀匹配
j 和 i 指向的末尾的前面部分相等:以aaabaaa 为例,最长相等 前缀aaa 后缀aaa
若此时求解 aaabaaaf 的最长相等前后缀长度,b不等于f,next[ j - 1] 即 最长相等前后缀长度为2
用对应位置来识别字符,如第一个a 为 1a
12a 与 23a 相等,由于前后缀相等说明 56a 与 67a 相等,又 12a 与 56a 相等,那么 12a 与 67a 也相等,因此我们只需比较3a与f是否匹配,无需从头开始。
理论上下一个匹配字符是 最长相等前后缀长度 的下一个字符,又数组下标从0开始,即无需next[ j - 1] +1 , 下一步即是 j 指向 3a , 3a 跟 f 匹配。
3. KMP算法代码
算法思路: i 指向文本串,j 指向模式串,即 文本串的当前匹配下标haystack[i],模式串的当前匹配字符下标needle[j]
- 匹配则分别+1;
- 当不匹配, 即needle[j] != haystack[i] 时,j 回退到 next[j - 1] , i 不变,即不动i的位置,只移动j的位置。
public int strStr(String haystack, String needle) {
if(needle.length() == 0) return 0;//当 needle 是空字符串时返回 0
int[] next = new int[needle.length()];
getNext(next,needle);
int j = 0;
for(int i = 0; i < haystack.length(); i++){
while(j > 0 && haystack.charAt(i) != needle.charAt(j))
j = next[j-1];//回退
if(haystack.charAt(i) == needle.charAt(j))
j++;
if(j == needle.length())
return i - j + 1;
}
return -1;
}
private void getNext(int[] next, String needle) {
int j = 0;
next[0] = 0;//第一个字符不含有前后缀,故初始化为0
for(int i = 1; i < needle.length(); i++){
while(j > 0 && needle.charAt(j) != needle.charAt(i))
j = next[j-1];//回退
if(needle.charAt(j) == needle.charAt(i))
j++;
next[i] = j;
}
}