【字符串】leetcode28. 实现 strStr()(C/C++/Java/Python/Js)

news2024/11/14 20:25:45

leetcode28. 实现 strStr()

  • 1 题目
  • 2 KMP
    • 2.1 什么是KMP?
    • 2.2 KMP有什么用?
    • 2.3 什么是前缀表?
    • 2.4 最长公共前后缀
    • 2.5 为什么一定要用前缀表?
    • 2.6 如何计算前缀表
    • 2.7 前缀表与next数组
    • 2.8 使用next数组来匹配
    • 2.9 时间复杂度分析
  • 3 KMP代码思路
    • 3.1 构造next数组
    • 3.2 使用next数组来做匹配
  • 4 代码
    • 4.1 C++
    • 4.2 C版本
    • 4.3 Java版本
    • 4.4 Python版本
    • 4.5 JavaScript版本
  • 5 总结:


在一个串中查找是否出现过另一个串,这是KMP的看家本领。

1 题目


题源链接

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。

示例 1:

输入:haystack = “sadbutsad”, needle = “sad”
输出:0
解释:“sad” 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:

输入:haystack = “leetcode”, needle = “leeto”
输出:-1
解释:“leeto” 没有在 “leetcode” 中出现,所以返回 -1 。

提示:

1 <= haystack.length, needle.length <= 104
haystack 和 needle 仅由小写英文字符组成


2 KMP


下面是Carl老师的视频讲解:
帮你把KMP算法学个通透!(理论篇)
帮你把KMP算法学个通透!(求next数组代码篇)

KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。


2.1 什么是KMP?

为什么叫做KMP呢。

因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP。


2.2 KMP有什么用?

KMP主要应用在字符串匹配上。

KMP的主要思想当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。


2.3 什么是前缀表?

next数组就是一个前缀表(prefix table)。

前缀表有什么作用呢?

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

为了清楚地了解前缀表的来历,我们来举一个例子:

要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

请记住文本串和模式串的作用。
在这里插入图片描述
动画演示

文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。

但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。

前缀表是如何记录的呢?

前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。

那么什么是前缀表记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。


2.4 最长公共前后缀

字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。

后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。

可以将最长公共前后缀理解为最长相等前后缀,因为前缀表要求的就是相同前后缀的长度。

所以字符串a的最长相等前后缀为0。 字符串aa的最长相等前后缀为1。 字符串aaa的最长相等前后缀为2。 等等…


2.5 为什么一定要用前缀表?

刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:

在这里插入图片描述
然后就找到了下标2,指向b,继续匹配:如图:
在这里插入图片描述
下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。

所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。


2.6 如何计算前缀表

在这里插入图片描述
长度为前1个字符的子串a,最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。

在这里插入图片描述
长度为前2个字符的子串aa,最长相同前后缀的长度为1。
在这里插入图片描述
长度为前3个字符的子串aab,最长相同前后缀的长度为0。

以此类推: 长度为前4个字符的子串aaba,最长相同前后缀的长度为1。 长度为前5个字符的子串aabaa,最长相同前后缀的长度为2。 长度为前6个字符的子串aabaaf,最长相同前后缀的长度为0。

那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:
在这里插入图片描述
可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:
在这里插入图片描述
动画演示
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。

因为要找前面字符串的最长相同的前缀和后缀。

所以要看前一位的 前缀表的数值。

前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。

最后就在文本串中找到了和模式串匹配的子串了。


2.7 前缀表与next数组

很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?

next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。

其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。


2.8 使用next数组来匹配

以下我们以前缀表统一减一之后的next数组来做演示。

有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。

注意next数组是新前缀表(旧前缀表统一减一了)。

匹配过程动画如下:
在这里插入图片描述
动画演示


2.9 时间复杂度分析

其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。

暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。


3 KMP代码思路


3.1 构造next数组

定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串。 代码如下:

void getNext(int* next, const string& s)

构造next数组其实就是计算模式串s前缀表的过程。 主要有如下三步:

  1. 初始化
  2. 处理前后缀不相同的情况
  3. 处理前后缀相同的情况

1. 初始化:

定义两个指针i和j,j 指向前缀末尾位置i 指向后缀末尾位置。

然后还要对next数组进行初始化赋值,如下:
j 为什么要初始化为 -1呢,因为之前说过 前缀表要统一减一的操作仅仅是其中的一种实现,我们这里选择j初始化为-1

next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)

所以初始化next[0] = j 。

2. 处理前后缀不相同的情况

因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。

所以遍历模式串s的循环下标i 要从 1开始,代码如下:

for (int i = 1; i < s.size(); i++) {

如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。

next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。
那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。

所以,处理前后缀不相同的情况代码如下:

while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
    j = next[j]; // 向前回退
}

3. 处理前后缀相同的情况
如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。

代码如下:

if (s[i] == s[j + 1]) { // 找到相同的前后缀
    j++;
}
next[i] = j;

最后整体构建next数组的函数代码如下:

void getNext(int* next, const string& s){
    int j = -1;
    next[0] = j;
    for(int i = 1; i < s.size(); i++) { // 注意i从1开始
        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
            j = next[j]; // 向前回退
        }
        if (s[i] == s[j + 1]) { // 找到相同的前后缀
            j++;
        }
        next[i] = j; // 将j(前缀的长度)赋给next[i]
    }
}

代码构造next数组的逻辑流程动画如下:

在这里插入图片描述
动画演示


3.2 使用next数组来做匹配

在文本串s里 找是否出现过模式串t。

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

那么j初始值依然为-1,为什么呢? 依然因为next数组里记录的起始位置为-1。

i就从0开始,遍历文本串,代码如下:

for (int i = 0; i < s.size(); i++) 

接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。

如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。

代码如下:

while(j >= 0 && s[i] != t[j + 1]) {
    j = next[j];
}

如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。

本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。

使用next数组,用模式串匹配文本串的整体代码如下:

int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
    while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
        j = next[j]; // j 寻找之前匹配的位置
    }
    if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
        j++; // i的增加在for循环里
    }
    if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
        return (i - t.size() + 1);
    }
}

4 代码


4.1 C++

class Solution {
public:
    void getNext(int* next, const string& s) {
        int j = -1;
        next[0] = j;
        for (int i = 1; i < s.size(); i++) {
            while (j >= 0 && s[i] != s[j + 1]) {    //处理前后缀不相同
                j = next[j];    //向前回退
            }
            if (s[i] == s[j + 1]) { //找到相同的前后缀
                j++;
            }
            next[i] = j;    //将j(前缀的长度)赋给next[i]
        }
    }
    int strStr(string haystack, string needle) {
        if (needle.size() == 0) 
            return 0;
        int next[needle.size()];
        getNext(next, needle);
        int j = -1; 
        for (int i = 0; i < haystack.size(); i++) {
            while (j >= 0 && haystack[i] != needle[j + 1])  //不匹配
                j = next[j];    //j 寻找之前匹配的位置
            if (haystack[i] == needle[j + 1]) //匹配,j和i同时后移
                j++;    //i的增加在for里
            if (j == (needle.size() - 1) )  //文本串s里出现了模式串t
                return (i - needle.size() + 1);
        }return -1;
    }
};

4.2 C版本

int strStr(char * haystack, char * needle){
    int hLen = strlen(haystack);
    int nLen = strlen(needle);
    int *next = (int *)malloc(nLen*sizeof(int));
    int j = -1;
    next[0] = j;
    //求next数组
    for (int i = 1; i < nLen; i++) {
        while (j >= 0 && needle[i] != needle[j+1]) {
            j = next[j];
        }
        if (needle[i] == needle[j + 1])
            j++;
        next[i] = j;
    }
    //匹配
    j = -1;
    for (int i = 0; i < hLen; i++) {
        while (j >= 0 && haystack[i] != needle[j+1])
            j = next[j];
        if (haystack[i] == needle[j+1])
            j++;
        if (j == nLen - 1)
            return (i - nLen + 1);
    }
    return -1;
}

4.3 Java版本

class Solution {

    public void getNext(int[] next, String s) {
        int j = -1;
        next[0] = j;
        for (int i = 1; i < s.length(); i++) {
            while (j >= 0 && s.charAt(i) != s.charAt(j + 1))    //处理不匹配的情况
                j = next[j];
            if (s.charAt(i) == s.charAt(j + 1)) //处理匹配的情况,j和i都向后移
                j++;    //i的增加在for里
            
            next[i] = j;
        }
    }

    public int strStr(String haystack, String needle) {
        if (needle.length() == 0) 
            return 0;
        int [] next = new int[needle.length()];
        getNext(next, needle);
        int j = -1;
        for (int i = 0; i < haystack.length(); i++) {
            while (j >= 0 && haystack.charAt(i) != needle.charAt(j + 1) )
                j = next[j];
            if (haystack.charAt(i) == needle.charAt(j + 1))
                j++;
            if(j == needle.length() - 1)
                return i - needle.length() + 1;
        }
        return -1;
    }
}

4.4 Python版本

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        a = len(needle)
        b = len(haystack)
        if a == 0:
            return 0
        next = self.getnext(a,needle)
        p=-1
        for j in range(b):
            while p >= 0 and needle[p+1] != haystack[j]:
                p = next[p]
            if needle[p+1] == haystack[j]:
                p += 1
            if p == a-1:
                return j-a+1
        return -1

    def getnext(self,a,needle):
        next = ['' for i in range(a)]
        k = -1
        next[0] = k
        for i in range(1, len(needle)):
            while (k > -1 and needle[k+1] != needle[i]):
                k = next[k]
            if needle[k+1] == needle[i]:
                k += 1
            next[i] = k
        return next

4.5 JavaScript版本

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function (haystack, needle) {
    if (needle.length === 0)
        return 0;

    const getNext = (needle) => {
        let next = [];
        let j = -1;
        next.push(j);

        for (let i = 1; i < needle.length; ++i) {
            while (j >= 0 && needle[i] !== needle[j + 1])
                j = next[j];
            if (needle[i] === needle[j + 1])
                j++;
            next.push(j);
        }

        return next;
    }

    let next = getNext(needle);
    let j = -1;
    for (let i = 0; i < haystack.length; ++i) {
        while (j >= 0 && haystack[i] !== needle[j + 1])
            j = next[j];
        if (haystack[i] === needle[j + 1])
            j++;
        if (j === needle.length - 1)
            return (i - needle.length + 1);
    }

    return -1;
};

5 总结:

在一个串中查找是否出现过另一个串,这是KMP的看家本领。

扩展知识点就是next数组还可以优化为nextval数组。这里不做过多介绍。往年408 数据结构与算法中会有考察到。
可以参考王道。

下面是Carl老师的视频讲解:
帮你把KMP算法学个通透!(理论篇)
帮你把KMP算法学个通透!(求next数组代码篇)

KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。


By Suki 2023/3/2

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/383098.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2022 年度_职业项目总结_Java技术点归纳

Java技术点归纳目录概述需求&#xff1a;设计思路实现思路分析1.Structs 元工程改造2.个贷子系统开发3.架构的迭代开发&#xff0c;升级&#xff0c;部署&#xff0c;参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,fu…

学编程的 4 大阶段,你到哪了?

大家好&#xff0c;我是阅黑马学生无数的播妞……通过观察黑马学生的学习状态&#xff0c;播妞总结了他们来黑马后的四个学习状态&#xff0c;可以说&#xff0c;只要跨过这四个阶段&#xff0c;走向辉煌的编程人生就是必然的事情。阶段一&#xff1a;一窍不通这个阶段的学生&a…

2023初级会计详细学习计划打卡表!自律逆袭,一次上岸!

2023年初级会计职称考试报名时间&#xff1a;2月7日-28日考试时间&#xff1a;5月13日—17日给大家整理了《经济法基础》和《初级会计实务》两科超实用的学习打卡表重要程度、难易度、易错点、要求掌握内容、章节估分等都全部总结在一起&#xff0c;一目了然&#xff01;为什么…

rk3288-android8-IR-mouse

IR问题: mouse按键使用不了 然后排查: 1.排查上报 ir_key6{ rockchip,usercode <0xbf00>;rockchip,key_table <0xff KEY_POWER>,<0xfe KEY_MUTE>, <0xfd KEY_1>, <0xfc KEY_2>, <0xfb KEY_3>, <0xfa KEY_4>, <0xf9 KEY_5>…

JavaEE进阶第六课:SpringBoot ⽇志⽂件

上篇文章介绍了SpringBoot配置文件&#xff0c;这篇文章我们将会介绍SpringBoot ⽇志⽂件 荔枝1.日志有什么用2.自定义日志输出2.1获取程序日志对象2.2使用相关方法输出日志2.3日志级别2.3.1日志级别的作用2.3.2日志级别如何设置2.4日志格式3.持久化日志4.更简单的日志输出4.1使…

【移动端表格组件】uniapp简单实现H5,小程序,APP多端兼容表格功能,复制即用,简单易懂【详细注释版本】

前言&#xff1a; 由于最近需要做移动端的项目 有个pc端的后台系统里面需要移一部分页面过来 而里面就有很多的表格&#xff0c;我就开始惯例网上先找前人栽的树&#xff0c;我好乘凉 然后找了一圈发现&#xff0c;不管是主流的移动端ui库或者网上自己写的帖子&#xff0c;或者…

224. 基本计算器

224. 基本计算器给你一个字符串表达式 s &#xff0c;请你实现一个基本计算器来计算并返回它的值。注意:不允许使用任何将字符串作为数学表达式计算的内置函数&#xff0c;比如 eval() 。 示例 1&#xff1a;输入&#xff1a;s "1 1"输出&#xff1a;2示例 2&#…

【Pygame实战】变异狗大战:据说是最近还不错的小游戏,这一个个玩到表情崩坏,点开即玩,赶紧来~(Python代码搞笑版本)

前言 只有你想不到&#xff0c;没有我找不到写不了的好游戏&#xff01; 哈喽。我是你们的栗子同学啦~ 所有文章完整的素材源码都在&#x1f447;&#x1f447; 粉丝白嫖源码福利&#xff0c;请移步至CSDN社区或文末公众hao即可免费。 今天小编去了我朋友家里玩儿&#xff0c…

ExSwin-Unet 论文研读

ExSwin-Unet摘要1 引言2 方法2.1 基于窗口的注意力块2.2 外部注意力块2.3 不平衡的 Unet 架构2.4 自适应加权调整2.5 双重损失函数3 实验结果3.1 数据集3.2 实现细节3.3 与 SOTA 方法的比较3.4 消融研究4 讨论和限制5 结论数据集来源&#xff1a; https://feta.grand-challenge…

图扑 Web SCADA 智慧制硅厂,打造新时代制硅工业

前言 我国目前是全球最大的工业硅生产国、消费国和贸易国&#xff0c;且未来该产业的主要增量也将来源于我国。绿色低碳发展已成为全球大趋势和国际社会的共识&#xff0c;随着我国“双碳”目标的推进&#xff0c;光伏产业链快速发展&#xff0c;在光伏装机需求的带动下&#…

武汉凯迪正大KD305系列智能数字绝缘电阻测试仪

一、概述 KD305系列智能数字绝缘电阻测试仪采用嵌入式工业单片机实时操作系统&#xff0c;数字模拟指针与数字段码显示结合&#xff0c;该系列表具有多种电压输出等级&#xff08;500V、1000V、2500V、5000V、10000V&#xff09;、容量大、抗干扰强、模拟指针与数字同步显示、交…

数据结构考研习题精选

&#xff11; A假设比较&#xff54;次&#xff0c;由于换或不换&#xff0c;则必然有&#xff12;&#xff3e;&#xff54;种可能。又设有&#xff4e;个关键字&#xff0c;&#xff4e;&#xff01;排列组合&#xff0c;则必然有&#xff12;&#xff3e;&#xff54;&…

vue-element-admin执行npm install时的一些报错。

文章目录1. 首先在gitee上拉取的中文版2. 执行npm install的一些报错3. 参考文章1. 首先在gitee上拉取的中文版 git clone -b i18n https://gitee.com/panjiachen/vue-element-admin.git 2. 执行npm install的一些报错 npm install Please make sure you have the correct acc…

跨境电商平台,亚马逊、eBay、Shopee……哪个好?

2023一开始&#xff0c;随着各项利好政策的出台&#xff0c;中国跨境电商正在重新步入最好的时代。一些跨境电商企业纷纷开启上市热潮&#xff0c;身边许多人也跃跃欲试想转行跨境电商。专业数据显示&#xff0c;接下来将会有更多的跨境企业走向资本化的道路&#xff0c;借助资…

设备运行状况不能远程手机查看。难道就妥协吗?为何不试试这个办法

一、背景 随着国家经济结构逐步调整&#xff0c;纺织行业自动化、智能化水平逐步提高&#xff0c;业内竞争程度也将加大&#xff1b;整个市场变化快&#xff0c;并呈现出智能化、通用化、网络化、复杂化的新发展趋势。客户订单小批量、个性化、快速交货的特点越来越明显&#…

阅读(1)-----六级

目录 1.单词不懂怎么办&#xff1f; 1.1构词法 1.2上下文 2.句子不通怎么办&#xff1f; 3.时间不够怎么办 &#xff1f; 4.题型 4.1细节题 问文章的细节 4.2主旨题(文章主旨和段落主旨) 4.3语义题 4.4观点题 &#xff08;一共三种&#xff0c;支持、反对和中立 &…

从0开始学python -47

Python CGI编程 -2 GET和POST方法 浏览器客户端通过两种方法向服务器传递信息&#xff0c;这两种方法就是 GET 方法和 POST 方法。 使用GET方法传输数据 GET方法发送编码后的用户信息到服务端&#xff0c;数据信息包含在请求页面的URL上&#xff0c;以"?"号分割…

【面试题】社招中级前端笔试面试题总结

大厂面试题分享 面试题库后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★地址&#xff1a;前端面试题库typeof null 的结果是什么&#xff0c;为什么&#xff1f;typeof null 的结果是Object。在 JavaScript 第一个版本中&#xff0c;所有值都存储在…

Nginx的反向代理配置笔记

1、反向代理的概念&#xff1a; 对一个大型网站来说&#xff0c;随着网站的访问量的快速增长&#xff0c;单台服务器已经无法承担大量用户的并发访问&#xff0c;必须采用多态服务器协同工作&#xff0c;以提高计算机系统的处理能力。通过Nginx提供的反向代理和负载均衡功能&a…

扬帆优配|国家队重磅出手!千亿巨头突然爆雷,股价狂跌12%!

国家队又出手啦&#xff01; 综合天眼查和国家商场监督管理总局旗下企业信用信息公示系统显现&#xff0c;长江存储科技控股有限责任公司股东结构新增国家集成电路工业出资基金二期股份有限公司、长江工业出资集团有限公司及湖北长晟开展等股东。其中&#xff0c;大基金二期认缴…