使用 KMP 算法解决字符串匹配问题
使用 KMP 算法解决字符串匹配问题
作者:Grey
原文地址:
要解决的问题
假设字符串str
长度为N
,字符串match
长度为M
,M <= N
, 想确定str
中是否有某个子串是等于match
的。返回和match
匹配的字符串的首字母在str
的位置,如果不匹配,则返回-1
OJ可参考:LeetCode 28. 实现 strStr()
暴力方法
从str
串中每个位置开始匹配match
串,时间复杂度O(M*N)
KMP算法
KMP算法可以用O(N)
时间复杂度解决上述问题。
流程
我们规定数组中每个位置的一个指标,这个指标定义为
这个位置之前的字符前缀和后缀的匹配长度,不要取得整体。
例如: ababk
这个字符串,k
之前位置的字符串为abab
,前缀ab
等于 后缀ab
,长度为2,所以k
位置的指标为2,
下标为3的b
之前的字符串aba
,前缀a
等于后缀a
, 长度为1,所以下标为3的b
的指标为1,
人为规定:下标为0的字符的指标是-1,下标为1的字符的指标0
假设match串中每个位置我们都已经求得了这个指标值,放在了一个next
数组中,这个next
数组有助于我们加速整个匹配过程。
假设在某个时刻,匹配的到的字符如下
其中str的i..j
一直可以匹配上match
串的0...m
, str
中的x
位置和match
串中的y
位置第一次匹配不上。如果使用暴力方法,此时我们需要从str
的i+1
位置重新开始匹配match
串的0位置,这样算法的复杂度比较高。
而使用KMP算法,利用next
数组,可以加速这一匹配过程,具体流程是,我们可以先得到y
位置的next
数组信息,假设y
的next
数组信息是2,如下图
那么0...k
这一段完全等于f...m
这一段,所以对于match
来说,当y
位置匹配不上x
位置以后, 无需从i+1
开始匹配0位置的值,而是可以直接让x
位置匹配位置p
上的值,如下图
其中p
的位置由y
位置的next
数组确定,因为y
的next
数组值为2(即p
位置)。
如果匹配上了,则x
来到下一个位置(即x+1
位置),p
来到下一个位置(即f
位置)继续匹配,如果再次匹配不上,假设p
位置的next
数组值为0, 则继续用x
匹配0
位置上的值,如下图
如果x
位置的值依旧不等于0
位置的值,则宣告本次匹配失败,str
串来到x
下一个位置,match
串从0
位置开始继续匹配。
next数组求解
next
数组的求解是KMP算法中最关键的一步,要快速求解next
数组,需要做到当我们求i
位置的next
信息时,能通过i-1
的next
数组信息加速求得,如下图
当我们求i
位置的next
信息时,假设j
位置的next
信息为6,则表示
m...n
这一段字符串等于s...t
这一段字符,此时可以得出一个结论,如果:
x
位置上的字符等于j
位置上的字符,那么i
位置上的next
信息为j
位置上的next
信息加1,即为7。如果不等,则继续看x
位置上的next
信息,假设为2,则有:
此时,判断q
位置的值是否等于j
位置的值,如果相等,那么i
位置上的next
信息为x
位置上的next
信息加1,即为3,如果不等,则继续看q
位置上的next
信息,假设为1,那么有
此时,判断p
位置的值是否等于j
位置的值,如果相等,那么i
位置上的next
信息为q
位置上的next
信息加1,即为2,如果不等,则继续如上逻辑,如果都没有匹配上j
位置的值,则i
位置的next
信息为0。
主流程代码复杂度估计
public class LeetCode_0028_ImplementStrStr {
public static int strStr(String str, String match) {
if (str == null || match == null || match.length() > str.length()) {
return -1;
}
if (match.length() < 1) {
return 0;
}
char[] s = str.toCharArray();
char[] m = match.toCharArray();
int[] next = getNextArr(m);
int x = 0;
int y = 0;
while (y < s.length && x < m.length) {
if (s[y] == m[x]) {
y++;
x++;
} else if (x != 0) {
x = next[x];
} else {
y++;
}
}
return x == m.length ? y - x : -1;
}
// 求解next数组逻辑
private static int[] getNextArr(char[] str) {
if (str.length == 1) {
return new int[]{-1};
}
int[] next = new int[str.length];
next[0] = -1;
next[1] = 0;
int i = 2; // 目前在哪个位置上求next数组值
int cn = 0; // 前后缀最长字符的长度,也表示下一个要比的信息位置
while (i < next.length) {
if (str[i - 1] == str[cn]) {
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
return next;
}
}
现在估计主流程的复杂度,主流程的下述while
循环中,x
能取得的最大值为str字符串的长度N,定义一个变量x-y
,能取得的最大值不可能超过N(即当x = N,y=0时候),在主流程的wile循环中,有三个分支
if (s[y] == m[x]) {
y++;
x++;
} else if (x != 0) {
x = next[x];
} else {
y++;
}
我们考虑这三个分支对于y
和y - x
变化范围的影响
分支 | y | y - x |
---|---|---|
x++; y++ |
推高 | 不变 |
x = next[x] |
不变 | 推高 |
y++ |
推高 | 推高 |
如上分析,y
和y-x
都不可能降低,且三个分支只能中一个,所以,而y
和y-x
的最大值均为N
,所有分支执行总推高的次数不可能超过2N
。即得出主流程的复杂度O(N)
next
数组的求解流程时间复杂度类似,也有三个分支
if (str[i - 1] == str[cn]) {
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
我们可以设置两个变量
分支 | i | i - cn |
---|---|---|
next[i++] = ++cn; |
推高 | 推高 |
cn = next[cn]; |
不变 | 推高 |
next[i++] = 0; |
推高 | 不变 |
如上分析,i
和i-cn
都不可能降低,且三个分支只能中一个,所以,而i
和i-cn
的最大值均为M
,所有分支执行总推高的次数不可能超过2M。即得出流程的复杂度O(M)
KMP算法应用
求一个字符串的旋转词
链接: LeetCode 796. Rotate String
思路
将这个字符串拼接一下, 比如原始串为:
123456
,拼接成:123456123456
如果匹配的字符串是这个拼接的字符串的子串,则互为旋转词。
一棵二叉树是否为另外一棵二叉树的子树
链接: LeetCode 572. Subtree of Another Tree
思路
先将两棵树分别序列化为数组
A
和数组B
,如果B
是A
的子串,那么A对应的二叉树中一定有某个子树的结构和B对应的二叉树完全一样。
更多
参考资料
本文来自博客园,作者:Grey Zeng,转载请注明原文链接:https://www.cnblogs.com/greyzeng/p/15317466.html