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

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

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,重新开始进行比较


c1feb60c383b4743884774fee2b6252e.png


那KMP算法会怎么做呢?

       KMP算法在匹配失败的时候,会去看最后一个匹配的字符所对应的next数组,查看这个数,模式串的前几位(next数组对应的数)就不用再进行匹配了

       以下图举例:也是 i 从0 开始,j 从 0 开始两个字符串进行第一轮匹配,两个指针都走到4的时候,字符不匹配,此时:模式串去查找最后一个匹配的字符对应的next数组,是2,那这代表着从文本串当前的位置开始,不再和模式串的前两个字符进行对比了,让指针 j 指向跳过模式串前两个字符的位置,而这个位置刚好是模式串的索引为2的位置,开始继续进行匹配

       整个过程中 i 指针没有发生过回退,只有指针 j 进行回退,按照next数组进行回退

       文本串的 i 永远不递减,这是KMP算法的精髓

af5edd4a8ac142d9a737deedc81d2f2b.png


所以接下来要做的就是:


了解为什么next数组可以做到这一点

怎么得到“模式串”的next数组

怎么使用next数组与“文本串”进行匹配


next数组(前缀表)


next数组代表了在匹配失败时,模式串中可以跳过匹配的字符个数,也是模式串的指针要回退到的索引位置

       next数组具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力


       为什么next数组可以做到这点?让模式串准确得跳过这么多字符串呢?

       以下图为例:next数组的本质其实就是寻找模式串中“相同的前后缀的长度”,并且一定是最长的前后缀,而且前后缀不能是字符串本身。所以说不匹配的时候,跳过的字符是最长的前缀,因为上一轮最长后缀已经给你试过水了。也可以说这一轮跳过匹配的这几个字符在上一轮匹配过了。

       模式串下标4之前的这部分的字符串(ABAB)的最长相等的前缀和后缀字符串 是子字符串AB,因为找到了最长相等的前后缀,匹配失败的位置就是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了(也就是从模式串的ABAB的最长前缀AB后面,下标2的地方开始就可以了)

       模式串与next数组对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。代码随想录


534824e966c543319cf1a86bc260d06e.png


        指针 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++) {
}

939dc53af8184057803d3fba660a94a3.png

 遇到前后缀不同的情况:

               遇到前后缀不相同的时候,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 同时向后移动


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


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


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

       

  总结在一起:

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;
 }


       参考资料:


28. 实现 strStr()

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

思路


实现 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 算法
相关文章
|
1月前
|
机器学习/深度学习 算法
力扣刷题日常(一)
力扣刷题日常(一)
20 2
|
1月前
|
存储 算法 JavaScript
怎么刷算法,leetcode上有哪些经典题目
怎么刷算法,leetcode上有哪些经典题目
16 0
|
1月前
|
存储 索引
《LeetCode》—— LeetCode刷题日记
《LeetCode》—— LeetCode刷题日记
|
1月前
|
搜索推荐
《LeetCode》——LeetCode刷题日记3
《LeetCode》——LeetCode刷题日记3
|
1月前
|
容器
《LeetCode》——LeetCode刷题日记1
《LeetCode》——LeetCode刷题日记1
|
1月前
|
存储
实现单链表的基本操作(力扣、牛客刷题的基础&笔试题常客)
实现单链表的基本操作(力扣、牛客刷题的基础&笔试题常客)
143 38
|
6天前
刷题之Leetcode160题(超级详细)
刷题之Leetcode160题(超级详细)
9 0
|
6天前
刷题之Leetcode206题(超级详细)
刷题之Leetcode206题(超级详细)
13 0
刷题之Leetcode206题(超级详细)
|
6天前
|
索引
刷题之Leetcode707题(超级详细)
刷题之Leetcode707题(超级详细)
10 0
|
6天前
|
索引
刷题之Leetcode35题(超级详细)
刷题之Leetcode35题(超级详细)
12 0