一 、KMP算法
KMP算法指的是字符串模式匹配算法,问题是:在主串T中找到第一次出现完整子串P时的起始位置。该算法是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的,以其名字首字母命名
1 next数组
next数组的含义:next[i]第i个字符之前,前缀(长度不能等于i)与后缀相等(长度不能等于i)的最长的长度。
如 abcabcw,对于下表为6的w字符 next[6]为3
前缀 | – | 长度 | 是否匹配 |
---|---|---|---|
a | c | 1 | N |
ab | bc | 2 | N |
abc | abc | 3 | Y |
abca | cabc | 4 | N |
abcab | bcabc | 5 | N |
abcabc | abcabc | 6 | N 不合法 |
2 如何快速求next数组
数组 next 的提取是整个 KMP 算法中最核心的部分,主要是通过消除主串指针的回溯来提高匹配的效率的(记忆化),那么, 消除回溯的呢?
这种信息就是对于每模式串 t 的每个元素 t j,都存在一个实数 k ,使得模式串 t 开头的 k 个字符(t 0 t 1…t k-1)依次与 t j 前面的 k(t j-k t j-k+1…t j-1,这里第一个字符 t j-k 最多从 t 1 开始,所以 k < j)个字符相同。如果这样的 k 有多个,则取最大的一个。模式串 t 中每个位置 j 的字符都有这种信息,采用 next 数组表示,即 next[ j ]=MAX{ k }。
private int [] getNext(char [] chars) {
int n = chars.length;
if(n==0) {
return new int[] {-1};
}
int [] next = new int [n];
next[0] =-1;
int k =-1;
int j =0;
while(j<n-1) {
if(k==-1||chars[k]==chars[j]) {
k++;
j++;
next[j] = k;
} else {
k= next[k];
}
}
return next;
}
}
三种情况来讲 next 的求解过程
1 特殊情况
当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。
next[0] =-1;
2 当 t[j] == t[k] 的情况
举个栗子
观察上图可知,当 t[j] == t[k] 时,必然有"t[0]…t[k-1]" == " t[j-k]…t[j-1]",此时的 k 即是相同子串的长度。因为有"t[0]…t[k-1]" == " t[j-k]…t[j-1]",且 t[j] == t[k],则有"t[0]…t[k]" == " t[j-k]…t[j]",这样也就得出了next[j+1]=k+1。
3 当t[j] != t[k] 的情况
关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];k 回退?我当然知道这是 k 退回.但是它为什么要会退到 next[k] 的位置?看下图
当 t[j] == t[k] 时,t[j+1] 的最大子串的长度为 k,即 next[j+1] = k+1。但是此时t[j] != t[k] 了,被t[j] != t[k] 打断了,不可能出现next[j+1] =k+1了, 所以就有 next[j+1] < k,那么求 next[j+1] 就等同于求 t[j] 往前小于 k 个的字符(包括t[j],看上图蓝色框框)与 t[k] 前面的字符(绿色框框)的最长重合串,即 t[j-k+1] ~ t[j] 与 t[0] ~ t[k-1] 的最长重合串(这里所说“最长重合串”实不严谨,但你知道是符合 k 的子串就行…),那么就相当于求 next[k](只不过 t[k] 变成了 t[j],但是 next[k] 的值与 t[k] 无关)!!!。所以才有了这句 k = next[k],如果新的一轮循环(这时 k = next[k] ,j 不变)中 t[j] 依然不等于 t[k] ,则说明倒数第二大 t[0~next[k]-1] 也不行,那么 k 会继续被 next[k] 赋值(这就是所谓的 k 回退…),直到找到符合重合的子串或者 k == -1。
再此特别感谢昵称为“sofu6”的博客园主
3 运用next数组实现indexOf
普通算法每次匹配失配i++,j回退到0;
KMP算法是如何加速的?
1 每次匹配失配 ,S串的索引i变为i+j,
2 j变为next[j]。
注意 :极端条件下,next[j]为-1;j回退为0;i++; 与普通算法一样。
4相关题目
28. 实现 strStr()
class Solution {
public int strStr(String haystack, String needle) {
if(needle ==null || needle.equals("")){
return 0;
}
int len1 = haystack.length();
int len2 = needle.length();
if(len2>len1) {
return -1;
}
int i =0;
int j =0;
int [] nexts = getNext(needle.toCharArray());
while(i<len1 && j<len2) {
if(haystack.charAt(i) == needle.charAt(j)) {
i++;
j++;
} else {
if(nexts[j]==-1) {
i++;
} else{
j = nexts[j];
}
}
if(j==len2) return i-len2;
}
return -1;
}
private int [] getNext(char [] chars) {
int n = chars.length;
if(n==0) {
return new int[] {-1};
}
int [] next = new int [n];
next[0] =-1;
int k =-1;
int j =0;
while(j<n-1) {
if(k==-1||chars[k]==chars[j]) {
k++;
j++;
next[j] = k;
} else {
k= next[k];
}
}
return next;
}
}
25 214. 最短回文串
给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
示例 1:
输入: “aacecaaa”
输出: “aaacecaaa”
示例 2:
输入: “abcd”
输出: “dcbabcd”
class Solution {
public String shortestPalindrome(String s) {
if(s==null||s.length() ==0) {
return "";
}
String s2 = s+"#" + new StringBuilder(s).reverse().toString();
return new StringBuilder(s.substring(getNext(s2.toCharArray())+1)).reverse().toString()+s;
}
private int getNext(char [] chars) {
int n = chars.length;
if(n==0) {
return -1;
}
int [] next = new int [n];
next[0] =-1;
int k =-1;
int j =0;
while(j<n-1) {
if(k==-1||chars[k]==chars[j]) {
k++;
j++;
next[j] = k;
} else {
k= next[k];
}
}
eturn next[n-1];
}
}
686. 重复叠加字符串匹配
给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。
注意:字符串 “abc” 重复叠加 0 次是 “”,重复叠加 1 次是 “abc”,重复叠加 2 次是 “abcabc”。
示例 1:
输入:a = “abcd”, b = “cdabcdab”
输出:3
解释:a 重复叠加三遍后为 “abcdabcdabcd”, 此时 b 是其子串。
示例 2:
输入:a = “a”, b = “aa”
输出:2
示例 3:
输入:a = “a”, b = “a”
输出:1
示例 4:
输入:a = “abc”, b = “wxyz”
输出:-1
提示:
1 <= a.length <= 104
1 <= b.length <= 104
a 和 b 由小写英文字母组成
通过次数13,746提交次数39,741
参考
1 https://blog.csdn.net/qq_37969433/article/details/82947411
2 https://blog.csdn.net/dark_cy/article/details/88698736