1. 程式人生 > 實用技巧 >每日一道 LeetCode (9):實現 strStr()

每日一道 LeetCode (9):實現 strStr()

每天 3 分鐘,走上演算法的逆襲之路。

前文合集

每日一道 LeetCode 前文合集

程式碼倉庫

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()

 以及 Java的 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 做比較,那麼能不能少比較幾次呢?

當然是可以的,以下內容來源於官網:

  1. 第一件事兒就是隻有第一個字元相等的才有比較的意義,如果第一個字元都不相等,這也就不用比了(圖片來源於官方)。

  1. 接著一個字元一個字元比較,一旦不匹配了就立刻終止(圖片來源於官方)。

截止到目前,都還是很好理解的,下面這一步就稍微有點抽象了,而這個方案的精髓也是下面這一步。

  1. 這裡,比較到最後一位的時候發現不匹配,開始回溯。需要注意的是,pn 指標是移動到 pn = pn - curr_len + 1 的位置(圖片來源於官方)。

  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 原始碼的大神。