问题描述
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1。
示例
示例 1:
1 | 输入:haystack = "sadbutsad", needle = "sad" |
示例 2:
1 | 输入:haystack = "leetcode", needle = "leeto" |
约束条件
1 <= haystack.length, needle.length <= 10^4haystack和needle仅由小写英文字符组成
解题思路
这道题是经典的字符串匹配问题,最优解是使用 KMP(Knuth-Morris-Pratt)算法。KMP算法的核心思想是:当匹配失败时,利用已匹配的前缀信息,避免从头开始重新匹配。
KMP算法核心概念
- next数组(部分匹配表):记录模式串中每个位置的最长相等前后缀长度
- 失配跳转:当字符不匹配时,根据next数组跳转到合适位置,而不是从头开始
算法步骤详解
视频讲解
步骤1:构建next数组
next数组的定义:next[i] 表示模式串 needle[0...i] 的最长相等前后缀的长度。
构建过程示例(以 needle = "ababaca" 为例):
1 | 模式串: a b a b a c a |
详细构建过程:
i=0:next[0] = 0(规定为0)i=1:"ab"无相等前后缀,next[1] = 0i=2:"aba"最长相等前后缀是"a",长度为1,next[2] = 1i=3:"abab"最长相等前后缀是"ab",长度为2,next[3] = 2i=4:"ababa"最长相等前后缀是"aba",长度为3,next[4] = 3i=5:"ababac"无相等前后缀,next[5] = 0i=6:"ababaca"最长相等前后缀是"a",长度为1,next[6] = 1
步骤2:模式匹配
使用构建好的next数组进行字符串匹配。
实现细节
KMP算法的两个关键函数
- 构建next数组:
1 | func buildNext(str string) []int { |
- 执行匹配:
1 | func strStr(haystack string, needle string) int { |
具体例子演示
让我们用 haystack = "ababcababa" 和 needle = "ababa" 来演示整个过程:
1. 构建next数组
1 | needle = "ababa" |
构建过程:
i=1, j=0:'b' != 'a',next[1] = 0i=2, j=0:'a' == 'a',j++,next[2] = 1i=3, j=1:'b' == 'b',j++,next[3] = 2i=4, j=2:'a' == 'a',j++,next[4] = 3
2. 执行匹配过程
1 | haystack: a b a b c a b a b a |
匹配步骤:
i=0, j=0:'a' == 'a'✓,j++i=1, j=1:'b' == 'b'✓,j++i=2, j=2:'a' == 'a'✓,j++i=3, j=3:'b' == 'b'✓,j++i=4, j=4:'c' != 'a'✗, 失配!
失配处理:
j = next[j-1] = next[3] = 2- 跳转后继续比较
haystack[4]和needle[2]
1 | haystack: a b a b c a b a b a |
i=4, j=2:'c' != 'a'✗, 继续失配j = next[j-1] = next[1] = 0i=4, j=0:'c' != 'a'✗,j保持为0i=5, j=0:'a' == 'a'✓,j++- 继续匹配… 最终在位置5找到完整匹配
代码实现
1 | func strStr(haystack string, needle string) int { |
代码解析
主函数 strStr:
- 获取两个字符串的长度
n和m - 调用
buildNext函数构建next数组 - 使用双指针
i和j进行KMP匹配 - 当失配时,根据next数组进行智能跳转
- 当完全匹配时,返回起始位置
i - m + 1
辅助函数 buildNext:
- 创建长度为
m的next数组,初始化next[0] = 0 - 使用双指针
i和j构建next数组 - 当字符不匹配时,根据已有的next值进行回退
- 当字符匹配时,更新next数组
算法优势
这个实现的优点:
- 代码结构清晰:将next数组构建抽取为独立函数,职责分离
- 易于理解:主函数专注于匹配逻辑,辅助函数专注于预处理
- 可复用性强:
buildNext函数可以在其他需要next数组的场景中复用 - 空间效率高:只使用必要的空间存储next数组
方法比较
| 方面 | 暴力匹配 | KMP算法 |
|---|---|---|
| 时间复杂度 | O(n×m) | O(n+m) |
| 空间复杂度 | O(1) | O(m) |
| 预处理 | 无 | 需要构建next数组 |
| 失配处理 | 回退到起始位置+1 | 智能跳转,避免重复比较 |
| 适用场景 | 短模式串 | 长模式串或重复匹配 |
| 推荐度 | ★★☆☆☆ | ★★★★★ |
复杂度分析
时间复杂度:$O(n + m)$
- 构建next数组:$O(m)$
- 模式匹配:$O(n)$
- 虽然有嵌套循环,但每个字符最多被比较常数次
空间复杂度:$O(m)$
- next数组需要 $O(m)$ 空间
KMP算法优化详解
1. next数组的本质
next数组记录的是最长相等前后缀长度,这样设计的目的是:
- 当失配时,我们知道前面已经匹配的部分中哪些可以复用
- 避免不必要的回退和重复比较
2. 失配跳转的数学原理
假设在位置 i 失配,此时已匹配长度为 j:
- 传统方法:回退到起始位置,时间复杂度退化为 $O(n \times m)$
- KMP方法:跳转到
next[j-1],利用已匹配信息,时间复杂度保持 $O(n + m)$
关键收获
- KMP的核心思想:利用已匹配的信息,避免无效的重复比较
- next数组是关键:正确理解和构建next数组是掌握KMP的重点
- 失配处理策略:
j = next[j-1]而不是j = 0,这是效率提升的关键 - 时间复杂度保证:虽然有嵌套循环,但每个位置最多被访问常数次
- 实际应用价值:KMP在文本编辑器、搜索引擎等场景中有重要应用
常见陷阱
- 边界条件:空字符串的处理
- 索引计算:返回值应该是
i - m + 1而不是i - next数组理解:要明确是"长度"而不是"索引"
- 失配逻辑:必须是
j > 0时才能跳转
这道题不仅考查字符串处理能力,更重要的是对KMP算法思想的深入理解。掌握KMP算法对于解决复杂的字符串匹配问题具有重要意义。