KMP算法
KMP算法用于在文本串S内查找模式串P的出现位置 时间复杂度为O(M + N)
最大长度表
前缀和后缀
前缀:除了最后一个字符以外,一个字符串的全部头部组合
后缀:除了第一个字符以外,一个字符串的全部尾部集合
前缀后缀的最长公共元素长度
举例说明,假如模式串为P = "ABCABD"
模式串 | A | B | C | A | B | D |
---|---|---|---|---|---|---|
最大前缀后缀公共元素长度 | 0 | 0 | 0 | 1 | 2 | 0 |
- “A” :前缀为空集,后缀为空集,共有元素长度为0
- “AB”:前缀为[A],后缀为[B],共有元素长度为0
- “ABC”:前缀为[A, AB],后缀为[C, BC],共有长度为0
- “ABCA”:前缀为[A, AB, ABC],后缀为[A, CA, BCA],共有长度为1
- “ABCAB”:前缀为[A, AB, ABC, ABCA],后缀为[B, AB, CAB, BCAB],共有长度为2
- “ABCABD”:前缀为[A, AB, ABC, ABCA, ABCAB],后缀为[D, BD, ABD, CABD, BCABD],共有长度为0
next数组
next数组是KMP算法的核心部分,其中next[i]
的定义为:在模板串P[0, i - 1]范围内的前缀后缀最长公共元素长度
根据定义可以知道next数组可以由最大长度表向右移动一位得到
为了计算方便next[0]
通常设为-1
示例
模板串 | A | B | C | A | B | D |
---|---|---|---|---|---|---|
next[i] | -1 | 0 | 0 | 0 | 1 | 2 |
next[i+1]的推导
假如已经匹配到了next[i]
,接下来要得到next[i+1]
,要分两种情况考虑:
1. P[i] = P[k]
模板串 | A | B | C | A | B | C | D |
---|---|---|---|---|---|---|---|
next[i] | -1 | 0 | 0 | 0 | 1 | 2 | 3 |
当i = 5
时,next[i-1] = 1
,前缀后缀最长公共子串长度为1,此时k = 0
,P[k+1] = B
,P[i] = B
,可以看到前缀后缀最长公共子串为"AB",长度为2,所以next[5] = 2
结论:next[i+1] = next[i] + 1
2. P[i] != P[k]
模板串 | A | B | C | A | B | D | D |
---|---|---|---|---|---|---|---|
next[i] | -1 | 0 | 0 | 0 | 1 | 2 | ? |
当i = 5
时,next[i] = 2
,前缀后缀最长公共子串为"AB",此时k = 2
,P[k] = C
,P[i] = D
,P[k] != P[i]
,但是因为已知前面有相同的"AB",可以找P[0,k-1] = P[0, 1]
和P[i-k+1, i] = P[4,5]
的最长重合串,根据next数组的定义可知即为求next[k]
k = next[k] = 0
,重新比较P[i] = C
和P[k] = A
,两者不匹配,此时next[k] = -1
,说明P[0, i ]
的前缀后缀最长公共子串长度为0,所以next[6] = 0
推荐看参考2的博客,这部分讲的很详细
结论:k = next[k]
个人对于k = next[k]
的理解:
当p[i] != p[k]
时,就需要找出比next[i] + 1
小的新前缀后缀最长公共子串长度。next数组本身的作用是找出[0,i-1]部分的前缀和后缀的最长公共子串长度,假设要找的新前缀后缀最长公共子串长度为
l
1
l_1
l1,相比于逐个减一去判断,可以利用next数组做优化, k = next[k]
,判断的前缀和后缀也相应的缩短到P[0,k]
和P[i-k,i]
,如果得到了新的前缀和后缀,结束迭代,否则继续k = next[k]
直至k == -1
为止
实现
public int[] createNext(String P) {
char[] p = P.toCharArray();
int[] next = new int[p.length];
int i = 0, k = -1;
while (i < p.length - 1) {
if (k == -1 || p[i] == p[k]) {
i++;
k++;
next[i] = k;
} else {
k = next[k]; // 重点是这一行
}
}
}
KMP算法
KMP算法就是利用next数组对暴力算法进行了优化。暴力算法在失配之后会回到最开始重新匹配,在S[i]
和P[j]
失配的时候,利用next数组将模板串P往右移动j - next[j]
思路
设置两个变量i = 0
和j = 0
,文本串S和模式串P
- 如果
S[i]
和P[j]
匹配,文本串和模式串继续向后匹配:i++;j++;
- 如果
S[i]
和P[j]
不匹配,保持i
不变,将模板串向右移动j - next[j]
:j = next[j];
- 如果
j = -1
,说明当前S[i]
开头的字符串无法与P匹配,j++
让模板串P回到开头P[0]
,i++
让S[i+1]
开头的字符串与P开始匹配
实现
public int KMP(String text, String template) {
char[] textChar = text.toCharArray();
char[] templateChar = template.toCharArray();
int[] next = createNext(templateChar);
int i = 0, j = 0;
while (i < textChar.length && j < templateChar.length) {
if (j == -1 || textChar[i] == templateChar[j]) {
i += 1;
j += 1;
} else if (j != -1 && textChar[i] != templateChar[j]) {
j = next[j];
}
}
if (j >= templateChar.length) {
return i - t.length; // 匹配成功
} else {
return -1;
}
}
优化
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
模式串 | A | B | C | D | A | B | D |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 |
若i = 5
时匹配失败,此时应该把i = 1
处的字符拿来继续比较,但是P[5] == P[1] == B
,两个字符一样
实现
public int[] createNext(String P) {
char[] p = P.toCharArray();
int[] next = new int[p.length];
int i = 0, k = -1;
while (i < p.length - 1) {
if (k == -1 || p[i] == p[k]) {
i++;
k++;
if (P[i] != P[k]) {
next[i] = k;
} else {
next[i] = next[k]; // 相同情况下就继续往前找
}
} else {
k = next[k];
}
}
}