力扣刷题19

第一题:找出字符串中第一个匹配项的下标

来源: https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/

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

这个题我刚开始想到的是暴力法,遍历第一个字符串,遍历每个位置,直到找出完全匹配的第一个位置。

python 复制代码
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n = len(haystack)
        m = len(needle)
        if m == 0:
            return 0
        # 遍历所有可能的起始位置
        for i in range(n - m + 1):
            if haystack[i:i+m] == needle:
                return i
        return -1

时间复杂度为O((n-m)*m),最坏的情况就是需要逐个字符比较,得优化一下。

所以我用了KMP算法,先处理needle生成前缀函数(部分匹配表),在匹配失败时跳过不必要的比较,从而实现线性时间复杂度。

python 复制代码
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n = len(haystack)
        m = len(needle)
        if m == 0:
            return 0
        
        # 1. 构建前缀函数数组 pi
        pi = [0] * m
        for i in range(1, m):
            j = pi[i-1]
            while j > 0 and needle[i] != needle[j]:
                j = pi[j-1]
            if needle[i] == needle[j]:
                j += 1
            pi[i] = j
        
        # 2. 利用 pi 进行匹配
        j = 0
        for i in range(n):
            while j > 0 and haystack[i] != needle[j]:
                j = pi[j-1]
            if haystack[i] == needle[j]:
                j += 1
            if j == m:
                return i - m + 1
        return -1

记录一下KMP 算法的核心 ------前缀函数:

前缀函数也叫部分匹配表,是针对模式串needle生成的一个数组pi,其中的每一个元素pii表示:

对于needle0...i这个字串,最长的相等真前缀和真后缀的长度。

真前缀是指不包含子串本身的前缀,比如"abc"的真前缀是"a"、"ab";真后缀的定义也一样。

举个例子,以needle = "ababc"为例,我们写出它的前缀函数数组:

|---------|--------------------------------|---------|
| 子串 | 最长相等真前缀&真后缀 | pii |
| "a" | 无(只有一个字符) | 0 |
| "ab" | 无("a" != "b") | 0 |
| "aba" | "a"(前缀)= "a"(后缀) | 1 |
| "abab" | "ab"(前缀) = "ab"(后缀) | 2 |
| "ababc" | 无("abab" != "bc","aba" != "c") | 0 |

最终pi = 0,0,1,2,0

前缀函数的核心作用:避免重复比较。

暴力匹配失败时,会把needle总体后移一位,重新从头比较,而前缀函数可以做到:失败后不从头开始,直接跳到pij-1的位置继续比较(j是当前匹配失败的位置)。

举个例子:haystack = "abababc",needle = "ababc":

1、匹配到haystack4 = "a",needle4 = "c"时失败,前四位"abab"都匹配;

2、前缀函数表明pi3 = 2("abab"的最长相等前后缀长度是2);

3、直接把needle后移,让needle2对齐haystack4,继续比较,跳过了前两位"ab"的重复比较。

现在来拆解一下前缀函数的生成逻辑:

python 复制代码
 pi = [0] * m
        for i in range(1, m):
            j = pi[i-1]
            while j > 0 and needle[i] != needle[j]:
                j = pi[j-1]
            if needle[i] == needle[j]:
                j += 1
            pi[i] = j

构造方法上面已经写了。


关键细节return i - m + 1 的推导

假设needle的长度是m,当j = m时,说明needle = 0,...,m-1均匹配成功,此时 i 是主串中最后一个匹配字符的下标,起始下标 = 最后一个匹配下标 - (模式串长度 - 1) = i - (m-1) = i - m + 1

例:主串 abcabc,模式串 abc,匹配到 i=2j=3,起始下标 = 2 - 3 + 1 = 0,符合预期。


举个例子模拟一下整个过程:

主串:haystack = "abababc",模式串:needle = "ababc"

第一步:构建pi数组(前缀函数)

过程在上面,pi = 0,0,1,2,0

步骤 i j(初始 = pi i-1 while 循环判断(j>0 且 needle i≠needle j 字符匹配判断 j 更新后 pii 最终 pi 数组
1 1 j=pi0=0 j=0,不进入循环 needle 1=B vs needle 0=A → 不匹配 j=0 0 0,0,0,0,0
2 2 j=pi1=0 j=0,不进入循环 needle 2=A vs needle 0=A → 匹配 j=1 1 0,0,1,0,0
3 3 j=pi2=1 j=1>0?是;needle 3=B vs needle 1=B → 匹配,不进入循环 匹配 j=2 2 0,0,1,2,0
4 4 j=pi3=2 j=2>0?是;needle 4=C vs needle 2=A → 不匹配 → 执行 j=pi 1=0现在 j=0,退出循环 needle 4=C vs needle 0=A → 不匹配 j=0 0 0,0,1,2,0

第二步:kmp匹配过程

初始化:j=0(模式串指针),遍历 i 从 0 到 6(主串所有字符):

步骤 i haystacki while 循环判断(j>0 且 haystack i≠needle j 字符匹配判断 j 更新后 j==m? 结果
1 0 A j=0,不进入循环 A == A → 匹配 j=1 1≠5 继续
2 1 B j=1>0?是;B == B → 匹配,不进入循环 B == B → 匹配 j=2 2≠5 继续
3 2 A j=2>0?是;A == A → 匹配,不进入循环 A == A → 匹配 j=3 3≠5 继续
4 3 B j=3>0?是;B == B → 匹配,不进入循环 B == B → 匹配 j=4 4≠5 继续
5 4 A j=4>0?是;A vs needle 4=C → 不匹配 → 执行 j=pi 3=2现在 j=2>0,再判断:A vs needle 2=A → 匹配,退出循环 A == A → 匹配 j=3 3≠5 继续
6 5 B j=3>0?是;B vs needle 3=B → 匹配,不进入循环 B == B → 匹配 j=4 4≠5 继续
7 6 C j=4>0?是;C vs needle 4=C → 匹配,不进入循环 C == C → 匹配 j=5 5==5 匹配成功

第三步:计算匹配起始下标

当j = 5即m = 5时触发返回,i - m + 1 = 2。

验证结果:主串 haystack 中,从下标 2 开始的子串是 "ABABC",和模式串完全匹配(haystack[2:7] = "ABABC"),结果正确。

步骤 4 结束后 j=4,步骤 5 中 haystack[4]=Aneedle[4]=C 不匹配,所以触发回退:j = pi[j-1] = pi[3] = 2(这就是 KMP 的核心 ------ 利用 pi 数组避免主串指针 i 回退);

回退后 j=2,此时 haystack[4]=Aneedle[2]=A 匹配,所以 j 继续增加到 3,而不是从头开始匹配(如果是暴力匹配,i 会回退到 1,效率低)。

ok,记录完毕,打了几天游戏,我又有灵感了,继续研究下一题。

相关推荐
05候补工程师3 分钟前
【考研高数核心突破】极限的本质、高频解题套路与海涅定理深度解析(附经典例题思维导图式拆解)
经验分享·笔记·考研·算法
智者知已应修善业7 分钟前
【51单片机8个LED的花样12亮34熄56间隔78闪烁3秒3闪烁】2023-11-4
c++·经验分享·笔记·算法·51单片机
老鱼说AI13 分钟前
统计学习方法第五章:从浅入深解析决策树
人工智能·深度学习·算法·决策树·机器学习·学习方法
KaMeidebaby15 分钟前
卡梅德生物技术快报|蛋白修饰调控 NETosis 分子机制及实验研究进展
前端·数据库·人工智能·算法·百度
初中就开始混世的大魔王19 分钟前
5 Fast DDS-Discovery
网络·c++·算法·中间件
Deep-w19 分钟前
【MATLAB】基于模型预测控制的自适应巡航车辆过渡工况安全控制研究
开发语言·人工智能·算法·机器学习·matlab
运行时记录21 分钟前
Sirchmunk 让搜索随查询自进化
算法
浮生望25 分钟前
双指针算法面试通关指南:从入门到精通
算法
SimpleLearingAI28 分钟前
PyTorch & Numpy 实现线性回归详解
人工智能·算法·多模态大模型
papership28 分钟前
【入门级-数据结构-1、线性结构:链 表(单链表、双向链表、循环链表 )】
数据结构·算法·链表