搞懂KMP,,这一片文章就够了

今日任务
●28. 实现 strStr()
●459.重复的子字符串
●字符串总结
●双指针回顾

KMP算法

认识前缀表

  1. KMP算法较于暴力解法的优化:KMP算法简化了查找过程中的遍历次数
  • 暴力解法中当遇到冲突时,是将模式串后移一位,然后再和文本串进行比较,其事件复杂度为O(M*N)。
  • KMP算法为当遇到冲突时,下一次开始位置为:冲突位之前的字符串的最长相等前后缀的前缀的下一位开始
  1. KMP算法为什么要使用前缀表:前缀表中记录了不匹配字符与之前所有字符构成的字符串的所有字串子串的最长相等前后缀的值
  2. 什么是前缀,什么是后缀:
  • 前缀为包含首字符但不包含包含尾字符的所有子串
  • 后缀为不包含首字符但包含尾字符的所有字串
    如:模式串为 a a b a a f,其所有子串的前后缀及最长相等前后缀的长度如下:
    在这里插入图片描述
    前缀表(next数组):前缀表中记录了一个字符串的所有子串的最长相等前后缀的值
    如aabaaf的前缀表为[0,1,0,1,2,0],而遇到冲突时下一次的开始位置下标为:冲突位的前一位对应前缀表的值
    如匹配aabaaf时在f位发生冲突,下一次开始匹配位的下标为 f前的字符a对应的next数组值:2,因此从开始匹配(遇到冲突时指向文本字符串位置的指针不动)

实现一个字符串的前缀表

解过程为以下四部:

  • 初始化:初始化指向前缀末尾的指针值、指向后缀末尾的指针值、前缀表next数组
  • 处理前缀末尾值与后缀末尾数组不相等的情况
  • 处理前缀末尾值与后缀末尾值相等的情况
  • 更新next数组

实现过程:

//求模式串的前缀表组成的next数组
    public static int[] nextArray(String str) {
        char[] chars = str.toCharArray();
        //初始化:
        // j:指向前缀的末尾,且代表i(包括i)之前的子串最长相等前后缀的长度;
        // i指向后缀的末尾
        int[] next = new int[chars.length];
        int j = 0;
        //第一个字符组成的前缀的最大相等前后缀的长度为0
        next[0] = 0;
        //比较子串的最长前缀和最长后缀的最后一个字符是否相等
        for (int i = 1; i < chars.length; i++) {
            //前后缀不相等的情况
            while (j > 0 && chars[i] != chars[j]) {
                //j向后回退,回退位置为冲突位的前一位对应的next数组的值
                j = next[j - 1];
            }
            //前后缀相等的情况
            if (chars[i] == chars[j]) {
                j++;
            }
            //更新next的值
            next[i] = j;
        }
        return next;
    }

KMP算法匹配子串

//KMP算法匹配子串
    public static int strStr1(String haystack, String needle) {
        //求needle子串的前缀表
        int[] next = nextArray(needle);
        //查找是否存在子串
        char[] charh = haystack.toCharArray();
        char[] charn = needle.toCharArray();
        int a = 0;
        int b = 0;
        while (a < charh.length && b < charn.length) {
            //匹配文本串与模式串的第一位字符相等的位置
            if (b == 0 && charn[b] != charh[a]) {
                a++;
            } 
            //遇见冲突,模式串的指针回退,回退位置为:冲突位的前一位对应next数组中的值;文本串的指针站立不动
            else if (b != 0 && charn[b] != charh[a]) {
                b = next[b - 1];
            } 
            //无冲突(模式串指针和文本串对应字符相等):两个指针一致前进
            else if (charn[b] == charh[a]) {
                b++;
                a++;
            }
        }
        //跳出循环时a值等于文本串的长度,b值等于模式串的长度
        if (b == charn.length) {
            return a - b;
        }
        return -1;
    }
  • 28.实现 strStr()
    暴力解决:O(M*N)
public class KMPSubString {
    public static int strStr(String haystack, String needle) {
        char[] w = haystack.toCharArray();
        char[] m = needle.toCharArray();
        int ws = w.length;
        int ms = m.length;
        for (int i = 0; i < ws - ms + 1; i++) {
            int a = i;
            int b = 0;
            while (b < ms && w[a] == m[b]) {
                a++;
                b++;
            }
            if (b == ms) {
                return i;
            }
        }
        return -1;
    }

KMP算法匹配子串

public static int strStr1(String haystack, String needle) {
        //求needle子串的前缀表
        int[] next = nextArray(needle);
        //查找是否存在子串
        char[] charh = haystack.toCharArray();
        char[] charn = needle.toCharArray();
        int a = 0;
        int b = 0;
        while (a < charh.length && b < charn.length) {
            //匹配文本串与模式串的第一位字符相等的位置
            if (b == 0 && charn[b] != charh[a]) {
                a++;
            }
            //遇见冲突,模式串的指针回退,回退位置为:冲突位的前一位对应next数组中的值;文本串的指针站立不动
            else if (b != 0 && charn[b] != charh[a]) {
                b = next[b - 1];
            }
            //无冲突(模式串指针和文本串对应字符相等):两个指针一致前进
            else if (charn[b] == charh[a]) {
                b++;
                a++;
            }
        }
        //跳出循环时a值等于文本串的长度,b值等于模式串的长度
        if (b == charn.length) {
            return a - b;
        }
        return -1;
    }

    //求模式串的前缀表组成的next数组
    public static int[] nextArray(String str) {
        char[] chars = str.toCharArray();
        //初始化 j:指向前缀的末尾,且代表i(包括i)之前的子串最长相等前后缀的长度;
        // i指向后缀的末尾
        int[] next = new int[chars.length];
        int j = 0;
        //第一个字符组成的前缀的最大相等前后缀的长度为0
        next[0] = 0;
        //先比较子串的最长前缀和最长后缀的最后一个字符是否相等
        for (int i = 1; i < chars.length; i++) {
            //前后缀不相等的情况
            while (j > 0 && chars[i] != chars[j]) {
                //j向后回退,回退位置为冲突位的前一位对应的next数组的值
                j = next[j - 1];
            }
            //前后缀相等的情况
            if (chars[i] == chars[j]) {
                j++;
            }
            //更新next的值
            next[i] = j;
        }
        return next;
    }
}

相关题目:(明日KMP)
459.重复的子字符串
在这里插入图片描述

我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法KMP算法是拿来处理字符串匹配的。换句话说,给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。比如,字符串A="I'm matrix67",字符串B="matrix",我们就说B是A的子串。你可以委婉地问你的MM:“假如你要向你喜欢的人表白的话,我的名字是你的告白语中的子串吗?” 解决这类问题,通常我们的方法是枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是O (mn)的。虽然很多时候复杂度达不到mn(验证时只看头一两个字母就发现不匹配了),但我们有许多“最坏情况”,比如,A= "aaaaaaaaaaaaaaaaaaaaaaaaaab",B="aaaaaaaab"。我们将介绍的是一种最坏情况下O(n)的算法(这里假设 m<=n),即传说中的KMP算法。 之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。这时,或许你突然明白了AVL 树为什么叫AVL,或者Bellman-Ford为什么中间是一杠不是一个点。有时一个东西有七八个人研究过,那怎么命名呢?通常这个东西干脆就不用人名字命名了,免得发生争议,比如“3x+1问题”。扯远了。 个人认为KMP是最没有必要讲的东西,因为这个东西网上能找到很多资料。但网上的讲法基本上都涉及到“移动(shift)”、“Next函数”等概念,这非常容易产生误解(至少一年半前我看这些资料学习KMP时就没搞清楚)。在这里,我换一种方法来解释KMP算法
KMP算法(Knuth-Morris-Pratt算法)是一种字符串匹配算法,用于在一个文本串S中查找一个模式串P的出现位置。它的核心思想是利用已经匹配过的部分字符,尽量减少不必要的比较次数。 KMP算法的步骤是这样的: 1. 预处理模式串P,得到一个next数组。next[i]表示P[0:i]这个子串中,最长的相等前缀和后缀的长度。我们可以通过不断比较P的前缀和后缀得到这个next数组。 2. 遍历文本串S,同时遍历模式串P。当遇到不匹配的字符时,根据next数组,将模式串P向右移动尽量少的距离。移动的距离由next数组中的值决定。 3. 当P移到最右端时,如果还是没有匹配成功,则继续将P向右移动一个位置,继续匹配。 这样,通过预处理模式串P,我们能够在匹配过程中尽量少的进行字符比较,提高了算法的效率。 终于全部弄懂了KMP算法,我明白了它的原理和实现过程。它的核心在于构建next数组,这个数组能够帮助我们在匹配过程中避免重复比较已经匹配过的字符。通过next数组,我们可以事先知道模式串P中的每个位置的最长相等前缀和后缀的长度,从而决定每次匹配过程中的移动距离。 KMP算法是一种高效的字符串匹配算法,它的时间复杂度为O(n+m),其中n和m分别是文本串S和模式串P的长度。相比于暴力匹配算法的时间复杂度O((n-m+1)m),KMP算法具有明显的优势。 了解KMP算法的原理和实现,对于我在日常编程中遇到的字符串匹配问题将会更加得心应手。我相信通过不断实践和总结,我能够更加熟练地运用KMP算法,解决字符串匹配相关的挑战。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值