java实现kmp算法

简介引言和问题引入

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一直“前进”,而不用存在这个回溯的操作。

KMP

代码

我们直接看代码,一步一步分析

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肯定要做一些非常规变化。

kmp在解决哪一类情况

举一个简单的例子

文本串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)的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想在旧琴房弹不能说的秘密

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值