码迷,mamicode.com
首页 > 编程语言 > 详细

KMP算法

时间:2016-07-19 11:07:09      阅读:226      评论:0      收藏:0      [点我收藏+]

标签:

左老师KMP算法:

KMP算法
【题目】
给定两个字符串str和match,?度分别为N和M。实现?个算法,如果字符串str
中含有字串match,则返回match在str中的开始位置,不含有则返回-1。
【举例】
str=“acbc”, match=“bc”。 返回2。
str=“acbc”, match=“bcc”。返回-1。
【要求】
如果match的?度?于str?度(M>N), str必然不会含有match,可直接返回-1。
但如果N>=M,要求算法复杂度O(N)。
【难度】
将 ★★★★
【解答】
本?是想重点介绍?下KMP算法,该算法是由Donald Knuth、 Vaughan Pratt和James H.
Morris于1977年联合发明的。在介绍KMP算法之前,我们先来看看普通解法怎么做。
最普通的解法是从左到右遍历str的每?个字符,然后看看如果以当前字符作为第?个字符
出发是否匹配出match。?如str=“aaaaaaaaaaaaaaaaab”, match=“aaaab”。从str[0]出
发,开始匹配。匹配到str[4]==‘a’时发现和match[4]==‘b’不?样,所以匹配失败。说明从
str[0]出发是不?的。从str[1]出发,开始匹配。匹配到str[5]==’a’时发现和match[4]==’b’不
?样,所以匹配失败。说明从str[1]出发是不?的。从str[2..12]出发,都会?直失败。从
str[13]出发,开始匹配。匹配到str[17]==’b’时发现和match[4]==’b’?样, match已经全部匹
配完,说明匹配成功,返回13。普通解法的时间复杂度较?,从每个字符出发时,匹配的
代价都可能是O(M),那么?共有N个字符,所以整体的时间复杂度为O(N*M)。普通解法时间
复杂度这么?,是因为每次遍历到?个字符,检查?作相当于从?开始,之前的遍历检查
不能优化当前的遍历检查。
下?介绍KMP算法是如何快速的解决字符串匹配问题的。
1,?先?成match字符串的nextArr数组,这个数组的?度与match字符串的?度?样,
nextArr[i]的含义是在match[i]之前的字符串match[0..i-1]中,必须以match[i-1]结尾的后缀
?串(不能包含match[0])与必须以match[0]开头的前缀?串(不能包含match[i-1])最?匹配
?度是多少。这个?度就是nextArr[i]的值。?如, match=“aaaab”这个字符串, nextArr[4]
的值该是多少呢? match[4]==’b’,所以它之前的字符串为”aaaa”,根据定义这个字符串的后
缀?串和前缀?串最?匹配为”aaa”。也就是当后缀?串等于match[1..3]==”aaa”,前缀?串
等于match[0..2]==”aaa”的时候,这时前缀和后缀不仅相等,?且所有前缀和后缀的可能性
中最?的匹配。所以nextArr[4]的值等于3。再?如, match=”abc1abc1”这个字符串,
nextArr[7]的值该是多少呢? match[7]==’1’,所以它之前的字符串为”abc1abc”,根据定义这
个 字 符 串 的 后 缀 ? 串 和 前 缀 ? 串 最 ? 匹 配 为 ” a b c ” 。 也 就 是 当 后 缀 ? 串 等 于
match[4..6]==”abc”,前缀?串等于match[0..2]==”abc”的时候,这时前缀和后缀不仅相
等,?且所有前缀和后缀的可能性中最?的匹配。所以nextArr[7]的值等于3。关于如何快
速的得到nextArr数组的问题,我们在把KMP算法?概过程介绍完毕之后再详细的说明,接
下来先看看如果有了match的nextArr数组,如何加速str和match的匹配过程。
2,假设从str[i]字符出发时,匹配到j位置的字符发现与match中的字符不?致,也就是说
str[i]与match[0]?样,并且从这个位置开始?直可以匹配,即str[i..j-1]与match[0..j-i-1]
?样,直到发现str[j]!=match[j-i],匹配停?。如图1-1。
技术分享
图1-1

因为现在已经有了match字符串的nextArr数组, nextArr[j-i]的值表?了match[0..j-i-1]
这?段字符串前缀与后缀的最?匹配。假设前缀是图1-2中的a区域这?段,后缀是图1-2中
的b区域这?段,再假设a区域的下?个字符为match[k]。如图1-2。
技术分享
图1-2
那么下?次的匹配检查,不再像普通解法那样退回到str[i+1]重新开始与match[0]的匹
配过程,?是直接让str[j]与match[k]进?匹配检查。如图1-3。
技术分享
图1-3
如图1-3,在str中要匹配的位置仍是j,?不进?退回。对match来说,相当于向右滑
动,让match[k]滑动到与str[j]同?个位置上,然后进?后续的匹配检查。普通解法str要退
回到i+1位置,然后让str[i+1]与match[0]进?匹配,?我们的解法在匹配的过程中?直进?
这样的滑动匹配的过程,直到在str的某?个位置把match完全匹配完,就说明str中有
match。如果match滑到最后也没匹配出来,就说明str中没有match。那么为什么这样做是
正确的呢?如图1-4。
技术分享
图1-4
在图1-4中,匹配到A字符和B字符才发?的不匹配,所以c区域等于b区域, b区域?与a
区域相等(因为nextArr的含义如此),所以c区域和a区域是不需要检查的,必然会相等。所
以直接把字符C滑到字符A的位置开始检查即可。其实这个过程相当于是从str的c区域的第?
个字符重新开始的匹配过程(c区域的第?个字符和match[0]匹配,并往右的过程),只不过
因为c区域与a区域?定相等,所以省去了这个区域的匹配检查?已,直接从字符A和字符C
往后继续匹配检查。读者看到这?肯定会问,为什么开始的字符从str[i]直接跳到c区域的第
?个字符呢?中间的这?段为什么是”不?检查”的区域呢?因为在这个区域上,从任何?
个字符出发都肯定匹配不出match,下?还是图解来解释这?点。如图1-5。
技术分享
图1-5
图1-5中,假设d区域开始的字符是“不?检查”区域的其中?个位置,如果从这个位置
开始能够匹配出match,那么毫?疑问,起码整个d区域应该和从match[0]开始的e区域匹
配,即d区域与e区域?度?样,且两个区域的字符都相等。同时我们注意到, d区域?c区
域?, e区域?a区域?。如果这种情况发?了,假设d区域对应到match字符串中是d’区
域,也就是字符B之前的字符串的后缀,?e区域本?就是match的前缀,所以对于match来
说,相当于找到了B这个字符之前的字符串(match[0..j-i-1])的?个更?的前缀与后缀匹
配,?个?a区域和b区域更?的前缀后缀匹配, e区域和d’区域。这与nextArr[j-i]的值是?
相?盾的,因为nextArr[j-i]的值代表的含义就是match[0..j-i-1]字符串上最?的前缀与后缀
匹配?度。所以如果match字符串的nextArr数组计算正确,这种情况绝不会发?,也就是
说根本不会有更?的d’区域和e区域,所以d区域与e区域也必然不会相等。
匹配过程分析完毕,我们知道, str中的匹配的位置是不退回的, match则?直向右滑动,
如果在str中的某个位置完全匹配出match,整个过程停?。否则match滑到str的最右侧过程
也停?,所以滑动的?度最?为N,所以时间复杂度为O(N)。匹配的全部过程参看如下代码
中的getIndexOf?法。
public int getIndexOf(String s, String m) {
if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
return -1;
}
char[] ss = s.toCharArray();
char[] ms = m.toCharArray();
int si = 0;
int mi = 0;
int[] next = getNextArray(ms);
while (si < ss.length && mi < ms.length) {
if (ss[si] == ms[mi]) {
si++;
mi++;
} else if (next[mi] == -1) {
si++;
} else {
mi = next[mi];
}
}
return mi == ms.length ? si - mi : -1;
}
最后我们需要解释如何快速得到match字符串的nextArr数组,并且要证明得到nextArr数组
的时间复杂度为O(M)。对于match[0]来说,它的之前没有字符,所以nextArr[0]规定为-1。
对于match[1]来说,它的之前有match[0],但nextArr数组的定义要求任何?串的后缀不能
包括第?个字符(match[0]),所以match[1]之前的字符串,只有?度为0的后缀字符串,所
以nextArr[1]为0。之后对于match1”>i来说,求解过程如下:
1,因为是左到右依次求解nextArr,所以在求解nextArr[i]时, nextArr[0..i-1]的值都已经求
出了。假设match[i]字符为图1-6中的A字符, match[i-1]为图1-6中的B字符,如图1-6。
技术分享
图1-6
通过nextArr[i-1]的值可以知道B字符前的字符串的最?前缀与后缀匹配区域,图1-6中的l区
域为最?匹配的前缀?串, k区域为最?匹配的后缀?串,图1-6中字符C为l区域之后的字
符。然后看看字符C与字符B是否相等。
2,如果字符C与字符B相等,那么A字符之前的字符串的最?前缀与后缀匹配区域就可以确
定了,前缀?串为l区域+C字符,后缀?串为k区域+B字符,即nextArr[i]=nextArr[i-1]+1。
3,如果字符C与字符B不相等,就考察字符C之前的前缀后缀匹配情况,假设字符C是第cn个
字符(match[cn]),那么nextArr[cn]就是其最?前缀后缀匹配?度,如图1-7。
技术分享
图1-7
图1-7中, m区域和n区域分别是字符C之前的字符串的最?匹配的后缀与前缀区域,这
是通过通过nextArr[cn]的值确定的,当然两个区域是相等的, m’区域为k区域最右的区域且
?度与m区域?样,因为k区域和l区域是相等的,所以m区域和m’区域也相等,字符D为n区
域之后的?个字符,接下来?较字符D是否与字符B相等。
1)如果相等, A字符之前的字符串的最?前缀与后缀匹配区域就可以确定了,前
缀?串为n区域+D字符,后缀?串为m’区域+B字符,则令nextArr[i]=nextArr[cn]+1;
2)如果不等,继续往前跳到字符D,之后的过程与跳到字符C类似,?直进?这样
的跳过程,跳的每?步都会有?个新的字符拿出来和B?较(就像C字符和D字符?样),只要
有相等的情况,那nextArr[i]的值就能确定。
4,如果向前跳到最左位置即match[0]的位置,此时nextArr[0]==-1,说明字符A之前的字符
串不存在前缀后缀匹配的情况,则令nextArr[i]=0。?这种不断向前跳的?式可以算出正确
的nextArr[i]的值的原因,还是因为每跳到?个位置cn, nextArr[cn]的意义就是表?它之前
字符串的最?匹配?度。求解nextArr数组的具体过程请参看如下代码中的getNextArray?
法,先看代码然后分析这个过程为什么时间复杂度为O(M)。
public int[] getNextArray(char[] ms) {
if (ms.length == 1) {
return new int[] { -1 };
}
int[] next = new int[ms.length];
next[0] = -1;
next[1] = 0;
int pos = 2;
int cn = 0;
while (pos < next.length) {
if (ms[pos - 1] == ms[cn]) {
next[pos++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[pos++] = 0;
}
}
return next;
}
getNextArray?法中的while循环就是求解nextArr数组的过程,现在证明这个循环发?的次
数不会超过2M这个数量。先来看看两个量,?个为pos量,?个为(pos-cn)的量。对于pos量
来说,从2开始?必然不会?于match的?度,即pos

KMP算法

标签:

原文地址:http://blog.csdn.net/fay625/article/details/51933677

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!