[题目]
给定两个字符串
str
和
match
,长度为
N
N
N和
M
M
M。如果字符串
str
中含有子串
match
,返回
match
在
str
中开始位置
[要求]
如果match
的长度大于str
长度(
M
>
N
M>N
M>N),str
必然不会含有match
,可直接返回-1
。但如果
N
≥
M
N≥M
N≥M,要求算法复杂度为
O
(
M
)
O(M)
O(M)
1. next数组
1.1 next数组含义
next[i]
的含义是在match[i]
之前的字符串match[0..i-1]
中,必须以match[i-1]
结尾的后缀子串与必须以match[0]
开头的前缀子串最大匹配长度
- 后缀字符不能包含
match[0]
,即整个后缀不能是本身 - 前缀字符不能包含
match[i-1]
,即整个前缀不能是本身
1.2 求解next数组
match[0]
:他之前没有字符,next[0]
规定为
−
1
-1
−1
match[1]
:next数组定义要求任何子串后缀不能包含第一个字符,故match[1]
之前的字符串只有长度为
0
0
0的后缀字符串,next[1]
规定为
0
0
0
match[i]
-
从左至右依次求解
next
,求解next[i]
时,next[i-1]
已经求出
如图I区域
是最长匹配前缀,k区域
是最长匹配后缀 -
如果C和B相等,A之前最长公共前后缀就可以确定,前缀子串为I区域+C,后缀子串为k区域+B,即next[i]=next[i-1]+1
-
如果C和B不相等,就要看C之前的前缀和后缀匹配情况
假设字符C是第cn
个字符,那么next[cn]
就是其最长前缀和最长后缀的匹配长度
m
、n
区域是相等的,m'
区域为k
区域最右的区域且长度与m
区域相等,字符D是n
区域后面一个字符,所以
接下来比较字符D和字符B是否相等- 如果B、D相等,A字符之前的最长前缀与后缀匹配区域就可以确定,前缀区域为
n
区域+D,后缀区域是m'
区域+B,则令next[i]=next[cn]+1 - 如果B、D不等,继续往前跳到D字符,每一步都有新的字符和B比较,只要相等的情况,
next[i]
就确定
- 如果B、D相等,A字符之前的最长前缀与后缀匹配区域就可以确定,前缀区域为
-
如果跳到最左位置(
match[0]
位置),此时next[0]=-1
,说明字符A之前的字符串不存在前缀和后缀的匹配情况,则next[i]=0
2. KMP
假设从str[i]
字符出发时,匹配到j
位置的字符发现与match
中的字符不一致,
现在有match
字符串的next
数组,next[j-i]
表示match[0..j-i-1]
这段字符前缀与后缀的最长匹配
下一次匹配检查让str[i]
与match[k]
进行匹配检查,对于match[]
来说,相当于向右滑动,让match[k]
滑动到与str[j]
在同一个位置上,然后进行后续的匹配检查,一直进行这样的滑动匹配,直到在str某一位置把match
完全匹配,说明str
中有match
,如果滑动到最后也没匹配出来,说明str
中没有match
为什么中间不要检查,必然匹配不了呢
在str[j]
位置匹配失败,b
区域与c
区域相等,a
区域与b
区域相等,必然a
区域与c
区域相等
中间的区域不需要检查,直接将a
区域滑动到与c
区域对齐即可
假设d
区域开始字符是不要检查区域的一个位置,如果这位置开始匹配出match
,整个d
区域要和从match[0
]开始的e
区域匹配,d
和e
的长度一样,d
区域比c
区域大,e
区域比a
区域大
d'
区域和d
区域一样大,e
区域此时是最大前缀,d’
区域此时是最大后缀,这与此时next[j-i]
的值(a
、b
长度)矛盾,所以必然不相等
3. 时间复杂度分析
str
匹配位置是不退回的,match
一直向右移动,如果在str
某个位置完全匹配出match
,整个过程停止,否则match
滑动到str
最右侧停止,滑动最大长度为
N
N
N,所以时间复杂度为
O
(
N
)
O(N)
O(N)
4. 算法实现
4.1 getNext
public int[] getNext(char[] match) {
if (match.length == 1) {
return new int[]{-1};
}
int[] next = new int[match.length];
next[0] = -1;
next[1] = 0;
int i = 2;
int j = 0;
while (i < next.length) {
if (match[i - 1] == match[j]) { // 相等,匹配下一个
next[i++] = ++j;
} else if (j > 0) { // 不相等往前跳,继续匹配
j = next[j];
} else { // 来到最左边情况,说明i之前字符不存在前后缀匹配情况,匹配下一个
next[i++] = 0;
}
}
return next;
}
4.2 KMP
public int KMP(String str, String match) {
if (str == null || match == null ||
match.length() < 1 || str.length() < match.length()) {
return -1;
}
char[] mainStr = str.toCharArray();
char[] subStr = match.toCharArray();
int i = 0;
int j = 0;
int[] next = getNext(subStr);
while (i < mainStr.length && j < subStr.length) {
if (mainStr[i] == subStr[j]) {
i++;
j++;
} else if (next[j] == -1) { // 往前跳到开头都匹配不出来,找主串下一个位置开始匹配
i++;
} else { // 当前匹配不出来且没有跳到子串开头,往前跳
j = next[j];
}
}
return j == subStr.length ? i - j : -1;
}