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

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

前文合集

每日一道 LeetCode 前文合集

代碼倉庫

GitHub: //github.com/meteor1993/LeetCode

Gitee: //gitee.com/inwsy/LeetCode

題目:實現 strStr()

題目來源://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 源碼的大神。

Tags: