1.寻找最长前缀后缀
如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:
也就是说,原字符串对应的各个前缀后缀的公共元素的最大长度表为(
下简称《最大长度表》):
2.根据《最大长度表》求出next 数组
由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:
而且,根据这个表可以得出下述结论
- 失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。
给定字符串“ABCDABD”,可求得它的next 数组如下:
把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1!
换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:
根据最大长度表求出了next 数组后,从而有
失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值
而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:
- 根据《最大长度表》,失配时,模式串向右移动的位数 = 已经匹配的字符数 - 失配字符的上一位字符的最大长度值
- 而根据《next 数组》,失配时,模式串向右移动的位数 = 失配字符的位置 - 失配字符对应的next 值
- 其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字符不计数),而失配字符对应的next 值 = 失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。
3.下面,我们来基于next 数组进行匹配。
还是给定 文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:
在正式匹配之前,让我们来再次回顾下上文2.1节所述的KMP算法的匹配流程:
- “假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
- 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
- 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
- 换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。”
- 1. 最开始匹配时
- P[0]跟S[0]匹配失败
- 所以执行“如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]”,所以j = -1,故转而执行“如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++”,得到i = 1,j = 0,即P[0]继续跟S[1]匹配。
- P[0]跟S[1]又失配,j再次等于-1,i、j继续自增,从而P[0]跟S[2]匹配。
- P[0]跟S[2]失配后,P[0]又跟S[3]匹配。
- P[0]跟S[3]再失配,直到P[0]跟S[4]匹配成功,开始执行此条指令的后半段:“如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++”。
- P[0]跟S[0]匹配失败
- 2. P[1]跟S[5]匹配成功,P[2]跟S[6]也匹配成功, ...,直到当匹配到字符D时失配(即S[10] != P[6]),由于 j 从0开始计数,故数到失配的字符D时 j 为6,且字符D对应的next 值为2,所以向右移动的位数为:j - next[j] = 6 - 2 =4 位
- 3. 向右移动4位后,C再次失配,向右移动:j - next[j] = 2 - 0 = 2 位
- 4. 移动两位之后,A 跟空格不匹配,再次后移1 位
- 5. D处失配,向右移动 j - next[j] = 6 - 2 = 4 位
- 6. 匹配成功,过程结束。
匹配过程一模一样。也从侧面佐证了,next 数组确实是只要将各个最大前缀后缀的公共元素的长度值右移一位,且把初值赋为-1 即可。
Java代码:
package JavaAlgorithm;
/**
* 思想:每当一趟匹配过程中出现字符比较不等,不需要回溯i指针,
* 而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远
* 的一段距离后,继续进行比较。
* 时间复杂度O(n+m)
*/
public class KMP {
public static void main(String[] args) {
String s = "abbabbbbcab"; // 主串
String t = "bab"; // 模式串
char[] ss = s.toCharArray();
char[] tt = t.toCharArray();
// System.out.println(ss);
// System.out.println(tt);
System.out.println(KMP_Index(ss, tt)); // KMP匹配字符串
}
public static int[] next(char[] t)
{
int[] next = new int[t.length];
next[0] = -1;//0位置初始化为-1
int i = 0;
int k = -1;
while (i < t.length - 1)
{
if (k == -1 || t[i] == t[k]) //t[i]为前缀,t[k]为后缀。当与主串不匹配时:在next[i]的位置所应当回朔的位置为K
{
i++;//i一直加一,寻找与第一个字符相同的字符
k++;
next[i] = k;
}
else
k = next[k];//k重置为next[0]=-1
}
return next;
}
public static int KMP_Index(char[] s, char[] t)
{
int[] next = next(t);
int i = 0;//i为主串下标
int k = 0;//k为子串下标
while (i <= s.length - 1 && k <= t.length - 1)
{
if (k == -1 || s[i] == t[k])
{
i++;
k++;
// next[i] = k; 对比next函数,没有这一步
}
else
k = next[k];//这个地方子串右移k-next[k]
//当不满足IF判定时,则将当前匹配失败的回朔位置传递给K,使之进入到下一次运算。这样如果下一次一开始就匹配失败,则直接写入next[k]的回朔值就OK了。
}
if (k < t.length)
return -1;
else
return i - t.length; // 返回模式串在主串中的头下标
}
}