题目描述
给你两个字符串 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 仅由小写英文字符组成
解题方法
方法一:双指针
从haystack
的起始位置开始与needle
的起始位置匹配,一旦发现不匹配的字符,则haystack
从上次遍历的起始位置往后移动一格,needle
重新回到起始位置进行下一次匹配。若haystack
遍历到末尾之前匹配成功,则返回haystack
匹配成功的起始下标;否则,返回-1
。
java代码
public int strStr(String haystack, String needle) {
for (int i = 0; i <= haystack.length() - needle.length(); i++) {
for (int j = 0; j < needle.length(); j++) {
if (haystack.charAt(i + j) != needle.charAt(j)) {
break;
}
if (j == needle.length() - 1) {
return i;
}
}
}
return -1;
}
复杂度分析
时间复杂度:设haystack
长度为n
,needle
长度为m
,最坏的情况下haystack
遍历的次数为n - m
,每次遍历needle
的匹配长度为m,则渐进时间复杂度
O
(
m
×
(
n
−
m
)
)
O(m \times (n-m))
O(m×(n−m))
空间复杂度:
O
(
1
)
O(1)
O(1),除了双指针不需要存储其他变量。
方法二:KMP算法
我们从方法一可以看到,每次我们进行字符串匹配时,如果haystack
与needle
不匹配,则haystack
从上一次遍历的起始位置往后移动一格,再与needle
从头开始匹配。假设haystack
上一次遍历从起始位置开始与needle
的前k
个字符匹配,那么有没有一种方法使我们不需要让haystack
回到上一次起始位置的下一格与needle
从头匹配,而是继续在起始位置后面的第k
个坐标与needle
进行后续的匹配呢?答案是可以,需要我们使用KMP算法。
KMP算法的核心就是最长前缀和,那么什么是最长前缀和呢?
假设有一个字符串 a a b a a a b a aabaaaba aabaaaba,我们设 n e x t next next数组为每个位置的最长前缀和,我们需要求 n e x t [ i ] next[i] next[i]。 n e x t [ i ] next[i] next[i]代表的含义是匹配的字符串以 i i i位置为终点时(不包括终点 i i i),能与原字符串前缀匹配的最长前缀和,匹配的字符串不能与原字符串前缀是同一个字符串。
- 当 i = 0 i=0 i=0时,此时 i i i前面没有字符,故 n e x t [ 0 ] = 0 next[0]=0 next[0]=0。
- 当 i = 1 i=1 i=1时,此时 i i i前面的字符为 a a a,与字符串起始位置的 a a a在同一个位置,由于匹配的字符串与原字符串前缀是同一个字符串,此时也记 n e x t [ 1 ] = 0 next[1]=0 next[1]=0。
- 当 i = 2 i=2 i=2时,此时 i i i前面的字符为 a a a,后缀 a a a与原字符串前缀 a a a匹配,故 n e x t [ 2 ] = 1 next[2]=1 next[2]=1。
- 当 i = 3 i=3 i=3时,此时 i i i前面的字符为 b b b,由于匹配的字符串结尾为 b b b,无法与原字符串前缀 a a a或者 a a aa aa匹配,所以 n e x t [ 3 ] = 0 next[3] = 0 next[3]=0。
- 当 i = 4 i=4 i=4时, i i i前面的字符为 a a a,此时后缀 a a a只能匹配原字符串前缀 a a a,故 n e x t [ 4 ] = 1 next[4]=1 next[4]=1。
- 当 i = 5 i=5 i=5时, i i i前面的字符为 a a a,此时后缀 a a aa aa可以匹配原字符串前缀 a a aa aa,故 n e x t [ 5 ] = 2 next[5]=2 next[5]=2。
- 当 i = 6 i=6 i=6时, i i i前面的字符为 a a a,此时后缀 a a aa aa可以匹配原字符串前缀 a a aa aa,故 n e x t [ 6 ] = 2 next[6]=2 next[6]=2。
- 当 i = 7 i=7 i=7时, i i i前面的字符为 b b b,此时后缀 a a b aab aab可以匹配原字符串前缀 a a b aab aab,故 n e x t [ 7 ] = 3 next[7]=3 next[7]=3。
此时即求出了 n e x t next next数组每个位置的最长前缀和。
求出
n
e
x
t
next
next数组有什么用呢?那我们再举一个haystack
和 needle
字符串匹配的例子。
设needle
字符串还是
a
a
b
a
a
a
b
a
\color{red}aabaaaba
aabaaaba,haystack
字符串为
a
a
b
a
a
a
b
b
\color{red}aabaaabb
aabaaabb
a
a
b
a
a
a
b
a
\color{blue}aabaaaba
aabaaaba。当haystack
从起始位置匹配到字符串
a
a
b
a
a
a
b
b
\color{red}aabaaabb
aabaaabb时,此时haystack
匹配字符串末尾的
b
b
b与needle
末尾的
a
a
a不匹配。一般情况下,我们就将haystack
移动到起始位置的第二个字符,与needle
从头开始匹配了。但是有了next
数组之后,不匹配的位置在needle
下标
i
=
7
i=7
i=7,我们检查到
n
e
x
t
[
7
]
=
3
next[7] = 3
next[7]=3,也就是说haystack
匹配的字符串
a
a
b
a
a
a
b
b
\color{red}aabaaabb
aabaaabb中可以再从后缀
a
a
b
b
\color{red}aabb
aabb开始与needle
中
i
=
3
i=3
i=3位置的字符开始匹配,此时i=3
位置的字符为
a
a
a与后缀字符
b
b
b不匹配,next[3]=0
,此时没有后缀与needle
前缀匹配了,此时haystack
再从
a
a
b
b
aabb
aabb最后一个后缀
b
b
b开始,与needle
从头进行匹配。可以看出在匹配的过程中,只要haystack
的匹配位置移动到了第k
个字符,则haystack
就不需要再回到第k
个字符之前从头遍历,只需要移动needle
的匹配位置比较haystack
的第k
个字符,这样大大减少了匹配时间。
java代码
public int strStr(String haystack, String needle) {
if (haystack == null || needle == null ||
haystack.length() < needle.length()) {
return -1;
}
if (needle.length() == 0) {
return 0;
}
char[] str1 = haystack.toCharArray();
char[] str2 = needle.toCharArray();
int[] next = getNextArr(str2);
int i1 = 0;
int i2 = 0;
while (i1 < str1.length && i2 < str2.length) {
if (str1[i1] == str2[i2]) {
i1++;
i2++;
} else if (i2 > 0) {
// haystack与needle在needle第i2位置的字符不匹配之时,先让i2回到next[i2]
// 此时needle从0 ~ i2-1的前缀与haystack从i1-i2 ~ i-1的字符串匹配
i2 = next[i2];
} else {
i1++;
}
}
return i2 == str2.length ? i1 - i2 : -1;
}
// 使用kmp算法计算next最长前缀和数组
public int[] getNextArr(char[] str) {
if (str.length == 1) {
return new int[]{0};
}
// next[i]代表以i位置为终点时(不包括i),最长后缀与最长前缀匹配的长度。后缀的起点位置不能从下标0开始。
int[] next = new int[str.length];
// next下标0和1之前都没有后缀与前缀匹配
next[0] = 0;
next[1] = 0;
int i = 2;
// 最长前缀和计数
int cnt = 0;
while (i < next.length) {
if (str[i - 1] == str[cnt]) {
next[i++] = ++cnt;
} else if (cnt > 0) {
// 当前后缀最后的字符不匹配之时,先让后缀的起始位置移动到更靠后的位置,与next[cnt]处的字符进行比较
//(此时str的后缀与cnt之前的字符串匹配)
cnt = next[cnt];
} else {
next[i++] = 0;
}
}
return next;
}
复杂度分析
时间复杂度:设haystack
长度为n
,needle
长度为m
,haystack
只会遍历一次,needle
也会遍历一次,时间复杂度
O
(
m
+
n
)
O(m + n)
O(m+n)
空间复杂度:
O
(
m
)
O(m)
O(m),需要留出
n
e
x
t
next
next最长前缀和数组的空间。
- 个人公众号
- 个人小游戏