也谈KMP算法
2013-01-25 01:49
363 查看
看毛片算法,念书时学过,几年下来也就只记得个名字了。前几天偶然遇到,顺道捡起来摸一摸,还是那手感…… ^_^
言归正传,这是个模式匹配算法(好吧,正传第一句居然是废话~)。谈它之前,先说说最简单直观的模式匹配算法——朴素匹配。
朴素匹配,就是不动脑子去匹配的算法:
源串S[0…n-1] 模式串P[0…m-1] 很显然,n > = m
源串和模式串分别有一个指针标记,
对指针指向的源串中每一个位置[0…n-m](n-m向后的不用考虑,剩余长度都不够m了),模式串都是从第一位开始比对,匹配则两个指针同时后移一位;
如果遇到某一位不匹配,源串中开始比较位置后移一位,模式串仍然从第一位开始继续匹配。
直至模式串扫描完毕表示匹配 或者源串扫描完毕,表示不匹配。
给一个Java实现
简单分析下这个算法,由于外层循环是扫描源串S[0…n-m],内层循环最坏要扫描整个模式串P[m],故算法是O(nm)的。
朴素算法里,比如某趟从源串的i = 4,模式串j=0位置开始比对,i和j依次后移,到了i=7 j=3时发现不匹配了,那么下一趟 i又回到5,j继续从0开始匹配下去……
我们发现,源串的指针一直在做前后的“折返跑”(哦,好吧,行话叫“回溯”),有没有办法减少甚至消除此类回溯从而提高效率呢? 额,设问句问出来了,答案当然是有! 而且是本篇猪脚——KMP算法就是基于这种想法而提出的,它消除源串的回溯,是最坏情况O(n)的一个匹配算法!
具体说KMP,就是借助一个辅助数组,在匹配过程中一旦出现不匹配,源串指针不用回溯,直接移动模式串继续匹配,直到结束。
(注意,由于有形形色色的辅助函数,原理虽然一样,但是边界条件各有不同。各位童鞋如果考试遇到了,请务必按照自己教材的辅助函数计算方式,不然答错了概不负责哈**%¥…… 当然话说回来,作者一直认为,数据结构&算法分析啥的考试,让学生手动模拟是很傻很天真的…… 额 貌似又跑偏了)
算法思路:
如果 S[i-j...i] P[0…j] 在P[j] 失配了,即之前的S[i-j…i-1] = P[0…j-1] *
但 S[i]<>P[j]
为了达到S串不回溯,需要把模式串P向右移动到某位置(比如K),继续比较S[i] 和 P[k]
但是这有一个前提,必须满足P[0…k-1] = S[i-k…i-1] = P[j-k…j-1] (红色部分是由*式得到的), 否则在P[j]之前的部分就已经不匹配了
整理下,就是说如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们希望得到一个值K,可以让我们的匹配从S[i]和P[k]继续尝试下去
而K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大k ( k < j)
预定义边界条件 aux[0] = -1 ,如果P[0]失配,S串指针直接后移一位。
我们的辅助数组aux
定义就是上面的蓝色粗体字,再强调一遍
aux[j] = k 表示如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们可以从S[i]和P[k]继续匹配下去。
那么如果S[i] 和 P[k] 还不匹配呢? 再看看上面的定义,P[k]失配, 我们是不是要尝试S[i] 和 P[ P[k] ] ?
没错,重复这个过程,直到遇到S[i]可以和某个P[#] 匹配,或者 #=0还不匹配了,S串指针后移到i+1, P串再重头来过。
(注意,初涉KMP会导致习惯性头晕,个人建议头晕时反复咀嚼蓝色粗体字,效果不错哦~ 实际上,我写这篇的时候也嚼了好几遍的,嚼着嚼着就不晕了 :P)
有了aux数组,我们的匹配就可以在源串中一路向西了,哦不,是一路到底哈……
下面给出一个KMP的匹配实现,计算aux数组后面会谈到
(这里注意,预定义 aux[0] = -1)
下面说说计算aux数组(
4000
其取值仅与模式串本身有关)
再嚼一次:aux[j] = k 表示如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们可以从S[i]和P[k]继续尝试匹配。
K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大K
第一种方法,根据 “满足P[0…k-1] = P[j-k…j-1]的最大K”求K
即对每个1 <= j < m 求满足条件的k,如果没有,返回0
(神码?为啥返回0? 头晕嚼一嚼, aux[j] = 0 是不是表明, P[j]失配,我们用P[0]尝试)
同样,Java实现如下
第二种方法,这也是大多数算法书上使用的,递归计算辅助数组
初值还是 aux[0] = -1
假设我们已知 aux[j-1] = k,现在要计算 aux[j]
还是从定义出发, aux[j-1] = k 表示 当P[j-1]失配时,应该移动P串到P[k]开始尝试匹配,而且P[0…k-1] = P[j-k-1…j-2] (这个等式换个角度看其实就是P串自身和自身在做匹配了,当然看不出来也木有关系的)
那么,如果 P[k] = P[j-1] 那么P[0…k] = P[j-k-1…j-1] 对比下前面定义,等价于 aux[j] = k+1(这说明当P[j]失配时,移动P串到P[k+1]开始尝试匹配,而且P[0…k] = P[j-k-1…j-1] )
如果 P[k] <> P[j-1]
,相当于 P[k]失配了, 我们令 k' = P[ P[k] ] ,当然也有P[0…k'-1] = P[j-k-1…j-2],
同样,如果P[k'] = P[j-1] 那么P[0…k'] = P[j-k-1…j-1], 等价于 aux[j] = k'+1
重复上面的过程,直到找出aux[j] 或者 某个k = -1,此时 aux[j] = 0
递归实现如下
可以借助平摊分析,得出KMP算法是O(n)的时间复杂度,至此,KMP算法就算谈完了
总结一下,
KMP分两个部分
求辅助数组 &
应用辅助数组进行串匹配
通过使用辅助数组,KMP消除源串的回溯,(严格说,还是有原地踏步匹配的,但没有回头重复匹配的);
模式串是有回溯的——但未必每次都要回到第一位开始匹配,这个取决于辅助函数的返回
言归正传,这是个模式匹配算法(好吧,正传第一句居然是废话~)。谈它之前,先说说最简单直观的模式匹配算法——朴素匹配。
朴素匹配,就是不动脑子去匹配的算法:
源串S[0…n-1] 模式串P[0…m-1] 很显然,n > = m
源串和模式串分别有一个指针标记,
对指针指向的源串中每一个位置[0…n-m](n-m向后的不用考虑,剩余长度都不够m了),模式串都是从第一位开始比对,匹配则两个指针同时后移一位;
如果遇到某一位不匹配,源串中开始比较位置后移一位,模式串仍然从第一位开始继续匹配。
直至模式串扫描完毕表示匹配 或者源串扫描完毕,表示不匹配。
给一个Java实现
public int StringMatch(String sourceStr, String patternStr) { char[] source = sourceStr.toCharArray(); char[] pattern = patternStr.toCharArray(); int idxSource, idxPattern; // for each i, check if source[i~...i+m-1] matches pattern[0~...m] for(int i = 0; i <= source.length - pattern.length; i++) { idxSource = i; idxPattern = 0; while(idxPattern < pattern.length &&source[idxSource] == pattern[idxPattern]) { idxSource++; idxPattern++; } if(idxPattern == pattern.length) return i; } return -1; }
简单分析下这个算法,由于外层循环是扫描源串S[0…n-m],内层循环最坏要扫描整个模式串P[m],故算法是O(nm)的。
朴素算法里,比如某趟从源串的i = 4,模式串j=0位置开始比对,i和j依次后移,到了i=7 j=3时发现不匹配了,那么下一趟 i又回到5,j继续从0开始匹配下去……
我们发现,源串的指针一直在做前后的“折返跑”(哦,好吧,行话叫“回溯”),有没有办法减少甚至消除此类回溯从而提高效率呢? 额,设问句问出来了,答案当然是有! 而且是本篇猪脚——KMP算法就是基于这种想法而提出的,它消除源串的回溯,是最坏情况O(n)的一个匹配算法!
具体说KMP,就是借助一个辅助数组,在匹配过程中一旦出现不匹配,源串指针不用回溯,直接移动模式串继续匹配,直到结束。
(注意,由于有形形色色的辅助函数,原理虽然一样,但是边界条件各有不同。各位童鞋如果考试遇到了,请务必按照自己教材的辅助函数计算方式,不然答错了概不负责哈**%¥…… 当然话说回来,作者一直认为,数据结构&算法分析啥的考试,让学生手动模拟是很傻很天真的…… 额 貌似又跑偏了)
算法思路:
如果 S[i-j...i] P[0…j] 在P[j] 失配了,即之前的S[i-j…i-1] = P[0…j-1] *
但 S[i]<>P[j]
为了达到S串不回溯,需要把模式串P向右移动到某位置(比如K),继续比较S[i] 和 P[k]
但是这有一个前提,必须满足P[0…k-1] = S[i-k…i-1] = P[j-k…j-1] (红色部分是由*式得到的), 否则在P[j]之前的部分就已经不匹配了
整理下,就是说如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们希望得到一个值K,可以让我们的匹配从S[i]和P[k]继续尝试下去
而K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大k ( k < j)
预定义边界条件 aux[0] = -1 ,如果P[0]失配,S串指针直接后移一位。
我们的辅助数组aux
定义就是上面的蓝色粗体字,再强调一遍
aux[j] = k 表示如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们可以从S[i]和P[k]继续匹配下去。
那么如果S[i] 和 P[k] 还不匹配呢? 再看看上面的定义,P[k]失配, 我们是不是要尝试S[i] 和 P[ P[k] ] ?
没错,重复这个过程,直到遇到S[i]可以和某个P[#] 匹配,或者 #=0还不匹配了,S串指针后移到i+1, P串再重头来过。
(注意,初涉KMP会导致习惯性头晕,个人建议头晕时反复咀嚼蓝色粗体字,效果不错哦~ 实际上,我写这篇的时候也嚼了好几遍的,嚼着嚼着就不晕了 :P)
有了aux数组,我们的匹配就可以在源串中一路向西了,哦不,是一路到底哈……
下面给出一个KMP的匹配实现,计算aux数组后面会谈到
(这里注意,预定义 aux[0] = -1)
public int StringMatchKMP(String sourceStr, String patternStr) { // initialize auxiliary array int [] auxKMP = auxInitialNonRecursive(patternStr); // int [] auxKMP = auxInitialRecursive(patternStr); char[] source = sourceStr.toCharArray(); char[] pattern = patternStr.toCharArray(); for(int i = 0; i <= sourceStr.length() - patternStr.length(); i++) { int idxPattern = 0; while(idxPattern < pattern.length && idxPattern >= 0) { if(source[i] == pattern[idxPattern]) { i++; idxPattern++; } else { // mismatch in source[i] and pattern[idxPattern] idxPattern = auxKMP[idxPattern]; } } if(idxPattern == pattern.length) return i - pattern.length; } return -1; }
下面说说计算aux数组(
4000
其取值仅与模式串本身有关)
再嚼一次:aux[j] = k 表示如果 S[i-j...i] P[0…j]
在P[j]
失配了,我们可以从S[i]和P[k]继续尝试匹配。
K的求法,就是满足P[0…k-1] = P[j-k…j-1]的最大K
第一种方法,根据 “满足P[0…k-1] = P[j-k…j-1]的最大K”求K
即对每个1 <= j < m 求满足条件的k,如果没有,返回0
(神码?为啥返回0? 头晕嚼一嚼, aux[j] = 0 是不是表明, P[j]失配,我们用P[0]尝试)
同样,Java实现如下
private int[] auxInitialNonRecursive(String patternStr) { char[] pattern = patternStr.toCharArray(); int [] aux = new int[patternStr.length()]; aux[0] = -1; for(int j = 1; j < patternStr.length(); j++) { // check k from j-1 to 1, does p[0...k-1] matches p[j-k...j-1] ? int k = j - 1; while(k > 0) { if(checkMax(k, j, pattern)) { aux[j] = k; break; } k--; } if(k == 0) aux[j] = 0; } return aux; } /** check if pattern[0...k-1] matches pattern[j-k...j-1]*/ private boolean checkMax(int k, int j, char[] pattern) { int m = 0; int n = j - k; while(pattern[m] == pattern && m < k) { m++; n++; } return (m == k); }
第二种方法,这也是大多数算法书上使用的,递归计算辅助数组
初值还是 aux[0] = -1
假设我们已知 aux[j-1] = k,现在要计算 aux[j]
还是从定义出发, aux[j-1] = k 表示 当P[j-1]失配时,应该移动P串到P[k]开始尝试匹配,而且P[0…k-1] = P[j-k-1…j-2] (这个等式换个角度看其实就是P串自身和自身在做匹配了,当然看不出来也木有关系的)
那么,如果 P[k] = P[j-1] 那么P[0…k] = P[j-k-1…j-1] 对比下前面定义,等价于 aux[j] = k+1(这说明当P[j]失配时,移动P串到P[k+1]开始尝试匹配,而且P[0…k] = P[j-k-1…j-1] )
如果 P[k] <> P[j-1]
,相当于 P[k]失配了, 我们令 k' = P[ P[k] ] ,当然也有P[0…k'-1] = P[j-k-1…j-2],
同样,如果P[k'] = P[j-1] 那么P[0…k'] = P[j-k-1…j-1], 等价于 aux[j] = k'+1
重复上面的过程,直到找出aux[j] 或者 某个k = -1,此时 aux[j] = 0
递归实现如下
private int[] auxInitialRecursive(String patternStr) { char[] pattern = patternStr.toCharArray(); int [] aux = new int[patternStr.length()]; aux[0] = -1; for(int j = 1; j < patternStr.length(); j++) { int k = aux[j - 1]; while(k >= 0 && pattern[k] != pattern[j - 1]) k = aux[k]; aux[j] = (k < 0) ? 0 : k + 1; } return aux; }
可以借助平摊分析,得出KMP算法是O(n)的时间复杂度,至此,KMP算法就算谈完了
总结一下,
KMP分两个部分
求辅助数组 &
应用辅助数组进行串匹配
通过使用辅助数组,KMP消除源串的回溯,(严格说,还是有原地踏步匹配的,但没有回头重复匹配的);
模式串是有回溯的——但未必每次都要回到第一位开始匹配,这个取决于辅助函数的返回