代码随想录刷题|LeetCode KMP算法理论 28. 实现 strStr() 459.重复的子字符串

目录

KMP算法理论

说在前面

KMP算法的思路

next数组(前缀表)

使用next数据进行匹配

28. 实现 strStr()

思路

实现 strStr()

暴力算法

KMP算法

459.重复的子字符串

思路

重复的子字符串

移动匹配

KMP算法

字符串匹配算法


KMP算法理论

说在前面

先定义几个标识:

  • 文本串:对应力扣中的haystack,长的那个字符串,遍历文本串本文使用指针 i
  • 模式串:对应力扣中的needle,短的那个字符串,遍历模式串本文使用指针 j
  • a needle in a haystack 在草垛中的针

首先得明白几个问题,带着这几个问题去理解KMP算法

  • 什么是KMP算法?
    • 用于字符串匹配的一种算法
  • 为什么要用KMP算法?
    • 一般来说,如果要看两个字符串匹配,使用的是暴力循环,从第一个字符开始匹配,如果不匹配就从第二个字符进行匹配。时间复杂度是O(n*m)
    • 但是KMP算法可以不回退文本串的指针进行匹配,时间复杂度是O(n)
    • KMP算法可以当出现字符串不匹配的情况的时候,可以知道一部分之前已经匹配的文本内容,利用这些信息避免"模式串"从头再去做匹配
  • 怎么知道之前文本匹配过的内容,并准确找到位置呢?
    • next数组
    • next数组是什么? ——》是一个前缀表
    • next数组用来干什么的? ——》 当出现字符匹配失败的时候,模式串可以跳过的匹配的字符个数
  • KMP算法的基本思路?
    • 当我们发现某个字符不匹配的时候,由于已经知道之前遍历过的字符,利用这些信息避免暴力算法中的“回退”步骤
  • 最长相同前后缀?
    • 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串
      后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

    • 前缀表要求的就是相同前后缀的长度

KMP算法的思路

我们复盘暴力算法:是怎么进行匹配的?
        从文本串的第一个字符开始进行比较,如果存在不符合的字符,就打断匹配,文本串指针回退到<本次开始匹配的后一位>(因为这一轮开始的已经不行了),重新模式串从模式串第一个字符进行匹配,直到模式串每个字符都可以匹配上了,再返回结果,时间复杂度O(n*m)
        比如,下图中,i 指针指向文本串的4时,j 指向模式串的 4时,发现不匹配,i 就回退到 1 ,j就回退到 0,重新开始进行比较

代码随想录刷题|LeetCode KMP算法理论 28. 实现 strStr() 459.重复的子字符串_第1张图片

那KMP算法会怎么做呢?
        KMP算法在匹配失败的时候,会去看最后一个匹配的字符所对应的next数组,查看这个数,模式串的前几位(next数组对应的数)就不用再进行匹配了
        以下图举例:也是 i 从0 开始,j 从 0 开始两个字符串进行第一轮匹配,两个指针都走到4的时候,字符不匹配,此时:模式串去查找最后一个匹配的字符对应的next数组,是2,那这代表着从文本串当前的位置开始,不再和模式串的前两个字符进行对比了,让指针 j 指向跳过模式串前两个字符的位置,而这个位置刚好是模式串的索引为2的位置,开始继续进行匹配
        整个过程中 i 指针没有发生过回退,只有指针 j 进行回退,按照next数组进行回退
        文本串的 i 永远不递减,这是KMP算法的精髓
代码随想录刷题|LeetCode KMP算法理论 28. 实现 strStr() 459.重复的子字符串_第2张图片

所以接下来要做的就是:

  • 了解为什么next数组可以做到这一点
  • 怎么得到“模式串”的next数组
  • 怎么使用next数组与“文本串”进行匹配

next数组(前缀表)

        next数组代表了在匹配失败时,模式串中可以跳过匹配的字符个数,也是模式串的指针要回退到的索引位置
        next数组具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力

        为什么next数组可以做到这点?让模式串准确得跳过这么多字符串呢?
       
以下图为例:next数组的本质其实就是寻找模式串中“相同的前后缀的长度”,并且一定是最长的前后缀,而且前后缀不能是字符串本身。所以说不匹配的时候,跳过的字符是最长的前缀,因为上一轮最长后缀已经给你试过水了。也可以说这一轮跳过匹配的这几个字符在上一轮匹配过了。
        模式串下标4之前的这部分的字符串(ABAB)的最长相等的前缀和后缀字符串 是子字符串AB,因为找到了最长相等的前后缀,匹配失败的位置就是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了(也就是从模式串的ABAB的最长前缀AB后面,下标2的地方开始就可以了)
        模式串与next数组对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。代码随想录

代码随想录刷题|LeetCode KMP算法理论 28. 实现 strStr() 459.重复的子字符串_第3张图片

         指针 j 根据模式串已经有的next数组来进行回退,避免重复的运算,再想一下KMP算法的原理: KMP算法在匹配失败的时候,会去看最后一个匹配的字符所对应的next数组,查看这个数(保存了最长前后缀的长度),然后让模式串的前几位跳过匹配,就是说:下一轮,我看了最后这几位匹配的有这么几位(最长后缀),你再来匹配这几位(对应的最长前缀)就不用走啦

        知道了为什么next数组可以做到精准回退,接下来就要看看怎么获得每个字符对应的最长相同前后缀?

        需要的前提:
       
1、模式串s
        2、next[]数组 -----> 用来存储对应的前后缀

        要做三件事:
        1、初始化
        2、前后缀不相同,j指针回退
        3、前后缀相同,更新next数组的值

        首先:定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置(如下图所示),同时,j也代表最长前缀的长度
        对指针和next数组进行初始化:

int j = 0; 
next[0] = j; // 字符串第一个字符对应的最长前后缀长度为0

for (int i = 0; i < s.length(); i++) {
}

代码随想录刷题|LeetCode KMP算法理论 28. 实现 strStr() 459.重复的子字符串_第4张图片
 

        遇到前后缀不同的情况:
               
遇到前后缀不相同的时候,j 指针就找前一位需要回退的位置进行回退,而且回退到下一个目标的时候,如果不符合要求,要继续进行回退,所以这里应该使用while

while (j > 0 && s.charAt(j) != s.charAt(i)) {
    j = next[j - 1];
}

        遇到前后缀相同的情况:
               
遇到前后缀相同的情况,此时 i 对字符串的元素下标和对next数组的元素下标指针是对应的,所以先对 j 进行++, 赋值给 next[i] , 然后 i 再向后走(for循环自然++)

if (s.charAt(j) == s.charAt(i)) {
    j++;
}

next[i] = j;

        总结在一起:

public void getNext(int[] next,String s) {
        // 初始化
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < s.length(); i++) {
            // 前后缀不相同的情况
            while (j > 0 && s.charAt(j) != s.charAt(i)) {
                j = next[j - 1];
            }
            // 前后缀相同的情况
            if (s.charAt(j) == s.charAt(i)) {
                j++;
            }
            // 对next数组进行赋值
            next[i] = j;
        }
    }

使用next数据进行匹配

        上面得到了next数组,那怎么使用next数组进行匹配呢?

        定义两个下标,j 指向模式串起始位置;i 指向文本串起始位置,开始遍历文本串

int j = 0; // 因为next数组里记录的起始位置是0
for (int i = 0; i < haystack.length(); i++) {
            
}

        开始对needle.charAt(j)和haystack.charAt(i)进行比较,如果needle.charAt(j) != haystack.charAt(i),j就要从next数组里寻找下一个匹配的位置

while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
    j = next[j - 1];
}

        如果needle.charAt(j) == haystack.charAt(i),i 和 j 同时向后移动

if (needle.charAt(j) == haystack.charAt(i)) {
    j++; // i在for循环里面增加
}

        怎么判断文本串中出现了字符串呢?

if (j == needle.length()) {
    return i - needle.length() + 1;
}

        总结在一起:

public int strStr(String haystack, String needle) {
    // 使用next数据进行匹配
    if (needle.length() == 0) {
        return 0;
    }
    // 创建next数组
    int[] next = new int[needle.length()];
    // 调用方法填充next数组
    getNext(next,needle);

    int j = 0; // 因为next数组里记录的起始位置为0
    for (int i = 0; i < haystack.length(); i++) {
        while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
            j = next[j - 1];
        }
        if (needle.charAt(j) == haystack.charAt(i)) {
            j++;
        }
        if (j == needle.length()) {
            return i - needle.length() + 1;
        }
    }

    return -1;
 }

参考资料:

  • 代码随想录
  • 最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibili

28. 实现 strStr()

题目链接:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/

思路

        详见KMP算法理论

实现 strStr()

暴力算法

        可以让字符串needle每次从haystack中得第i个字符进行匹配,所以遍历haystack,限制条件是从haystack中得第i个字符开始后面得长度还要够字符串needle的长度,要不然就没有意义了

        然后开始对needle进行匹配,如果出现不匹配的情况,就将flag设置成false,再打断循环,因为出了循环要看看匹配的这个字符串有没有出现不匹配的情况

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

        int x = haystack.length();
        int y = needle.length();
        // 遍历haystack字符串
        for (int i = 0; i + y <= x; i++) {

            boolean flag = true;
            for (int j = 0; j < y; j++) {
                if (haystack.charAt(i + j) != needle.charAt(j)) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                return i;
            }
        }

        return -1;
    }
}

KMP算法

class Solution {
    public int strStr(String haystack, String needle) {
        // 使用next数据进行匹配
        if (needle.length() == 0) {
            return 0;
        }
        // 创建next数组
        int[] next = new int[needle.length()];
        // 调用方法填充next数组
        getNext(next,needle);

        int j = 0; // 因为next数组里记录的起始位置为0
        for (int i = 0; i < haystack.length(); i++) {
            while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
                j = next[j - 1];
            }
            if (needle.charAt(j) == haystack.charAt(i)) {
                j++;
            }
            if (j == needle.length()) {
                return i - needle.length() + 1;
            }
        }

        return -1;
    }


    // 构造needle的前缀表(next数组)
    public void getNext(int[] next,String s) {
        // 初始化
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < s.length(); i++) {
            // 前后缀不相同的情况
            while (j > 0 && s.charAt(j) != s.charAt(i)) {
                j = next[j - 1];
            }
            // 前后缀相同的情况
            if (s.charAt(j) == s.charAt(i)) {
                j++;
            }
            // 对next数组进行赋值
            next[i] = j;
        }
    }
}

459.重复的子字符串

题目链接:https://leetcode.cn/problems/repeated-substring-pattern/

思路

        看了一下题解,还没详细拆解

重复的子字符串

移动匹配

KMP算法

字符串匹配算法

常见的字符串匹配算法包括

  • 暴力匹配
  • Knuth-Morris-Pratt 算法
  • Boyer-Moore 算法
  • Sunday 算法

你可能感兴趣的:(LeetCode学习笔记,数据结构,leetcode,java)