大话 KMP 算法

简介: 大话 KMP 算法

LeetCode上有一题,简单难度的,以下是原题


这个题目应该刷LeetCode的同学大部分都刷到过,比较荒谬的是可以直接用 indexOf 方法实现,并且是最优的解法,看到评论以后一片哗然,大家扯到最多的是这道题就应该用KMP来做,如果用KMP做那这道题应该是困难级别的。所以看了这些评论以后,决定自己去摸索一下KMP的算法,以及大概其实现原理。


      网上找了很多关于解释KMP算法的,发现只有阮一峰老师的(字符串匹配的KMP算法)解释的比较通俗易懂,但是缺点是没有给出对应的代码,所以在编程方面还是比较晦涩的。


    简单说下KMP算法,我们在找字符串的时候需要找到第一次出现此字符串的地方,没有则返回-1.


    下面是KMP算法具体的代码实现 ,这边代码strStr方法是我自己写的,下面的初始化字典是参考的网上的

public class KMP {
    public int strStr(String haystack,String needle){
        int[] dp = kmpInt(needle);
        int M = haystack.length();
        int N = needle.length();
        // i 表示当前的起始位置,j表示当前 needle的匹配位置, z表示匹配数
        int z = 0;
        // i的累加交给循环处理
        loop:
        for (int i=0;i <= M - N;i++){
            for (int j=0;j<N;j++) {
                if (haystack.charAt(i + j) == needle.charAt(j)) {
                    z++;
                    if (z == N){
                        return i;
                    }
                }else{
                    if (z == 0){
                        continue loop;
                    }
                    // 根据KMP的算法规则,移动位数 = 已匹配的字符数 - 对应的部分匹配值
                    // 因为循环需要i++,所以这里需要 -1
                    i = i + (z - dp[z-1]) - 1;
                    z = 0;
                    continue loop;
                }
            }
        }
        return -1;
    }
    // 这里需要理解字典
    // 默认认为最优解,如果当前的第K个字符和最后一个新字符相等,那么结果一定是上一次的最大匹配字 
    符串加1
    // 栗子:比如上一次的最大字符串是 ABCDABD,
    // 当ABCDA的最大字符是A的时候,因为下标为1的是B,下标为5的也是B
    // 因此ABCDAB的最大字符一定是AB
    public int[] kmpInt(String needle){
        int[] kmp = new int[needle.length()];
        // 将其所有的数字都填充为0
        Arrays.fill(kmp,0);
        int k = 0;
        for (int i = 1;i<needle.length();i++){
            // 目前几乎所有的博客都在说这段代码是最复杂的地方,但是解释这段代码的很少
            while (k > 0 && needle.charAt(k) != needle.charAt(i)){
                k = kmp[k-1];
            }
            if (needle.charAt(k) == needle.charAt(i)){
                k++;
            }
            kmp[i] = k;
        }
        return kmp;
    }
}

  在strStr方法中,按照阮一峰老师的博客还是很好理解的, 完全维护了一个字典表,然后根据字典表来进行相对应的位移




    KMP真正难的地方在于怎么样构建上图这个字典树,而字典树里面最难的代码的就是这一段  

while (k > 0 && needle.charAt(k) != needle.charAt(i)){
                k = kmp[k-1];
            }

    我理解了很久没有理解,然后把代码改成了下图,也同样构建了KMP的字典数组,达到了同样的效果。我来解释下下面这段代码:

while (k > 0 && needle.charAt(k) != needle.charAt(i)){
                k--;
            }

   首先,K只是我们维护的一个常数,因为我们要取字典树中的值,因为大小一定要大于0,由于我们的i是累加的,因此我们需要判断第K个字符和第I个字符是否相等,如果不相等的话,并不能直接判断当前的字典树位置对应的就是0,而是要往前找。但是如果第K个字符和第I个字符相等,那么他的最大匹配数一定是k++的。


   如果上一个字符串的最大匹配数是0,在当前情况下,如果第一个字符和最后一个字符相等,那么此次的最大匹配数一定为1。


   意思就是说如果 ABCDABD,如果ABCD的最大匹配数是0,因为I是5的时候,A=A,所以ABCDA的最大匹配数就一定是1,这个不懂的可以自己画图理解下。


   举个栗子看一下,needle的字符串为”aabaaac“。


      - 1的默认长度为0;


  - "aa"的前缀为[a],后缀为[a],共有元素的长度为1;


  - "aab"的前缀为[a,aa],后缀为[ab, b],共有元素的长度0;


  - "aaba"的前缀为[a,aa,aab],后缀为[aba, ba, a],共有元素的长度为1;


  - "aabaa"的前缀为[a, aa, aab, aaba],后缀为[abaa, baa, aa, a],共有元素为"aa",长度为2;


  - "aabaaa"的前缀为[a, aa, aab, aaba, aabaa],后缀为[abaaa, baaa, aaa, aa, a],共有元素为"aa",长度为2;


  - "aabaaac"的前缀为[a, aa, aab, aaba, aabaa, aabaaa],后缀为[abaaac, baaac, aaac, aac, ac, c],共有元素的长度为0。


    我们是从i=1的时候开始算起,对应的就是aa,因为a == a 并且K初始化为0,所以kmp[1] = k++ = 1;


    i = 2的时候,对应的就是aab,此时K是1,因为kmp[1] != kmp[2],所以需要K--,使K的指针前移,直到匹配到有一个字符等于K[i]或者K减到0,所以kmp[2] = 0;


    i = 3的时候,对应的就是aaba,此时K是0,因为a == a,K还是等于0,所以kmp[3] = 1;


    i = 4的时候,对应的就是aabaa,此时K是1,那么K[1] = a,K[4] = a,因此直接 k++,所以kmp[4] = 2;


    i = 5的时候,问题就来了,当前对应的是aabaaa,此时的K是2,kmp[2] = b,而kmp[5] = a,b != a,当前KMP数组维护的指针是2,我们需要进行指针回退,匹配上一个字符是否等于最后一个字符,相当于我们回到了i=4的时候,不同的是,当前K-- = 1,而kmp[k] = a,kmp[i] = a,所以 needle,charAt(1) == needle.charAt(5) ,所以kmp[5] = 2;


   i = 6的时候,对应的就是aabaaac,由于K的指针递减,发现没有一个字符可以和最后的最新字符进行匹配,所以kmp[6] = 0;


  转化出来的字典数组即为 【0,1,0,1,2,2,0】,当字典转换出来理解以后,KMP的算法就基本理解了。


 另外附一下我的代码改和没有改时候在LeetCode的区别


  用

while (k > 0 && needle.charAt(k) != needle.charAt(i)){
                k = kmp[k-1];
     }


的时候:


while (k > 0 && needle.charAt(k) != needle.charAt(i)){
                k--;
            }


的时候:


  虽然我改版后执行用时增加了,但是我觉得比较方便我理解。。。


  果然是头秃的劝退算法。。。摸索了好久才把自己讲通。


   本身理解的也不是很深,有问题欢迎探讨。。。

目录
相关文章
|
7月前
|
算法
数据结构中的KMP算法及其改进算法
KMP算法通过引入部分匹配表,有效避免了重复计算,从而将字符串匹配的时间复杂度降低到O(m+n)。通过进一步优化next数组,KMP算法的效率得到了进一步提升。对于大规模字符串匹配问题,KMP算法及其改进算法提供了高效的解决方案,是计算机科学领域的经典算法之一。
112 3
|
3月前
|
算法
第四章 KMP算法理论基础
第四章 KMP算法理论基础
29 0
|
3月前
|
算法
KMP算法
KMP算法
50 0
|
5月前
|
算法 C++
A : DS串应用–KMP算法
这篇文章提供了KMP算法的C++实现,包括计算模式串的next数组和在主串中查找模式串位置的函数,用于演示KMP算法的基本应用。
|
6月前
|
数据采集 算法 JavaScript
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
JavaScript字符串搜索涵盖`indexOf`、`includes`及KMP算法。`indexOf`返回子字符串位置,`includes`检查是否包含子字符串。KMP是高效的搜索算法,尤其适合长模式匹配。示例展示了如何在数据采集(如网页爬虫)中使用这些方法,结合代理IP进行安全搜索。代码示例中,搜索百度新闻结果并检测是否含有特定字符串。学习这些技术能提升编程效率和性能。
150 1
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
|
5月前
|
算法
KMP算法
KMP算法
43 0
|
6月前
|
算法 Java
KMP算法详解及其在字符串匹配中的应用
KMP算法详解及其在字符串匹配中的应用
|
7月前
|
人工智能 算法 BI
一篇文章讲明白KMP算法(俗称看毛片算法)
一篇文章讲明白KMP算法(俗称看毛片算法)
74 0
|
7月前
|
算法 Java
Java数据结构与算法:字符串匹配算法之KMP算法
Java数据结构与算法:字符串匹配算法之KMP算法
|
8月前
|
算法