在问 · 算法题解

28. 找出字符串中第一个匹配项的下标

简单双指针暴力匹配力扣原题 ↗

题目描述

输入两个字符串 haystack 和 needle。

在 haystack 中查找 needle 作为连续子串出现的位置,返回最早一次出现的起始下标,下标从 0 开始。

如果 needle 从未在 haystack 中出现,返回 -1。

示例 1
输入:haystack = "sadbutsad", needle = "sad"
输出:0

解释:"sad" 在下标 0 和 6 都出现过,最早的是 0。

示例 2
输入:haystack = "leetcode", needle = "leeto"
输出:-1

解释:"leetcode" 中找不到 "leeto"。

数据范围

  • 1 <= haystack.length, needle.length <= 10^4
  • 两个字符串都只含小写英文字母。

解题思路:暴力匹配

用两个指针:i 是尝试的起点,j 是 needle 中正在比对的位置。对每个起点 i,j 从 0 往右逐个比较 haystack[i + j] 与 needle[j],相同就继续;遇到不同就换下一个起点。j 走到 m 说明整个 needle 都对上了,返回 i。起点只需试到 n − m,再往后剩余长度不够。

为什么正确

处理起点 i 时,所有更小的起点都已确认不匹配;内层循环中 haystack[i : i + j] == needle[:j]。

复杂度分析

最坏时间 O((n − m + 1) · m),如 haystack="aaaa…ab"、needle="aab";空间 O(1)。KMP 可以做到 O(n + m)。

易错点

外层循环上界是 n − m(含),写成 n − m 不含会漏掉 needle 在末尾的情况,不加限制则 haystack[i + j] 会越界。needle 比 haystack 长时循环一次也不执行,直接返回 -1。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n, m = len(haystack), len(needle)
        for i in range(n - m + 1):
            j = 0
            while j < m and haystack[i + j] == needle[j]:
                j += 1
            if j == m:
                return i
        return -1

载入编辑器,自己跑一遍 →

其他解法

暴力匹配最简单但最坏 O(n·m);KMP 用前缀函数保证最坏线性;Rabin-Karp 用滚动哈希在期望线性时间内筛掉不可能的位置。

解法 2:KMP 算法 时间 O(n+m),空间 O(m)

先为 needle 求前缀函数 pi:pi[i] 是 needle[0..i] 最长的相等真前缀与真后缀的长度。再扫描 haystack,j 表示当前已匹配的长度;字符不等时把 j 回退到 pi[j-1],而不是把 haystack 的指针退回去。j 达到 m 时返回 i-m+1。

haystack 的指针从不后退,最坏情况也是线性时间。前缀函数的含义和回退过程较难理解,短串上未必比暴力快。

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n, m = len(haystack), len(needle)
        if m == 0:
            return 0
        pi = [0] * m
        j = 0
        for i in range(1, m):
            while j > 0 and needle[i] != needle[j]:
                j = pi[j - 1]
            if needle[i] == needle[j]:
                j += 1
            pi[i] = j
        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

解法 3:Rabin-Karp 滚动哈希 时间 O(n+m),空间 O(1)

把长度为 m 的字符串看成 B 进制数,对大质数取模得到哈希值。窗口右移一位时,减去最高位字符乘 B^(m-1) 的贡献,再乘 B 并加上新字符,O(1) 得到新哈希。哈希值与 needle 相等时,再逐字符比较一次以排除冲突。

思路直观,并且能推广到多模式串和二维匹配。期望复杂度是线性的,但哈希冲突很多时会退化到 O(n·m);还要小心取模运算的溢出。

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n, m = len(haystack), len(needle)
        if m > n:
            return -1
        B, MOD = 131, 1_000_000_007
        pw = 1
        for _ in range(m - 1):
            pw = pw * B % MOD
        hn = hh = 0
        for i in range(m):
            hn = (hn * B + ord(needle[i])) % MOD
            hh = (hh * B + ord(haystack[i])) % MOD
        for i in range(n - m + 1):
            if hh == hn and haystack[i:i + m] == needle:
                return i
            if i + m < n:
                hh = (hh - ord(haystack[i]) * pw % MOD + MOD) % MOD
                hh = (hh * B + ord(haystack[i + m])) % MOD
        return -1

双指针 专题的其他题目

查看 双指针 全部题目 →