KMP算法
2015-12-23 15:14
99 查看
最近在刷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)位组成的前缀子字符串 与后缀子字符串的交集,取其中最长字符串的长度。
很明显,耗费太大了。
###复用比较过程
在直接匹配的过程肯定需要扫描一遍字符串进行比较操作,我们就可以得到下面的矩阵
通过这个数组我们可以得到 next数组为
0 0 0 0 1 2 0
我们在查找 abc 的最长前缀字符串与后缀字符串的交集的时候 就是查看 这个矩阵中 (1,2) (1,3)(3,2) 这个三角形中 从第一列开始到三行结束的平行于斜边的最长的连续1字符串的长度,那么我们可以直接在一遍扫描的时候动态的计算出next数组。
具体代码如下
当然这种方法也有问题,就是没有利用已经计算出来的值
###动态规划
我们已经得到了一场长度为 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 那么程序就能直接出来了
## 总结
理解KMP算法不大,主要的难点是理解KMP的优化过程 并同时将优化过程应用到工作实践中
核心思想还是 动态规划
完整代码
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)位组成的前缀子字符串 与后缀子字符串的交集,取其中最长字符串的长度。
很明显,耗费太大了。
###复用比较过程
在直接匹配的过程肯定需要扫描一遍字符串进行比较操作,我们就可以得到下面的矩阵
a | b | c | a | b | d | |
a | 1 | 1 | ||||
b | 1 | 1 | ||||
c | 1 | |||||
a | 1 | 1 | ||||
b | 1 | 1 | ||||
d | 1 |
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 ;//指向sourceArray int j = 0 ;//指向patternArray while(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; }
相关文章推荐
- iframe实现局部刷新和回调-- 文件上传
- MYSQL 5.7 添加新用户
- Eclipse构建Maven项目
- ASP.NET地址栏form提交安全验证
- 云平台、云计算详解
- MB与MQ区别(IBM)
- 数据结构基础概念
- JSTL 学习、应用记录
- Android View Scroller类,scrollTo(...)和scrollBy(...)方法
- 从菜鸟到资深工程师的进阶之路
- 主机中的图片库传到虚拟机中
- Autolayout 基础
- 第十三章 ------ RememberMe
- JSP基本语法
- 输入框与scrollview的滑动冲突
- 手机自动化测试:Appium源码分析之跟踪代码分析二
- C#获取本机IP
- Python3爬取图片
- RT-Thread ---开启基于RTGUI的LCD显示功能(2)<编译测试>
- IOS 上拉加载,下拉刷新,本人使用MJRefresh