文章目录
- 题目描述
- 解题方法
- 方法一:双指针
- java代码
- 复杂度分析
- 方法二:KMP算法
- 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 仅由小写英文字符组成
解题方法
方法一:双指针
从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最长前缀和数组的空间。
- 个人公众号
- 个人小游戏