完全搞懂KMP算法
来源:互联网 发布:网络球机控制线接线图 编辑:程序博客网 时间:2024/05/20 09:47
在串的模式匹配算法中,KMP算法称得上是经典算法了。简约而不简单,是对KMP算法最恰当的评价。该算法是由D.E.Knuth,J.H.Morris和V.R.Pratt三人同时发现,因此人们称它为KMP算法。废话不多说,现在开始进入正题。
首先声明两点:
【1】字符串 采用定长顺序存储结构,主串用S表示,模式串用T来表示。用n来表示主串S的有效字符长度,用m来表示子串T中有效字符的长度。
【2】数组下标从0开始,但要注意,数组S[0]和T[0] 中存放的是串的长度。也就是说,字符串从S[1]和T[1]开始。(与严蔚敏老师的书保持一致)
主串S: ababcababa 用 i 指示主串中正待比较的字符
模式串T: ababa 用 j 指示子串中正待比较的字符
目标:对于给定的主串S=‘ababcababa’ 和模式串T='ababa',判断串T 是否在 串S中,如果在,返回起始位置。
想要实现该目标,有很多方法,本文只介绍两种,即BF算法和KMP算法。
Brute Force(BF或蛮力搜索) 算法
首先,让我们完全从直觉出发,来认识一下最简单的字符串模式匹配算法——BF算法(又叫朴素的串匹配算法)。
BF算法是最笨的算法,也是一看就懂的算法。该算法在有些情况下时间复杂度为O(n+m),但是,在最坏的情况下,时间复杂度会飙升到O(n*m)。但是,BF算法也是理解KMP算法的基础。该算法的主要思想是:从主串S的第一个字符起,和模式串T中的第一个字符比较,若相等,则继续逐个比较后续的每个字符;否则就从主串的下一个字符起再重新和模式串T比较。
再次强调一遍,i 指向主串,j 指向子串。
算法的C语言代码表示如下:
int Index(const char* S, const char* T){//注意,S[0]和T[0]中存放的是字符串的长度int i = 1, j = 1;while(i <= S[0] && j <= T[0]){if(S[i] == T[j]) { ++i;++j; }else { i = i - j + 2;j = 1; }}if(j > T[0])return i - T[0];elsereturn -1;}
下图所示的为BF算法的一种最坏的情况,其中主串S为'aaaaaaaaaaaaaaaaaab',子串T为'aaaaab'。
我们可以看到,当一个一个字符比较,遇到不相同的字符时,BF算法的做法是让子串向右移动一位,然后重新子串头部开始比较。
为了更清楚地看到BF算法的缺陷,我们再来看一个BF算法的例子。
从该例子中可以看出,每次遇到不等的字符时,主串的“指针”i就要回退,而且子串每次也只是向右移动一位,这两点导致BF算法效率低至O(n*m),令人发指。
KMP算法
能不能换种方法,使得每次遇到不匹配的字符时,i呆在原地,不回退呢?同时,能不能想办法让子串每次向右移动的时候,在保证正确的情况下,子串尽量多 右移几位呢?
答案是 可以(Both)。这也正是KMP的做法。
我们在介绍KMP算法之前,先回头再研究一下BF算法,看一下该算法中遗漏掉了什么有用的信息。
如下图所示,当匹配到字符a处时,显然字符a与字符b不同,即串T匹配主串T失败。这时,子串T要一位一位右移,每移动一位,都要重新从头开始匹配。好,经历千辛万苦,终于,当下一次重新匹配到字符a处时(即B与C段完全相同),这时,先别着急着判断问号处的字符和字符a是否相同。
我们来观察一下这个状态有什么特殊之处。因为已经匹配到a处,所以之前的内容是完全相同的,即B与C段相同。之前,已经有C与A段相同了。哈哈,我们发现了,原来当右移重新匹配到上一次失败点的时候,必有A与B段相同。
B段显然是串T的一个前缀,而A段是失败点之前的串的一个后缀。为了不遗失可能正确的匹配结果,我们要再A与B第一次相同时就停止右移,即要保证A与B相同时,B的长度最大。
B是子串T的一个前缀,A是截止到b为止的子串T的一个后缀,且A与B完全相同。
用官方的术语来说,就是寻找匹配失败点之前的 subT的最长的前缀和后缀相等的串。
好了,上面讲了那么多,就是为了这一句话。只有理解了上面这一句话,你才会心悦诚服地 理解并记忆KMP。
KMP算法的整个流程就是:
1. 将S串和T串从第一个字符开始匹配;
2. 如果匹配成功,则subT即灰色部分增加;
3. 如果不成功,则T向后滑动,使滑动后的subT的前缀和滑动前的subT的后缀重合,再对问号处 进行匹配,如果还不成功,则再次滑动T,直到匹配成功或者T滑动到a处。
4. 如果到了a处,则从T串的起始位置进行匹配,跳至步骤1。
从上面的步骤可以知道,匹配失败后,每次滑动到哪里,只与子串T本身有关,而与主串S没有任何关系。所以,我们可以在匹配主串之前,先对子串T进行预处理,得到每个失败点 应该向前滑行的位置,用一个next数组来存储。这样,以后每次匹配失败,只用O(1)的时间查询next数组,直接将j指向那里,再重新开始与主串失败点处进行比较就OK了。
====================================================================================
好了,理解了算法的精髓,就一马平川了。
我们假定,当比较到子串的第j个字符时,遇到了失败点。
此时:我们假设此时主串S中的失败点应该与子串T中的第k个字符继续比较,显然,k<j。
有了next数组,我们先假定next数组已经求好了,那么KMP算法就是小菜一碟了。
int KMP(const char* S, const char* T){//S[0]和T[0]中存放的是有效字符串的长度int i = 1, j = 1;while(i <= S[0] && j <= T[0]){if(0 == j || S[i] == T[j]) { ++i;++j; }//继续比较后继字符elsej = next[j];//匹配失败,模式串向右移动}if( j > T[0])return i - T[0];//匹配成功else return 0;}
可见,next数组的求法 才是KMP算法的关键,也是我们要讲的大头。下面,让我们开始进入next数组的求解环节吧。胜利就是眼前。
子串中第1个字符到第j-1个字符都匹配成功,第j个字符匹配失败,则要查询next[j]。
记next[j] = k,则k表示子串中第j个字符与主串匹配失败之后,应该将主串中的失败点与子串中的第k个字符相比较。
也就是说,在子串第j个字符之前,存在如下关系:
并且k是满足上述关系式的最大值。(k < j)
好的。下面我们用类似数学归纳法的方法,也就是递推(不是递归)来求next数组。
当next[j] = k时,我们来求next[j+1]。
存在两种情况:
第一种情况很简单,不多说了。
第二种情况,表明k也是失败点,则我们仍要继续取next[k],然后比较next[k]处的字符是否与Pj相等,如果相等,则next[j+1] = next[k] + 1,如果还是不等,则继续向前迭代。直至到子串的开头为止,即j==0为止。用C语言表述next数组的求法如下所示:
void get_next(const char*T, int next[]){//因为字符串有效值从下标1开始,所以我们的next数组也是从下标1开始next[1] = 0;next[2] = 1;int i = 2, j = next[i];while(i < T[0]){if(0 == j || T[i] == T[j]) { ++i;++j;next[i] = j; }else j = next[j];}}
我们可以看出,该函数的时间复杂度为O(m),m为子串T的长度。
不过,用该方法求得的next数组仍然有缺陷。我们举个例子就一目了然了。
主串S: 'aaabaaaab'
模式串T: ‘aaaab’
根据T可以得出next数组为
j12345模式串aaaabnext[j]01234再次强调一下,数组的第一个元素T[0]存放的是数组T的长度。真正的元素从T[1]开始存储。我们看到,当i=4, j=4时,子串中T[4]a与主串的字符不匹配,则还要由next[j]的指示依次进行j=3, j=2, j=1这3次比较。但是,由于这四个字符也都相同,因此,假如j=4与主串i=4不匹配的话,就可以将子串一口气向右滑动4个字符,直接进行i=5, j=1的比较。
即我们再给next数组 递推赋值的时候,next[i] = j,还要再判断一下 T[i] 与 T[j] 是否相等。
因为正是由于子串T[i]与主串相比不符,失败的情况下才查找next[i],如果此时next[i]仍然是同一字符,那么比较肯定还是会失败的,所以如果T[i] == T[j],则next[i] = next[j].
修正后的next数组求法C语言描述如下所示:
void get_next(const char*T, int next[]){//2nd Edition//因为字符串有效值从下标1开始,所以我们的next数组也是从下标1开始next[1] = 0;int i = 1, j = next[i];while (i < T[0]){if (0 == j || T[i] == T[j]){++i;++j;if (T[i] == T[j])next[i] = next[j];elsenext[i] = j;}else j = next[j];}}
修改之后的next数组如下:j12345模式串Taaaabnext[j]01234修改后next[j]00004
- 完全搞懂KMP算法
- KMP算法真的搞死人,终于搞懂了
- KMP算法真的搞死人,终于搞懂了
- KMP算法让你真正搞懂它
- 彻底搞懂KMP算法(专为初学者而写)
- 完全掌握KMP算法思想
- 完全掌握KMP算法思想 (文本比较)
- 估计没几个人能看懂我的KMP算法了,搞得有点复杂,有蛮多注释
- 搞懂朴素贝叶斯分类算法
- KMP之完全版
- KMP算法详解 【KMP】
- 【KMP】KMP算法模板
- KMP hihoCoder1015 KMP算法
- kmp算法
- KMP算法
- KMP算法
- KMP算法
- KMP算法
- Map.Entry使用说明
- 文字的右上角显示TM的代码
- 连续子串中出现超过一半次数的字符串 后缀数组 uva 11107 Life Forms
- OC基础-继承
- UI高度可定制化KxMenu弹出菜单
- 完全搞懂KMP算法
- [笔试题]找数组中最长和为0连续子序列
- PHP学习(三)--变量的类型
- BNUOJ 34982 Beautiful Garden 2014北京邀请赛B (有意思的枚举题)
- uvalive 3026(kmp)
- 重复出现超过m次的最长的子串的最大下标 后缀数组或Hash+LCP UVA 12206 - Stammering Aliens
- ListFragment的使用
- java 基础总结
- 转:网页爬取页面去重策略