KMP算法

来源:互联网 发布:双枪入洞什么感觉知乎 编辑:程序博客网 时间:2024/06/07 12:21

最近在刷Hiho的题目,其中一题涉及到了这个算法,在这里我分享一下我对该算法的理解过程。

http://blog.csdn.net/v_july_v/article/details/7041827    这篇文章分析KMP 分析的很透彻,如果想要深入了解该算法,可以直接看这篇博客。


KMP是一个字符串匹配的算法,面向的主要问题为在一个字符串中查找匹配的字符串

e.g   在     ababababa  找出 abab 出现的次数或者位置


为了更好的讨论问题,这里做一个假设问题

输入字符串为  abcabaabac

输入的模式字符串为 aba

求模式字符串在输入字符串中出现的次数


##直接方案

我们直接遍历输入字符串的字符,遇到等于模式字符串首尾的字符,那么我们依次对之后的字符进行比较,匹配上 那么我们将计数加一再继续遍历,匹配失败则直接继续遍历

缺点 时间消耗太大,对于输入字符串长度为n,模式字符串长度为m的输入来说 我们的时间消耗为 m*n


##KMP优化思路

在我们寻找匹配字符的时候实际存在了两个指针,一个指针指向输入字符的当前遍历位置,一个指针指向模式字符的当前遍历位置,在匹配失败的时候我们将i和j都重置回了相应的位置。这中间就会存在着大部分的性能损失,如下面的输入格式

输入字符串  abababd

模式字符串为ababd

令指向输入字符串的指针标识符为i

指向模式字符串的指针标识符为j

当i = 0 的时候  input[i] == pattern[j]   所以开始匹配了    i = 4  j=4的时候匹配失败,i 被重置成 1    j被重置为0

然而 i  = [ 1... 3]这段实际上已经被遍历过了,重复遍历一下是确定是否有符合模式字符串的一个前缀字符串


那么我们下一步就是尝试怎么把这部分的代价消除掉。


我们看一下此时的匹配情况  x代表已经匹配上的字符串中的字符  .代表匹配失败或者未匹配的字符

输入字符串

. . . . x x x x . . . .

模式字符串

x x x x .

1 首个已经匹配上的字符之前的内容是不做考虑的

2 如果配上的字符中不存在一个模式字符串的前缀字符串,那么也是不需要考虑的

e.g

输入字符串为  abceabcd

模式字符串为 abcd


3如果存在匹配上的字符中存在一个模式字符串的前缀字符串,那么这个字符串一定是匹配上的字符串的后缀字符串

e.g 

输入字符串为 abababc

模式字符串为 ababc

当首次匹配到 abab的时候,匹配失败,下次进入匹配的时候,我们使用的该字符串的后两位 ab


数学意义上也能证明:    新字符串的首位要在当前匹配上的字符串的首位之后,   新字符串的长度要大于当前匹配上的字符串


根据上面的结论,我们就能推导出以下的方案

在匹配到模式字符串第i位失败的时候,我们只需要将指向模式字符串的当前字符之前的字符串的最长的前缀字符串与后缀字符串的公共字符串的下一位,然后继续遍历就可以了

继续之前的案例

e.g 

输入字符串为 abababc

模式字符串为 ababc

首轮匹配的时候  指向输入字符串的指针i指向 4  也就是 字符a,模式字符串的指针j 值为4 也就是字符 c 的时候失败

此时我们将  j 移动到3  也就是字符a 尝试进行下轮匹配,最后匹配成功。


##KMP 程序逻辑

初始化遍历下标 i = j = 0;

当 input [ i ] == pattern [ j ] 的时候,将 i++   j ++

当input[ i ] != pattern [ j ]  的时候  将 j 指针回退,进行下轮匹配  如果j 指针没有回退的位置了,那么我们将i++ j++


这里回退位置采用一个next数组来确定

next数组记录了匹配到该位置的时候失败的时候应该跳转的位置


##next数组

next数组的长度是 模式字符串的长度+1

0 .....  pattern.length-1  上的数字表示为当在第i位匹配失败的时候下一轮匹配的位置, pattern.length 是当模式字符串匹配成功的时候下一步跳转的位置,因为会出现下面的情况

e.g.

输入字符串 

ababa

模式字符串

aba


next数组的0号位一般是赋值为 -1,表示没有位置回退了,需要进行 i++ j++操纵,


计算next数组有一个优化过程

###暴力计算next数组

在计算第i位值的时候直接遍历0-(i-1)位组成的前缀子字符串 与后缀子字符串的交集,取其中最长字符串的长度。

很明显,耗费太大了。


###复用比较过程

在直接匹配的过程肯定需要扫描一遍字符串进行比较操作,我们就可以得到下面的矩阵

 abcabda1  1  b 1  1 c  1   a1  1  b 1  1 d     1

通过这个数组我们可以得到 next数组为

 0 0 0 0 1 2 0

我们在查找  abc  的最长前缀字符串与后缀字符串的交集的时候 就是查看  这个矩阵中  (1,2) (1,3)(3,2) 这个三角形中  从第一列开始到三行结束的平行于斜边的最长的连续1字符串的长度,那么我们可以直接在一遍扫描的时候动态的计算出next数组。

具体代码如下

public static int[] getNextArray(char[] inputArray){int[] NextArray = new int[inputArray.length+1];int[] tempArray = new int[inputArray.length+1];for(int i = 1 ; i < inputArray.length ; i++){for(int j = 0 ; j < i ; j++){boolean ifEqual = inputArray[i] == inputArray[j];if(ifEqual){if(j == 0){tempArray[i] = 1;}else{tempArray[i-j] = tempArray[i-j] > 0 ? tempArray[i-j]+1 :0;}}else{tempArray[i-j] = 0;}}for(int j = 0; j <=i ; j ++){if(tempArray[j] > 0){NextArray[i+1] = tempArray[j] ;break;}}}NextArray[0] = -1;return NextArray;}


当然这种方法也有问题,就是没有利用已经计算出来的值

###动态规划

我们已经得到了一场长度为  0-i的是next数组了  我们此时怎么去获得next[i+1]的值

我们可以进行如下计算

k = next [ k ] ;  

如果pattern [ k ] == pattern [ i+1 ]  ,那么next[ i + 1 ] = next[ j ]=k + 1    (这里可以用反证法证明)

如果pattern [ k ] != pattern [ i+1 ]   ,那么我们重复这里过程去匹配只写更短的字符串,直到匹配成功或者 k == -1


如果k == -1   那么说明前缀字符串与后缀字符串没有交集,next[ i+1 ] = 0 ;


ok 那么程序就能直接出来了

public static int[] getNextArray(char[] inputArray){int[] NextArray = new int[inputArray.length+1];NextArray[0] = -1;int k = -1;int j = 0;/* * k 记录的是当前探测后缀字符串的最后一位 *  * j 记录的是当前探测的前缀字符串的最后一位 */while(j < inputArray.length){if(k == -1 || inputArray[j] == inputArray[k] ){j++;k++;NextArray[j] = k;}else{k= NextArray[k];}}return NextArray;}


## 总结

理解KMP算法不大,主要的难点是理解KMP的优化过程 并同时将优化过程应用到工作实践中


核心思想还是  动态规划


完整代码

public static int getMatchCount(char[] sourceArray,char[] patternArray){int matchCount = 0;int[] nextArray = getNextArray(patternArray);int i = 0 ;//指向sourceArrayint j = 0 ;//指向patternArraywhile(i <= sourceArray.length){if(j == -1){i++;j++;}if(j >= patternArray.length){matchCount++;j = nextArray[j];}if(i<sourceArray.length && sourceArray[i] != patternArray[j]){j = nextArray[j];}else{j++;i++;}}return matchCount;}public static int[] getNextArray(char[] inputArray){int[] NextArray = new int[inputArray.length+1];NextArray[0] = -1;int k = -1;int j = 0;/* * k 记录的是当前探测后缀字符串的下一位 *  * j 记录的是当前探测的前缀字符串的下一位 */while(j < inputArray.length){if(k == -1 || inputArray[j] == inputArray[k] ){j++;k++;NextArray[j] = k;}else{k= NextArray[k];}}return NextArray;}


0 0
原创粉丝点击