每日一道 LeetCode (9):實現 strStr()
每天 3 分鐘,走上演算法的逆襲之路。
前文合集
程式碼倉庫
GitHub: https://github.com/meteor1993/LeetCode
Gitee: https://gitee.com/inwsy/LeetCode
題目:實現 strStr()
題目來源:https://leetcode-cn.com/problems/implement-strstr/
實現 strStr() 函式。
給定一個 haystack 字串和一個 needle 字串,在 haystack 字串中找出 needle 字串出現的第一個位置 (從0開始)。如果不存在,則返回 -1。
示例 1:
輸入: haystack = "hello", needle = "ll"
輸出: 2
示例 2:
輸入: haystack = "aaaaa", needle = "bba"
輸出: -1
說明:
當 needle
是空字串時,我們應當返回什麼值呢?這是一個在面試中很好的問題。
對於本題而言,當 needle
是空字串時我們應當返回 0 。這與 C 語言的 strstr()
以及 Java的 indexOf()
定義相符。
解題思路:暴力方案
解題思路?
這道題還搞啥解題思路?
題目都直接把答案寫出來了,「這與 C 語言的 strstr()
indexOf()
定義相符」,我直接用 indexOf()
它不香麼?
public int strStr(String haystack, String needle) {
return haystack.indexOf(needle);
}
看著效率,槓槓的,我可真是個小機靈鬼。
但是如果你在面試的時候這麼答,會不會被面試打個半死我就不知道了。
那麼接下來,最符合常人的暴力思路來襲,我就喜歡幹這事兒。
借一張官方的圖:
我做一個迴圈,直接比較 needle
長度的字串,如果相等就可以直接返回了,如果比到最後沒比出來,就返回 -1 ,解題結束。
我就是這麼的直接。。。以及。。。暴力。。。
能用暴力解決的問題,絕不多動腦子。
public int strStr_1(String haystack, String needle) {
int h = haystack.length(), n = needle.length();
for (int i = 0; i < h - n + 1; i++) {
if (needle.equals(haystack.substring(i, i + n))) {
return i;
}
}
return -1;
}
好像結果也還算可以嘛,沒有那種慢到不可接受。
解題思路:暴力方案優化
做完題好習慣看看答案,然後知道了我上面的這種暴力方案是基於一個叫 「滑動視窗」 的東西,這個名字倒是蠻形象的。
上面的暴力方案有一個缺點是,會將 haystack 所有長度為 n 的子串都和 needle 做比較,那麼能不能少比較幾次呢?
當然是可以的,以下內容來源於官網:
- 第一件事兒就是隻有第一個字元相等的才有比較的意義,如果第一個字元都不相等,這也就不用比了(圖片來源於官方)。
- 接著一個字元一個字元比較,一旦不匹配了就立刻終止(圖片來源於官方)。
截止到目前,都還是很好理解的,下面這一步就稍微有點抽象了,而這個方案的精髓也是下面這一步。
- 這裡,比較到最後一位的時候發現不匹配,開始回溯。需要注意的是,pn 指標是移動到 pn = pn - curr_len + 1 的位置(圖片來源於官方)。
- 在這之後,接著 ++pn ,尋找開頭和
needle
第一位相同的子串,找到之後重複上面的比較的過程,然後找到了答案置(圖片來源於官方)。
實現程式碼如下(程式碼來自於官方):
public int strStr_2(String haystack, String needle) {
int L = needle.length(), n = haystack.length();
if (L == 0) return 0;
int pn = 0;
while (pn < n - L + 1) {
// 第一次迴圈 pn ,尋找和 needle 第一位相同的子串
while (pn < n - L + 1 && haystack.charAt(pn) != needle.charAt(0)) ++pn;
// 從 pn 開始,按位比較字元,獲得相同位數長度 currLen
int currLen = 0, pL = 0;
while (pL < L && pn < n && haystack.charAt(pn) == needle.charAt(pL)) {
++pn;
++pL;
++currLen;
}
// 如果 currLen 長度等於 needle 長度,匹配結束
if (currLen == L) return pn - L;
// 如果不等於,開始回溯
pn = pn - currLen + 1;
}
return -1;
}
這段程式碼自己做了字元的迴圈比較,但是很不幸,這種比較方案要比使用 equals()
來的要慢,我稍微修改下:
public int strStr_3(String haystack, String needle) {
int L = needle.length(), n = haystack.length();
if (L == 0) return 0;
int pn = 0;
while (pn < n - L + 1) {
// 第一次迴圈 pn ,尋找和 needle 第一位相同的子串
while (pn < n - L + 1 && haystack.charAt(pn) != needle.charAt(0)) ++pn;
// 如果 pn + L 的長度大於當前字串長度,直接返回 -1
if (pn + L > n) return -1;
// 如果 pn + L 得到的子串和 needle 相同,直接返回 pn
if (haystack.substring(pn, pn + L).equals(needle)) {
return pn;
}
// 沒匹配到 ++pn
++pn;
}
return -1;
}
思路還是同樣的思路,但是我在字串的比較換成了 equals()
,耗時重回 1ms 。
拋磚引玉
到這裡,我們往回看一個問題,為啥 jdk 提供的 indexOf()
這個方法,可以把耗時壓縮到 0ms ?為何 indexOf()
這個方法如此 NB ?
點開原始碼,找到核心方法(jdk 版本: 1.8.0_221):
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
// 1、當開始查詢位置 大於等於 源字串長度時,如果[查詢字串]為空,則:
// 返回字串的長度,否則返回-1.
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
// 2、如果 fromIndex 小於 0 ,則從 0 開始查詢。
if (fromIndex < 0) {
fromIndex = 0;
}
// 3、如果[查詢字串]為空,則返回 fromIndex
if (targetCount == 0) {
return fromIndex;
}
// 4、開始查詢,從[查詢字串]中得到第一個字元,標記為 first
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
// 4.1、計算[源字串最大長度]
for (int i = sourceOffset + fromIndex; i <= max; i++) {
// 4.2.1、從[源字串]中,查詢到第一個匹配到[目標字串] first 的位置
// for迴圈中,增加 while 迴圈
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
// 4.2.2、如果在[源字串]中,找到首個[目標字串],
// 則匹配是否等於[目標字串]
/* Found first character, now look at the rest of v2 */
if (i <= max) {
// 4.2.2.1、得到下一個要匹配的位置,標記為 j
int j = i + 1;
// 4.2.2.2、得到其餘[目標字串]的長度,標記為 end
int end = j + targetCount - 1;
// 4.2.2.3、遍歷,其餘[目標字串],從 k 開始,
// 如果 j 不越界(小於 end ,表示:其餘[目標字串]的範圍),
// 同時[源字串]==[目標字串],則
// 自增,繼續查詢匹配。 j++ 、 k++
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
// 4.2.2.4、如果 j 與 end 相等,則表示:
// 源字串中匹配到目標字串,匹配結束,返回 i 。
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
這段程式碼看起來平平無奇,而且查詢的方式和我們上面的優化方案非常像,都是先查詢首個匹配字元,然後再做迴圈查詢整個匹配的字串。
單純的靠程式碼優化把耗時從 2ms 縮減到了 0ms ,只能是一個大寫的佩服,不愧是寫 jdk 原始碼的大神。