一、KMP算法
KMP算法的理解请参考上一篇博客:http://blog.youkuaiyun.com/roy_70/article/details/78321930
这里说一下具体如何来用代码实现KMP算法
二、部分匹配表
接下来说一下部分匹配表是如何生成的。首先,要了解两个概念:”前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
例如:单词level的前缀有{l,le,lev,leve}四个,后缀有{evel,vel,el,l}四个,然后前缀和后缀集合取交集,得到{l},只有一个元素,长度为1,因此level的部分匹配值就是1。
搜索词 | Q | W | E | R | Q | W | R |
---|---|---|---|---|---|---|---|
部分匹配值 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
再回来看上面的字符串例子:
“Q”的前缀和后缀都为空集,共有元素的长度为0;
“QW”的前缀为[Q],后缀为[W],共有元素的长度为0;
“QWE”的前缀为[Q, QW],后缀为[WE, E],共有元素的长度0;
“QWER”的前缀为[Q, QW, QWE],后缀为[WER, ER, R],共有元素的长度为0;
“QWERQ”的前缀为[Q, QW, QWE, QWER],后缀为[WERQ, ERQ, RQ, Q],共有元素为”Q”,长度为1;
“QWERQW”的前缀为[Q, QW, QWE, QWER, QWERQ],后缀为[WERQW, ERQW, RQW, QW, W],共有元素为”QW”,长度为2;
“QWERQWR”的前缀为[Q, QW, QWE, QWER, QWERQ, QWERQW],后缀为[WERQWR, ERQWR, RQWR, QWR, WR, R],共有元素的长度为0。
三、next数组
由上文,我们已经知道,字符串“QWERQWR”各个前缀后缀的最大公共元素长度分别为:
搜索词 | Q | W | E | R | Q | W | R |
---|---|---|---|---|---|---|---|
部分匹配值 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。
给定字符串“ABCDABD”,可求得它的next 数组如下:
搜索词 | Q | W | E | R | Q | W | R |
---|---|---|---|---|---|---|---|
部分匹配值 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
next数组 | -1 | 0 | 0 | 0 | 0 | 1 | 2 |
把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。
所以我们求得next数组的方法代码为:
public int[] getNext(char[] p) {
// 已知next[j] = k,利用递归的思想求出next[j+1]的值
// 如果已知next[j] = k,如何求出next[j+1]呢?具体算法如下:
// 1. 如果p[j] = p[k], 则next[j+1] = next[k] + 1;
// 2. 如果p[j] != p[k], 则令k=next[k],如果此时p[j]==p[k],则next[j+1]=k+1,
// 如果不相等,则继续递归前缀索引,令 k=next[k],继续判断,直至k=-1(即k=next[0])或者p[j]=p[k]为止
int pLen = p.length;
int[] next = new int[pLen];
int k = -1;
int j = 0;
next[0] = -1; // next数组中第一位next[0]为-1
while (j < pLen - 1) {
if (k == -1 || p[j] == p[k]) {
k++;
j++;
next[j] = k;
} else {
k = next[k];
}
}
return next;
}
三、KMP算法代码
public class KMP {
public int indexOf(String source, String pattern) {
int i = 0, j = 0;
char[] src = source.toCharArray();
char[] ptn = pattern.toCharArray();
int sLen = src.length;
int pLen = ptn.length;
int[] next = getNext(ptn);
while (i < sLen && j < pLen) {
// 如果j = -1,或者当前字符匹配成功(src[i] = ptn[j]),都让i++,j++
if (j == -1 || src[i] == ptn[j]) {
i++;
j++;
} else {
// 如果j!=-1且当前字符匹配失败,则令i不变,j=next[j],即让pattern模式串右移j-next[j]个单位
j = next[j];
}
}
if (j == pLen)
return i - j;
return -1;
}
public int[] getNext(char[] p) {
// 已知next[j] = k,利用递归的思想求出next[j+1]的值
// 如果已知next[j] = k,如何求出next[j+1]呢?具体算法如下:
// 1. 如果p[j] = p[k], 则next[j+1] = next[k] + 1;
// 2. 如果p[j] != p[k], 则令k=next[k],如果此时p[j]==p[k],则next[j+1]=k+1,
// 如果不相等,则继续递归前缀索引,令 k=next[k],继续判断,直至k=-1(即k=next[0])或者p[j]=p[k]为止
int pLen = p.length;
int[] next = new int[pLen];
int k = -1;
int j = 0;
next[0] = -1; // next数组中next[0]为-1
while (j < pLen - 1) {
if (k == -1 || p[j] == p[k]) {
k++;
j++;
next[j] = k;
} else {
k = next[k];
}
}
return next;
}
}
测试代码:
public static void main(String[] args){
KMP kmp = new KMP();
String a = "QWERQWR";
String b = "WWE QWERQW QWERQWERQWRT";
int[] next = kmp.getNext(a.toCharArray());
for(int i = 0; i < next.length; i++){
System.out.println(a.charAt(i)+" "+next[i]);
}
int res = kmp.indexOf(b, a);
System.out.println(res);
}
执行结果:
Q -1
W 0
E 0
R 0
Q 0
W 1
R 2
15