力扣解题-28. 找出字符串中第一个匹配项的下标
实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
说明:
当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。
示例 1:
输入:haystack = "hello", needle = "ll"
输出:2
示例 2:
输入:haystack = "aaaaa", needle = "bba"
输出:-1
提示:
1 <= haystack.length, needle.length <= 104
haystack 和 needle 仅由小写英文字符组成
Related Topics
双指针、字符串、字符串匹配
第一次解答
解题思路
核心方法:调用Java内置indexOf方法,直接利用JDK优化后的字符串匹配逻辑,实现极简代码且性能拉满,时间复杂度接近O(n+m)(n为haystack长度,m为needle长度),是工程开发中最简洁的解法。
核心逻辑拆解
Java的String.indexOf(String str)方法底层实现了高效的字符串匹配算法(并非简单暴力匹配,JDK会根据字符串长度选择不同优化策略),其核心行为完全贴合本题要求:
- 若
needle为空字符串,返回0; - 若
needle在haystack中存在,返回第一个匹配的起始下标; - 若不存在,返回-1。
该解法直接复用JDK的成熟实现,无需手动编写匹配逻辑,兼顾简洁性和性能。
性能说明
- 时间复杂度:接近O(n+m)(JDK内置实现的优化效果),耗时0ms击败100%用户;
- 空间复杂度:O(1)(无额外内存开销,仅调用内置方法);
- 优势:代码行数最少,无需关注匹配细节,开发效率极高;
- 内存表现:42.21MB击败81.40%用户,是内置方法的正常内存开销,无冗余操作。
public int strStr(String haystack, String needle) {
int index=haystack.indexOf(needle);
return index;
}
暴力匹配法
解题思路
核心方法:暴力匹配法(逐位比对),手动实现字符串匹配的基础逻辑,逐个遍历haystack的起始位置,与needle逐字符比对,是理解字符串匹配核心原理的基础解法。
核心逻辑拆解
暴力匹配的核心是“穷举所有可能的起始位置”:
- 若
needle为空,直接返回0; - 遍历
haystack中所有可能的起始位置i(范围:0 ~ haystack.length() - needle.length()); - 从
i开始,逐字符比对haystack[i+j]和needle[j](j为needle的字符下标); - 若所有字符都匹配,返回起始下标
i;若某一位不匹配,跳出当前比对,尝试下一个起始位置; - 遍历完所有位置仍未匹配,返回-1。
具体步骤(以示例1 haystack="hello",needle="ll"为例)
- needle长度=2,haystack长度=5,起始位置范围:0~3;
- i=0:比对h vs l → 不匹配;
- i=1:比对e vs l → 不匹配;
- i=2:比对l vs l → 匹配,继续比对l vs l → 全部匹配,返回2。
性能说明
- 时间复杂度:O(n×m)(最坏情况需遍历n-m+1个起始位置,每个位置比对m个字符);
- 空间复杂度:O(1)(仅使用几个指针变量);
- 优势:逻辑直观,无需依赖内置方法,是字符串匹配的入门级实现;
- 劣势:在最坏场景(如haystack="aaaaa...a",needle="aaaaab")下性能较差,无法适配10⁴级别的大数据量。
public int strStr(String haystack, String needle) {
// 处理空模式串
if (needle.isEmpty()) return 0;
int n = haystack.length();
int m = needle.length();
// 遍历所有可能的起始位置
for (int i = 0; i <= n - m; i++) {
int j = 0;
// 逐字符比对
while (j < m && haystack.charAt(i + j) == needle.charAt(j)) {
j++;
}
// 全部匹配成功
if (j == m) {
return i;
}
}
// 无匹配项
return -1;
}
示例解答
解题思路
核心方法:KMP算法(最优字符串匹配算法),通过预处理needle生成next数组(部分匹配表),避免暴力匹配中的重复比对,将时间复杂度优化至O(n+m),是字符串匹配的经典最优解。
核心原理铺垫
KMP算法的核心是“利用已匹配的部分信息,跳过不必要的比对”:
next数组:记录needle中每个位置j的“最长相等前后缀长度”,用于匹配失败时快速回退needle的指针,而非回退haystack的指针;- 匹配过程:
- 用
i遍历haystack,j遍历needle; - 字符匹配时,
i和j同时前进; - 字符不匹配时,
j根据next数组回退(j = next[j-1]),i不回退; - 当
j等于needle长度时,匹配成功,返回i-j。
- 用
next数组生成逻辑
next[i]表示needle[0..i]的最长相等前后缀长度:
- 初始化
j=0(前缀指针),next[0]=0; - 遍历
i=1到needle.length()-1:- 若
needle[i] != needle[j]且j>0,j回退到next[j-1]; - 若
needle[i] == needle[j],j++; next[i] = j。
- 若
性能优势
- 时间复杂度:O(n+m)(预处理
next数组O(m),匹配过程O(n)),无重复比对,适配10⁴级别的大数据量; - 空间复杂度:O(m)(存储
next数组); - 核心价值:在
haystack很长、needle有重复前缀的场景下,性能远超暴力匹配,是面试高频考点。
public int strStr(String haystack, String needle) {
if (needle.isEmpty()) return 0; // 模式串为空,直接返回 0
int[] next = getNext(needle); // 先生成 next 数组
int i = 0; // 文本串指针
int j = 0; // 模式串指针
while (i < haystack.length()) { // 遍历文本串
// 字符相等,两个指针都前进
if (haystack.charAt(i) == needle.charAt(j)) {
i++;
j++;
}
// 匹配成功:j 等于模式串长度,返回起始下标
if (j == needle.length()) {
return i - j;
}
// 字符不相等的情况
else if (i < haystack.length() && haystack.charAt(i) != needle.charAt(j)) {
// j≠0:利用 next 数组回退 j,不回退 i(KMP 优化点)
if (j != 0) {
j = next[j - 1];
}
// j=0:没有可回退的,只能前进文本串指针 i
else {
i++;
}
}
}
return -1; // 遍历完文本串都没匹配到,返回 -1
}
public int [] getNext(String needle){
int next[]= new int[needle.length()];
int j = 0;
for(int i=1;i<needle.length();i++){
while (j>0 && needle.charAt(i) != needle.charAt(j)){
j = next[j-1];
}
if(needle.charAt(i) == needle.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
总结
- 内置方法法:工程开发首选,代码极简且性能优异,依赖JDK优化实现;
- 暴力匹配法:逻辑直观,适合理解字符串匹配核心原理,但最坏时间复杂度O(n×m),大数据量下性能差;
- KMP算法:字符串匹配的最优解,时间复杂度O(n+m),通过
next数组避免重复比对,是面试核心考点; - 关键技巧:
- 处理边界条件(空模式串返回0);
- KMP算法的核心是
next数组的生成,理解“最长相等前后缀”是掌握KMP的关键; - 暴力匹配可通过“剪枝”(遍历到n-m为止)减少无效操作。