找出字符串中第一个匹配项的下标:Java解法详解
题目
给你两个字符串 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 <= 104
haystack
和needle
仅由小写英文字符组成
详解
一、暴力手撕法(双指针法)
思路
采用双指针法进行匹配:
- 初始化指针p(主串)和q(子串),从头开始逐个字符比对
- 若当前字符不匹配,则p回溯到主串下一位置,q重置到子串起始位置
- 重复上述过程,直至q遍历完整个子串
- 此时p-q的差值即为子串首次出现的位置索引
完整代码
class Solution {
public int strStr(String haystack, String needle) {
if (needle.isEmpty()) {
return 0;
}
int p = 0; // 索引 p 用于 haystack
int q = 0; // 索引 q 用于 needle
while (p < haystack.length() && q < needle.length()) {
if (haystack.charAt(p) == needle.charAt(q)) {
p++;
q++;
} else {
p = p - q + 1; // 重置 p 和 q
q = 0;
}
// 如果 needle 全部匹配,返回起始位置
if (q == needle.length()) {
return p - q;
}
}
return -1; // 未找到匹配
}
}
缺点
重复匹配失败会导致从头开始重新尝试,不仅效率低下且浪费资源。
二、KMP算法(最通俗易懂版)
当 haystack = "abcabefg" 且 needle = "abefg" 时,采用双指针匹配方法。在匹配过程中,当遇到主串中的字符'c'与模式串不匹配时,算法会触发回溯机制。
a | b | c | a | b | e | f | g |
a | b | e | f | g |
匹配失败后重新从起始位置开始尝试:
haystack | a | b | c | a | b | e | f | g |
needle | a | b | e | f | g |
为什么不直接从匹配失效后面再接着重新找呢?这样不就快一些吗?
haystack | a | b | c | a | b | e | f | g |
needle | a | b | e | f | g |
原因:直接从匹配失效的后面接着找可能会导致遗漏某些潜在的匹配位置。因为模式串内部可能存在重复的子串,回溯到部分匹配的位置可以确保我们不会错过这些子串可能带来的匹配机会。
好,接下来再看一个例子
haystack = "abababcaa" 且 needle = "ababc"
haystack | a | b | a | b | a | b | c | a | a |
needle | a | b | a | b | c |
当匹配最后一个元素不成功时,若直接从匹配失效后再接着找那就出错了,因为实质上要从一下地方再进行匹配
haystack | a | b | a | b | a | b | c | a | a |
needle | a | b | a | b | c |
如何确定匹配的起始位置?当匹配失败时,如何精准定位需要回溯的位置?
当两个字符串在匹配过程中出现错误时,若它们之前存在相同的前缀(如"ab"),我们可以通过定义一个数组来记录每个位置匹配失败时,其前缀是否与其他元素相匹配。
代码解析
private int[] computeLPSArray(String needle) {
int length = 0; // 当前相同前缀后缀的长度
int i = 1; // 从第二个字符开始计算 LPS
int[] lps = new int[needle.length()]; // 初始化 LPS 数组
lps[0] = 0; // 第一个字符的 LPS 值为 0
while (i < needle.length()) { // 遍历 needle
if (needle.charAt(i) == needle.charAt(length)) { // 如果字符匹配
length++; // 增加相同前缀后缀的长度
lps[i] = length; // 记录 LPS 值
i++; // 移动 i
} else { // 如果字符不匹配
if (length != 0) {
length = lps[length - 1]; // 回退 length
} else {
lps[i] = 0; // 如果 length 为 0,记录 LPS 值
i++; // 移动 i
}
}
}
return lps; // 返回 LPS 数组
}
这个方法就可以得到一个数组,数组元素表示当前元素存在的最大前缀
例如,对于模式串 needle = "ababc"
,部分匹配表如下:
字符 | a | b | a | b | c |
---|---|---|---|---|---|
前缀函数值 | 0 | 0 | 1 | 2 | 0 |
- 对于第一个字符
a
,没有前缀和后缀,所以值为0。 - 对于第二个字符
b
,没有相同前后缀,所以值为0。 - 对于第三个字符
a
,与第一个字符相同,所以值为1。 - 对于第四个字符
b
,与第二个字符相同,所以值为2。 - 对于第五个字符
c
,没有相同前后缀,所以值为0。
-
匹配过程:
在匹配过程中,当发生不匹配时,我们根据部分匹配表中的值来决定回溯的位置。例如,对于
haystack = "abababcaa"
和needle = "ababc"
:haystack a b a b a b c a a needle a b a b c - 当匹配到第四个字符
b
时,发生不匹配。 - 根据部分匹配表,我们知道在位置3(从0开始计数)的值为2,这意味着在模式串的前缀
abab
中,最长相同前后缀的长度为2。 - 所以,我们可以将模式串回溯到位置2(即第二个
a
的位置)重新开始匹配。
- 当匹配到第四个字符
-
回溯位置的确定:
- 当匹配失败时,我们查看部分匹配表中当前位置的值,这个值就是我们需要回溯到的位置。
- 在上述例子中,匹配失败时,我们根据部分匹配表的值2,将模式串回溯到第二个
a
的位置。
借助该数组,我们能够快速确定匹配失效时需要回溯的位置。完整代码及具体解析如下:
class Solution {
public int strStr(String haystack, String needle) {
if (needle.isEmpty()) {
return 0; // 如果 needle 为空,直接返回 0
}
int[] lps = computeLPSArray(needle); // 计算 needle 的 LPS 数组
int i = 0; // 索引 i 用于遍历 haystack
int j = 0; // 索引 j 用于遍历 needle
while (i < haystack.length()) { // 遍历 haystack
if (haystack.charAt(i) == needle.charAt(j)) { // 如果字符匹配
i++; // 移动 i
j++; // 移动 j
}
if (j == needle.length()) { // 如果 needle 全部匹配
return i - j; // 返回匹配的起始位置
} else if (i < haystack.length() && haystack.charAt(i) != needle.charAt(j)) { // 如果字符不匹配
if (j != 0) {
j = lps[j - 1]; // 根据 LPS 数组回退 j
} else {
i++; // 如果 j 为 0,直接移动 i
}
}
}
return -1; // 未找到匹配
}
private int[] computeLPSArray(String needle) {
int length = 0; // 当前相同前缀后缀的长度
int i = 1; // 从第二个字符开始计算 LPS
int[] lps = new int[needle.length()]; // 初始化 LPS 数组
lps[0] = 0; // 第一个字符的 LPS 值为 0
while (i < needle.length()) { // 遍历 needle
if (needle.charAt(i) == needle.charAt(length)) { // 如果字符匹配
length++; // 增加相同前缀后缀的长度
lps[i] = length; // 记录 LPS 值
i++; // 移动 i
} else { // 如果字符不匹配
if (length != 0) {
length = lps[length - 1]; // 回退 length
} else {
lps[i] = 0; // 如果 length 为 0,记录 LPS 值
i++; // 移动 i
}
}
}
return lps; // 返回 LPS 数组
}
}
既然你看到这里了,那么恭喜你,KMP算法你已经会写了!