当前位置: 首页 > article >正文

深入理解KMP算法

看这个算法之前,希望你做过:"leetcode-28-找出字符串中第一个匹配项的下标" 这一道题。

题目描述(直接上例子):
主串(T):leetcode
字串(P):leeto
在主串(T)中找出包含字串(P)的最小下标。

BF算法(暴力)

BF算法:下一个主串与子串不匹配时,满足以下条件 👇
1. 主串回退到此次开始匹配位置+1处;
2. 子串回退到0位置。
  1. 第一次比较

主串在'c'处与子串在'o'处不匹配
1. 主串回退到此次开始匹配位置+1,即是下标为1的位置;
2. 子串回退到0位置。

  1. 第二次比较

主串在'e'处与子串在'l'处不匹配
1. 主串回退到此次开始匹配位置+1,即是下标为2的位置;
2. 子串回退到0位置。

  1. 第三次比较

主串在'e'处与子串在'l'处不匹配
1. 主串回退到此次开始匹配位置+1,即是下标为3的位置;
2. 子串回退到0位置。

  1. 第四次比较

主串在't'处与子串在'l'处不匹配
1. 主串回退到此次开始匹配位置+1,即是下标为4的位置;
2. 子串回退到0位置。

下一次比较子串长度已经大于主串长度,不可能包含字串了,所以结束比较。

从上面的算法可以得出时间复杂度为:O((n-m)*m),其中n为主串字符串长度,m为子串字符串长度。

class Solution {
    public int strStr(String haystack, String needle) {

        int n = haystack.length();      // 主串(T)长度
        int m = needle.length();        // 子串(P)长度

        // 两个特例
        if (n < m) return -1;           // 子串(P)大于主串(T)
        if (haystack.equals(needle)) {  // 子串(P)等于主串(T)
            return 0;
        }


        int end = n - m;                // 控制提前结束,就是剩下的主串长度小于子串长度,不用比了,肯定不相等
        for (int i = 0; i <= end; i++) {
            if(haystack.charAt(i) != needle.charAt(0)) {
                continue;               // 子串开头就不等于主串的开头,跳过比较
            }
            int k = 0;                  // 子串与主串相等的字符个数
            for (int j = i; j < i+m; j++, k++) {
                if(haystack.charAt(j) != needle.charAt(k)){
                    break;
                }
            }
            if(k == m){                 // 一共有m个相等,即是找到答案啦,返回下标
                return i;
            }
        }

        return -1;

    }
}

KMP算法

KMP算法:下一个主串与子串不匹配时,满足以下两个条件 👇
1. 主串不回退;
2. 子串回退到特定位置。

理解2(子串回退到特定位置)之前,首先要了解一下"最长公共前后缀",这里是子串回退到哪的重点。

最长公共前后缀

首先,我先通过一个例子解释一下"最长公共前后缀",因为这个词可能表达得不是很正确,但是我找不到比这个更合适得词来描述这种情况了。

例如,上图中的例子,其中字符串为 "leet"。

前缀:包含第一个单词,不包含后一个单词的连续子串,则为前缀。
lee:包含第一个单词"l",不包含最后一个单词"t"
le:包含第一个单词"l",不包含最后一个单词"t"
l:包含第一个单词"l",不包含最后一个单词"t"

后缀:包含最后一个单词,不包含第一个单词的连续子串,则为后缀。
eet:包含最后一个单词"t",不包含第一个单词"l"
et:包含最后一个单词"t",不包含第一个单词"l"
t:包含最后一个单词"t",不包含第一个单词"l"

有了字符串的"前后缀"概念之后,我们可以找出子串的最长公共前后缀,例如上述例子中,字符串的最长公共前后缀为:0。因为没有一个前、后缀子串是相等的。

为了加深理解,再举一个例子:

图中例子,字符串为:"abab",则最长的公共前后缀为2,因为,前缀"ab"=后缀"ab"。

回退数组

理解了最长公共前后缀,其实子串与主串匹配失败,应该回退到哪个位置,已经出现答案了。
举个例子:
主串:abeabababeabf
子串:abeabf

由图可得,主串在'a'处于子串在'f'处不相等,按照BF算法的回退过程,应该如下图所示:

KMP算法,回退如下图所示:

其实,这里就运用到"最长公共前后缀"的知识进行回退的。

主串在'a'处于子串在'f'处不相等,那么,前面字符串"abeab"其实已经之前比较过了(是相等的),那我们只需要找"abeab"的最长公共前后缀位置的下一个下标位置即可(这一步很关键,我尝试过多次入门KMP,但是都被这一步拒之门外,虽然这样做没错,但是为什么可以这么做??这其实也是我之前一直想知道为什么的一步,下面做一个解释)。

解释以下为什么只要找到最长公共前后缀位置的下标就是回退的位置!

匹配错误的字符串"abeaba",是在最后一个位置"a"处匹配错误的,是不是可以理解成有一对组合它以"a"为结尾与子串匹配不上。那我是不是可以找另一对组合,不是以"a"结尾的,但是前面是相同的。那我看一下前面有没有(看的这一步就是最长公共前后缀)。

由图可得,找到了一对"ab",匹配错误的子串是"aba",但是这一对是"abe"。那我们就回退到最长公共前后缀位置的下一个下标位置即可。

class Solution {
    public int strStr(String haystack, String needle) {

        //int n = haystack.length();      // 主串(T)长度
        //int m = needle.length();        // 子串(P)长度
        //
         两个特例
        //if (n < m) return -1;           // 子串(P)大于主串(T)
        //if (haystack.equals(needle)) {  // 子串(P)等于主串(T)
        //    return 0;
        //}
        //
        //
        //int end = n - m;                // 控制提前结束,就是剩下的主串长度小于子串长度,不用比了,肯定不相等
        //for (int i = 0; i <= end; i++) {
        //    if(haystack.charAt(i) != needle.charAt(0)) {
        //        continue;               // 子串开头就不等于主串的开头,跳过比较
        //    }
        //    int k = 0;                  // 子串与主串相等的字符个数
        //    for (int j = i; j < i+m; j++, k++) {
        //        if(haystack.charAt(j) != needle.charAt(k)){
        //            break;
        //        }
        //    }
        //    if(k == m){                 // 一共有m个相等,即是找到答案啦,返回下标
        //        return i;
        //    }
        //}
        //
        //return -1;

        int n = haystack.length();      // 主串(T)长度
        int m = needle.length();        // 子串(P)长度

        // 两个特例
        if (n < m) return -1;           // 子串(P)大于主串(T)
        if (haystack.equals(needle)) {  // 子串(P)等于主串(T)
            return 0;
        }

        int[] next = getNext(needle);   // 获取回退数组

        for (int i = 0; i < m; i++) {   // 打印回退数组
            System.out.println(next[i]);
        }


        int i = 0;                      // 主串下标
        int j = 0;                      // 子串下标
        while (i < n){
            if(haystack.charAt(i) == needle.charAt(j)) {
                i++;
                j++;
                if(j == m) {
                    return i-m;
                }
            } else {
                j = next[j];            // 不相等,则子串进行回退
                if(j == -1){            // 到了第一个子串位置
                    j++;
                    i++;
                }
            }
        }

        return -1;

    }


    public int[] getNext(String str){

        int n = str.length();
        int[] next = new int[n];
        next[0] = -1;
        for (int i = 2; i < n; i++) {
            String tempStr = str.substring(0, i);
            int right = i-1;
            int left = 1;
            int find = 0;
            while (left < i){
                String leftStr = tempStr.substring(0, right);
                String rightStr = tempStr.substring(left, i);
                if (leftStr.equals(rightStr)){
                    find = leftStr.length();
                    break;
                }
                left++;
                right--;
            }
            next[i] = find;
        }
        return next;
    }
}

http://www.kler.cn/a/1599.html

相关文章:

  • C语言的语法
  • 中国科技统计年鉴EXCEL版(2021-2023年)-社科数据
  • JWT与Token
  • TCP 如何获取端口信息
  • FastAPI 的依赖注入与生命周期管理深度解析
  • PCL 分段线性函数
  • 【JavaEE】如何将JavaWeb项目部署到Linux云服务器?
  • 华为OD机试 - 货币单位换算(C 语言解题)【独家】
  • 开发也可以很快乐,让VSCode和CodeGPT带给你幸福感
  • 【Hello Linux】进程间通信
  • 浅谈C库函数——memcpy、memmove、memcmp、memset函数
  • 【日志包】go语言如何设计日志包 - 基于zap封装适合自己的日志包
  • Servlet的详细使用
  • OpenCv + Qt5.12.2 文字识别
  • JVM学习.01 内存模型
  • C语言刷题(6)(猜名次)——“C”
  • linux进程和进程通信编程(1)
  • 软件测试 - 非技术常见面试题
  • 【C++学习】日积月累——SLT中stack使用详解(1)
  • 012+limou+C语言深入知识——(4)“结构体”与“枚举体”与“联合体”
  • 011+limou+C语言深入知识——(3)“字符串函数”和“字符分类函数”和“内存操作函数”以及“部分库函数的模拟实现”
  • 【DP动态规划】最长子序列
  • [数据分析与可视化] Python绘制数据地图1-GeoPandas入门指北
  • string类(下)
  • 如何优雅地让谷歌浏览器中的网页旋转90度?掌握这个技巧,让你的网页与众不同!
  • linux kernel 5.0 inline hook框架