假设现在我们面临这样一个问题:有一个主串(文本串)S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?
如果使用暴力匹配的思路,并假设现在主串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
public static int violentMatch(String s, String p) {
int sLen = s.length();
int pLen = p.length();
int i = 0;//主串遍历指针
int j = 0;//模式串遍历指针
while (i < sLen && j < pLen) {
if (s.charAt(i) == p.charAt(j)) {//如果匹配,则i++,j++;
i++;
j++;
} else {//如果失配,令i = i - (j - 1),j = 0;
i = i - j + 1;
j = 0;
}
}
//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
if (j == pLen) return i - j;
else return -1;
}
举个例子,如果给定文本串S=“BBC ABCDAB ABCDABCDABDE”,和模式串P=“ABCDABD”,现在要拿模式串P去跟文本串S匹配,整个过程如下所示:
下面先直接给出KMP的算法流程:假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置;
下面是对上面思想的一些解释或者补充:
public static int kmpMatch(String s, String p) {
int i = 0;
int j = 0;
int sLen = s.length();
int pLen = p.length();
int [] next=new int[pLen];
getNext(p,next);//先求模式串p的next数组
while (i < sLen && j < pLen) {//KMP匹配过程
if (j == -1 || s.charAt(i) == p.charAt(j)) {//如果j == -1,或匹配成功,令i++,j++ ;
i++;
j++;
} else {//如果j != -1,且失配,则令 i 不变,j = next[j]不回溯
j = next[j];
}
}
if (j == pLen)
return i - j;
else
return -1;
}
这只是一个匹配失败时的举例,基于KMP的完整匹配过程会在后面给出。
对于某一个字符串,它的前缀、后缀分别定义为:
例如:给定字符串P “abcba”,P的前缀集合为{ a , ab , abc , abcb },前缀集合为{ bcba , cba , ba , a } ;P的公共前缀后缀只有一个{ a },所以P的最长公共前缀后缀的长度为1;
对于2.3中给的例子,模式串P=“ABCDABD”,其各个前子串的前缀后缀分别如下表格所示:
也就是说,模式串各个前子串对应的最长公共前缀后缀长度表为(下简称《最大长度表》):
next 数组的求法:最大长度表右移一位,next [0]赋值为-1;
对于2.3中给的例子,模式串P=“ABCDABD”,其最大长度表和next 数组表为:
已知next [0, …, j],如何求next [j + 1]呢?
首先j为0时, next [ j ] = -1;假设k= next [ j ];然后执行下面递归过程:
下面是一些补充或解释:
(1)已知k=next [ j ],所以在pj前存在p0 p1, …, pk-1 = pj-k, pj-k+1, …, pj-1;
(2)现在要求next [ j+1 ],先判断pk?=pj;如果相等,那么pj+1前存在p0 p1, …, pk-1,pk = pj-k, pj-k+1, …, pj-1,pj长度为k+1的最大公共前缀后缀,所以next[ j + 1 ] = k + 1;
(3)如果pk!=pj,就去找一个长度更小的公共前缀后缀;递归比较pnext [k]?=pj,如果相等,则next[ j + 1 ] = k + 1 = next [ k ] + 1;否则继续让k=next [k],然后比较pk!和pj;
至于为什么递归地令(下一个)k=next [k]就能够找到的长度更小的公共前缀后缀,我的理解是模式串的自我匹配;详情可以参考原文博客;
求next数组其实就是一个动态规划问题,dp方程为next[ j + 1 ] = next [ j ] + 1 ;
public static void getNext(String p,int [] next){
int pLen = p.length();
int j = 0;
next[j] = -1;
int k = next[j];
while (j < pLen - 1) {
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p.charAt(j) == p.charAt(k)) {
next[++j] = ++k;
} else {
k = next[k];
}
}
}
用代码重新计算下“ABCDABD”的next 数组:
还是之前2.2的例子,文本串S=“BBC ABCDAB ABCDABCDABDE”,模式串P=“ABCDABD”:
用之前的next 数组方法求模式串“abab”的next 数组,可得其next 数组为-1 0 0 1(0 0 1 2整体右移一位,初值赋为-1),当它跟下图中的文本串去匹配的时候,发现b跟c失配,于是模式串右移j - next[j] = 3 - 1 =2位。
右移2位后,b又跟c失配。事实上,因为在上一步的匹配中,已经得知p[3] = b,与s[3] = c失配,而右移两位之后,让p[ next[3] ] = p[1] = b 再跟s[3]匹配时,必然失配。问题出在哪呢?
问题出在:当出现p[j] = p[ next[j] ]时,这次比较是非必要的。为什么呢?理由是:当p[j] != s[i] 时,下次匹配必然是p[ next [j]] 跟s[i]匹配,如果p[j] = p[ next[j] ],必然导致后一步匹配失败(因为p[j]已经跟s[i]失配,然后你还用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很显然,必然失配),所以不能允许p[j] = p[ next[j ]]。如果出现了p[j] = p[ next[j] ]咋办呢?如果出现了,则需要再次递归,即令next[j] = next[ next[j] ]。
优化策略:当出现p[j] = p[ next[j ]]时,再次递归,令next[j] = next[ next[j] ];
ps:当然优化后,仍然可能会出p[j] = p[ next[j ]]=p[ next[ next[j ] ]],我们可以再递归一次;当然满足这种这种场景的出现是小概率事件,也就没必要再递归一次了;
只要求出了原始next 数组,便可以根据原始next 数组快速求出优化后的next 数组。还是以abab为例,如下表格所示:
public static void getNext(String p,int [] next){
int pLen = p.length();
int j = 0;
next[j] = -1;
int k = next[j];
while (j < pLen - 1) {
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p.charAt(j) == p.charAt(k)) {
//当p[k] == p[j]或者 k为-1时,则next[ j + 1 ] = next [ j ] + 1 = k + 1
++j;
++k;
if (p.charAt(j) == p.charAt(k)) {
//优化处理:当出现p[j] = p[next[j]]时,再次递归,k = next[k];
next[j] = next[k];
}else {
next[j]= k;
}
} else {//当p[k] != p[j]且k不为-1时,k=next [k];
k = next[k];
}
}
}
假设主串长度为n,模式串长度为m;
暴力法时间复杂度为O(m*n);
KMP算法求next数组时间复杂度为O(m),匹配过程时间复杂度为O(m),总的时间复杂度为O(m+n);
转自:从头到尾彻底理解KMP