LeetCode 28. 找出字符串中第一个匹配项的下标(暴力匹配 + KMP 算法详解)
·
LeetCode 28. 找出字符串中第一个匹配项的下标(暴力匹配 + KMP 算法详解)
题目链接
28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
题目描述
给你两个字符串 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 <= 10^4haystack和needle仅由小写英文字符组成
解题思路
这道题是字符串匹配的经典问题,主要有两种解法:
- 暴力匹配:简单直观,时间复杂度
O(m×n)。 - KMP 算法:高效,时间复杂度
O(m+n)。
方法一:暴力匹配
思路
从主串 haystack 的每个位置开始,尝试与模式串 needle 进行匹配:
- 遍历主串的每个可能的起点
i(从 0 到m-n)。 - 对于每个起点,逐个字符比较
haystack[i+j]和needle[j]。 - 如果所有字符都匹配(
j == n),返回起点i。 - 如果没有任何起点匹配成功,返回
-1。
Python 代码
def strStr_brute(haystack: str, needle: str) -> int:
m = len(haystack)
n = len(needle)
# i主串起点
for i in range(m - n + 1):
j = 0
while j < n and haystack[i + j] == needle[j]:
j += 1
if j == n:
return i
return -1
Java 代码
class BruteForce {
public int strStr(String haystack, String needle) {
int m = haystack.length();
int n = needle.length();
for (int i = 0; i <= m - n; i++) {
int j = 0;
while (j < n && haystack.charAt(i + j) == needle.charAt(j)) {
j++;
}
if (j == n) {
return i;
}
}
return -1;
}
}
复杂度分析
- 时间复杂度:
O(m×n),其中m是主串长度,n是模式串长度。最坏情况下需要比较m×n次。 - 空间复杂度:
O(1)。
优缺点
- 优点:简单直观,易于理解。
- 缺点:时间复杂度高,在字符串较长时可能超时。
方法二:KMP 算法
思路
KMP(Knuth-Morris-Pratt)算法的核心思想是:利用已经匹配过的信息,避免重复匹配。
当匹配失败时,不是简单地将模式串向后移动一位,而是根据 前缀函数(next 数组) 跳过不可能匹配的位置。
什么是 next 数组?
next[i] 表示模式串 needle[0...i] 中,最长的相同前缀和后缀的长度。
例如,模式串 "ababc" 的 next 数组为 [0, 0, 1, 2, 0]:
next[0] = 0:单个字符没有前缀和后缀。next[1] = 0:"ab"没有相同的前缀和后缀。next[2] = 1:"aba"的前缀"a"和后缀"a"相同。next[3] = 2:"abab"的前缀"ab"和后缀"ab"相同。next[4] = 0:"ababc"没有相同的前缀和后缀。
KMP 匹配过程
- 使用两个指针
i(主串)和j(模式串)。 - 当
haystack[i] == needle[j]时,两个指针都向后移动。 - 当不匹配时:
- 如果
j > 0,将j回退到next[j-1],继续匹配。 - 如果
j == 0,将i向后移动。
- 如果
- 当
j == n时,说明匹配成功,返回i - j。
Python 代码
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
def build_next(pattern):
n = len(pattern)
next_arr = [0] * n
prefix_len = 0
i = 1
while i < n:
if pattern[prefix_len] == pattern[i]:
prefix_len += 1
next_arr[i] = prefix_len
i += 1
else:
if prefix_len == 0:
next_arr[i] = 0
i += 1
else:
prefix_len = next_arr[prefix_len - 1]
return next_arr
next_arr = build_next(needle)
i = 0 # haystack
j = 0 # needle
while i < len(haystack):
if haystack[i] == needle[j]:
i += 1
j += 1
elif j > 0:
j = next_arr[j - 1]
else:
i += 1
if j == len(needle):
return i - j
return -1
Java 代码
class KMP {
public int strStr(String haystack, String needle) {
int[] next = buildNext(needle);
int i = 0; // haystack指针
int j = 0; // needle指针
while (i < haystack.length()) {
if (haystack.charAt(i) == needle.charAt(j)) {
i++;
j++;
} else if (j > 0) {
j = next[j - 1];
} else {
i++;
}
if (j == needle.length()) {
return i - j;
}
}
return -1;
}
private int[] buildNext(String needle) {
int n = needle.length();
int[] next = new int[n];
int prefixLen = 0;
int i = 1;
while (i < n) {
if (needle.charAt(prefixLen) == needle.charAt(i)) {
prefixLen++;
next[i] = prefixLen;
i++;
} else {
if (prefixLen == 0) {
next[i] = 0;
i++;
} else {
prefixLen = next[prefixLen - 1];
}
}
}
return next;
}
}
复杂度分析
- 时间复杂度:
O(m+n),其中m是主串长度,n是模式串长度。构建 next 数组需要O(n),匹配过程需要O(m)。 - 空间复杂度:
O(n),需要存储 next 数组。
优点
- 时间复杂度低,适合处理长字符串。
- 是字符串匹配的经典算法,面试常考。
KMP 算法详解
构建 next 数组
def build_next(pattern):
n = len(pattern)
next_arr = [0] * n
prefix_len = 0
i = 1
while i < n:
if pattern[prefix_len] == pattern[i]:
prefix_len += 1
next_arr[i] = prefix_len
i += 1
else:
if prefix_len == 0:
next_arr[i] = 0
i += 1
else:
prefix_len = next_arr[prefix_len - 1]
return next_arr
关键点:
prefix_len表示当前最长相同前缀后缀的长度。- 当
pattern[prefix_len] == pattern[i]时,说明可以扩展最长相同前缀后缀。 - 当不匹配时,回退
prefix_len到next_arr[prefix_len - 1],继续尝试。
匹配过程
i = 0 # haystack
j = 0 # needle
while i < len(haystack):
if haystack[i] == needle[j]:
i += 1
j += 1
elif j > 0:
j = next_arr[j - 1]
else:
i += 1
if j == len(needle):
return i - j
关键点:
- 匹配成功时,两个指针都向后移动。
- 匹配失败时,根据 next 数组回退
j,避免重复匹配。 j == len(needle)时,说明完全匹配,返回起始位置。
两种方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|
| 暴力匹配 | O(m×n) | O(1) | 简单直观,但效率低 |
| KMP 算法 | O(m+n) | O(n) | 效率高,但较复杂 |
常见问题解答
Q1:为什么暴力匹配的循环条件是 i <= m - n?
因为当 i > m - n 时,主串剩余长度小于模式串长度,不可能匹配成功。例如主串长度为 5,模式串长度为 3,当 i = 3 时,主串剩余长度为 2,小于 3,无法匹配。
Q2:KMP 算法中 next 数组的第一个元素为什么是 0?
因为单个字符没有真前缀和真后缀,所以最长相同前缀后缀的长度为 0。
Q3:这道题在面试中应该用哪种方法?
如果面试官没有特别要求,建议先写暴力匹配,然后优化为 KMP。这样既展示了基础能力,又展示了算法优化能力。
总结
这道题是字符串匹配的经典问题,核心要点:
- 暴力匹配:从每个位置开始尝试匹配,简单但效率低。
- KMP 算法:利用 next 数组避免重复匹配,时间复杂度
O(m+n)。 - next 数组:记录模式串的最长相同前缀后缀长度。
关键点回顾:
- 暴力匹配:双重循环,逐个字符比较
- KMP 核心:构建 next 数组,匹配失败时回退
- 时间复杂度:暴力
O(m×n),KMPO(m+n) - 空间复杂度:暴力
O(1),KMPO(n)
KMP 算法是字符串匹配的经典算法,虽然理解起来有一定难度,但掌握后对解决其他字符串问题很有帮助。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注,我会持续更新更多 LeetCode 题解和算法干货!
更多推荐




所有评论(0)