赞
踩
kmp 算法:Knuth-Morris-Pratt 字符串查找算法,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。下文中统一称去查找的字符串为文本串,被查找的字符串为模式串。
kmp算法是一个比较难比较抽象的算法,网上对它的解释说明也很多,但是关于怎么找到next前缀表,为什么要用这个前缀表,为什么前缀表有用,纯理论即使是配了图加以说明,可能还是令人不是很理解。现在我想用我看见这个kmp算法代码的时候,想的思路分析一下,也许会有代入感,更好理解。
由于本人水平有限,仅阐述一下自己的一些想法,难免表达有误或者论述不严谨,欢迎纠错,感谢您的阅读。
- public int strStr(String haystack, String needle) {
- int m = needle.length();
- // 当 needle 是空字符串时我们应当返回 0
- if (m == 0) {
- return 0;
- }
- int n = haystack.length();
- if (n < m) {
- return -1;
- }
- int i = 0;
- int j = 0;
- while (i < n - m + 1) {
- // 找到首字母相等
- while (i < n && haystack.charAt(i) != needle.charAt(j)) {
- i++;
- }
- if (i == n) {// 没有首字母相等的
- return -1;
- }
- // 遍历后续字符,判断是否相等
- i++;
- j++; //i控制长字符串,j控制要匹配的字符串,若未匹配成功,则i,j分别回到开始匹配的地方
- while (i < n && j < m && haystack.charAt(i) == needle.charAt(j)) {
- i++;
- j++;
- }
- if (j == m) {// 找到
- return i - j;
- } else {// 未找到
- i -= j - 1;
- j = 0;
- }
- }
- return -1;
- }
在常规的思路中,i在文本串中检索,j在模式串中检索,当i,j对应位置的字符相等,开始一次匹配尝试,若不成功 i -= j-1 i要回到开始匹配的后一个位置,这会导致i在进入匹配尝试后进行的一些比对(在跳出匹配模式前)失去意义。所以希望产生一个算法,可以使i一直“前进”,而不用存在这个回溯的操作。
我们直接看代码,一步一步分析
- public void getNext(int[] next, String s){ //getNext_1(next, needle)
- int j = -1;
- next[0] = j;
- for (int i = 1; i < s.length(); i++){
- while(j >= 0 && s.charAt(i) != s.charAt(j+1)){
- j=next[j];
- }
-
- if(s.charAt(i) == s.charAt(j+1)){
- j++;
- }
- next[i] = j;
- }
- }
- public int strStr(String haystack, String needle) {
- if(needle.length()==0){
- return 0;
- }
- int[] next = new int[needle.length()];
- getNext(next, needle); //初始化getNext数组
- int j = -1;
- for(int i = 0; i < haystack.length(); i++){
- while(j>=0 && haystack.charAt(i) != needle.charAt(j+1)){
- j = next[j];
- }
- if(haystack.charAt(i) == needle.charAt(j+1)){
- j++;
- }
- if(j == needle.length()-1){
- return (i-needle.length()+1);
- }
- }
-
- return -1;
- }
我们通过观察主函数,注意到先是做了一个极端情况的判断,再是初始化了一个next数组,最后对文本串做了一次遍历。观察for循环,一看见会对while循环感到不理解,那就先假设永远跳不进while循环,观察一下运行逻辑:
假设 文本串abcS 模式串abc
很明显,进入不到while循环,在做了三次判断文本串和模式串的if语句后,给出了返回值。
从这边可以得出:
在可以匹配得到的情况下,不管前面发生了什么操作,这个for循环最后的几步,一定是i在文本串中开始,伴随j在模式串中变化,直到完成匹配,最后的i在文本串要匹配的末尾。
对于i的变化,逻辑非常清晰,就是for循环中,每次结束,都+1
但是j的变化,通过j = next[j]实现,它的变化一开始不明白。
然而j要发生变化,有两个条件 :
1 - j>=0对应是已经进入了匹配模式,因为直到第二个if成立第一次,才会使j>=0
2 - 是在i j+1 位置对应文本串,匹配串对应的字符不同,即要退出,也要注意 j 代表的是模式串中正在匹配的字符的前一个位置。在匹配失败后,i不变,所以j肯定要做一些非常规变化。
举一个简单的例子
文本串abababc 模式串ababc
按照传统的匹配方式,i j从0出发,当其值为4时,跳出匹配,i变为1,j变为0
但是可以发现 模式串ababc完全没有从文本串第二个b开始匹配的必要,如果可以直接从第二个a开始匹配那么就一步到位了,但是按照kmp的说法,i不变了,已经在4的位置,那么如果在这一次匹配失败的操作后,及在一次j = next[j]后,如果j可以去2的位置,那么i ,j继续向后遍历,也可以达到一样的效果。这是因为 模式串中两个ab通过了匹配,重复了,把模式串中第一个ab的位置移到文本串中第二个ab位置的开始匹配,就可以更加便捷。
那么使得上述的j去到该去位置的next数组就称为前缀表,前缀表的作用就是在匹配失败的时候,
让j跳到之前已经匹配过的地方。
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
前缀表记录的是 在 模式串到j的之前的字符串中最长相等前后缀
即用前缀表做了一次j的跳转后,会到最长相等前缀末尾的位置
说明 图中 [ ]表示一些重复文本(即为模式串的相同前后缀)xxx 代表不匹配的字符,...代表一些普通字符
我将j 指针的变化,转变为上下字符串 位置的对应,上下位置相对的,代表上下完成或正在进行匹配。
- public void getNext_1(int[] next, String s){ //getNext_1(next, needle)
- int j = -1;
- next[0] = j;
- for (int i = 1; i < s.length(); i++){
- while(j >= 0 && s.charAt(i) != s.charAt(j+1)){
- j=next[j];
- }
-
- if(s.charAt(i) == s.charAt(j+1)){
- j++;
- }
- next[i] = j;
- }
- }
在这个写法中,第一个元素无法回溯,next[0] 为-1
当if 被触发一次后,j>=0进入了一个匹配模式,可以进入while
假设一个模式串为 [] ... [] ...[] ...[]... []代表一段重复文本
当 i指向第二段重复文本的开头,j 开始变化,直到遇到第二个... 结束..
为说明while循环:j 要跳到一个 j+1和i对应字符串的字符相等的位置,此时j之前的一段 和 ?到
i-1的那一段是一样的
跳转j 而要比较的字符位置是j+1 ,那么当j+1 和 i相等时,就找到了0~i的 最长前后缀。
其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。