leetcode第28題實現strStr()

概述

暑假期間,腆着臉皮面了字節跳動的暑期夏令營活動,被虐很慘,發現好多基礎的算法題目都已經忘記了,而基本上筆試和麪試考察的都是算法、操作系統等計算機基礎知識。因此在後續的學習過程中,需要調整重點,將自己的精力放到刷題上面來,畢竟下半學期如果要出去實習算法和操作系統等基礎知識是不可或缺的。

閒話少敘,下邊來看下leetcode的第28題,具體題目如下:

實現 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() 定義相符。

分析

該問題一看就是一個傳統字符串匹配問題,最容易想到的方法,可能就是一個雙層的for循環。讓目標字符串不斷和源字符串進行比較,尋找到能夠匹配目標字符串的位置。

雙層for循環

因此我們可以實現出下邊的代碼:

public static int subStr(String haystack, String needle) {
    if (haystack == "" || haystack == null) {
      return -1;
    }
    int i = 0, j = 0;
    // 轉化成數組進行處理
    char hayStringArry[] = haystack.toCharArray();
    char needleArry[] = needle.toCharArray();
    for (i = 0; i <= hayStringArry.length - needleArry.length; i++) {
      for (j = 0; j < needleArry.length; j++) {
        if (hayStringArry[i + j] != needleArry[j]) {
          break;
        }
      }
      if (j == needle.length()) {
        return i;
      }
    }
    return -1;
}

上邊的代碼很明顯時間複雜度是O(m*n),其中m指的是源字符串haystack的長度,n表示的是目標字符串needle的長度。

很明顯這個算法不是最優解,我們考慮如何對其進行優化。首先我們思考,該算法時間複雜度比較高的原因主要是這兩層for循環,因此我們考慮能否通過一次for循環就得出結果那?

此時我們想到了《數據結構》這門課中講的一個算法--簡單的模式匹配算法。

簡單的模式匹配算法

該算法主要思想如下:

從源字符串的第一個字符串開始逐個與目標字符串(待匹配的字符串)進行比較,如果相等,則繼續逐個向後比較字符,直到目標字符串依次和源字符串比較完成,則稱爲匹配成功;如果比較過程中有某對字符不相等,則從源字符串的下一個字符起重新和目標字符串的第一個字符進行比較。如果源字符串在比較完成後仍然沒有匹配成功,則稱爲匹配失敗

進而我們可以設計出字符的匹配算法如下:

public int strStr(String haystack, String needle) {
        if (haystack == null) {
        return -1;
        }
        char hayStackArry[] = haystack.toCharArray();
        char needleArry[] = needle.toCharArray();
        // 記錄長度,減少length()函數的調用次數
        int hayStackLength = haystack.length();
        int needleLength = needle.length();
        int i = 0, j = 0;
        // i,j分別指向源字符串和目標字符
        while (i < hayStackLength && j < needleLength) {
        if (hayStackArry[i] == needleArry[j]) {
            i++;
            j++;
        } else {
            // 回退到下次進行匹配的字符串位置
            i = i - j + 1;
            // 從目標字符串的第一個位置開始進行匹配
            j = 0;
        }
        }
        if (j > needleLength - 1) {
        return i - needleLength;
        } else {
        return -1;
        }
}

我們分析該算法的時間複雜度,我們發現,雖然我們使用了單層循環,但我們發現由於j不斷的回退,從而在壞情況下該算法的時間複雜度也可能達到O(m*n)因此,我們考慮能否減少j的回退次數。此時我們考慮引入KMP算法。

KMP算法

KMP算法基本思想是這樣的:較之於簡單模式匹配算法其改進在於,每當一次匹配過程中出現的字符不相等的時候,不需要回溯j指針,而是通過已經得到的“部分匹配”的結果將模式向右“滑動”儘可能遠的距離,繼續進行比較。

因此,KMP算法分成了兩部分,第一部分是next數組的求解,第二部分是字符串匹配。

字符串匹配

KMP算法的核心之一在於next數組,但在瞭解next數組的意義之前,我們首先要了解一個叫做部分匹配表(Partial Match Table)表的東西。

對於字符串"abababca",其PMT如下表所示

index 0 1 2 3 4 5 6 7
char a b a b a b c a
value 0 0 1 2 3 4 0 1

我先解釋一下字符串的前綴和後綴。如果字符串A和B,存在A=BS,其中S是任意的非空字符串,那就稱B爲A的前綴。例如,”Harry”的前綴包括{”H”, ”Ha”, ”Har”, ”Harr”},我們把所有前綴組成的集合,稱爲字符串的前綴集合。同樣可以定義後綴A=SB, 其中S是任意的非空字符串,那就稱B爲A的後綴,例如,”Potter”的後綴包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然後把所有後綴組成的集合,稱爲字符串的後綴集合。要注意的是,字符串本身並不是自己的後綴。

有了這個定義,就可以說明PMT中的值的意義了。PMT中的值是字符串的前綴集合與後綴集合的交集中最長元素的長度。例如,對於”aba”,它的前綴集合爲{”a”, ”ab”},後綴 集合爲{”ba”, ”a”}。兩個集合的交集爲{”a”},那麼長度最長的元素就是字符串”a”了,長 度爲1,所以對於”aba”而言,它在PMT表中對應的值就是1。再比如,對於字符串”ababa”,它的前綴集合爲{”a”, ”ab”, ”aba”, ”abab”},它的後綴集合爲{”baba”, ”aba”, ”ba”, ”a”}, 兩個集合的交集爲{”a”, ”aba”},其中最長的元素爲”aba”,長度爲3。 好了,解釋清楚這個表是什麼之後,我們再來看如何使用這個表來加速字符串的查找,以及這樣用的道理是什麼。如圖 1.12 所示,要在主字符串"ababababca"中查找模式字符串"abababca"。如果在 j 處字符不匹配,那麼由於前邊所說的模式字符串 PMT 的性質,主字符串中 i 指針之前的 PMT[j −1] 位就一定與模式字符串的第 0 位至第 PMT[j−1] 位是相同的。這是因爲主字符串在 i 位失配,也就意味着主字符串從 i−j 到 i 這一段是與模式字符串的 0 到 j 這一段是完全相同的。而我們上面也解釋了,模式字符串從 0 到 j−1 ,在這個例子中就是”ababab”,其前綴集合與後綴集合的交集的最長元素爲”abab”, 長度爲4。所以就可以斷言,主字符串中i指針之前的 4 位一定與模式字符串的第0位至第 4 位是相同的,即長度爲 4 的後綴與前綴相同。這樣一來,我們就可以將這些字符段的比較省略掉。具體的做法是,保持i指針不動,然後將j指針指向模式字符串的PMT[j −1]位即可。

簡言之,以圖中的例子來說,在 i 處失配,那麼主字符串和模式字符串的前邊6位就是相同的。又因爲模式字符串的前6位,它的前4位前綴和後4位後綴是相同的,所以我們推知主字符串i之前的4位和模式字符串開頭的4位是相同的。就是圖中的灰色部分。那這部分就不用再比較了。

img

有了上面的思路,我們就可以使用PMT加速字符串的查找了。我們看到如果是在 j 位 失配,那麼影響 j 指針回溯的位置的其實是第 j −1 位的 PMT 值,所以爲了編程的方便, 我們不直接使用PMT數組,而是將PMT數組向後偏移一位。我們把新得到的這個數組稱爲next數組。下面給出根據next數組進行字符串匹配加速的字符串匹配程序。其中要注意的一個技巧是,在把PMT進行向右偏移時,第0位的值,我們將其設成了-1,這只是爲了編程的方便,並沒有其他的意義。在本節的例子中,next數組如下表所示。

img

因此KMP算法的匹配代碼如下:

public static int kmp(String haystack, String needle) {
    char[] needleArry = needle.toCharArray();
    char[] haystackArry = haystack.toCharArray();
    int needleLength = needle.length();
    int haystackLength = haystack.length();
    int[] next = getNext(needleArry);
    int i = 0, j = 0;
    while (i < haystackLength && j < needleLength) {
      if (j == -1 || haystackArry[i] == needleArry[j]) {
        i++;
        j++;
      } else {
        j = next[j];
      }
    }
    if (j > needleLength - 1) {
      return i - needleLength;
    } else {
      return -1;
    }
  }

next數組求解

前邊我們講了kmp算法的匹配過程,下邊我們主要講一下next數組的求解過程。其實簡單來說next數組的求解過程完全可能一個字符串的匹配過程,即以模式字符串爲主字符串,以模式字符串的前綴爲目標字符串,一旦字符串匹配成功,那麼當前的next值就是匹配 成功的字符串的長度。

其匹配過程的具體代碼如下:

  // 生成next數組
  private static int[] getNext(char[] needleArry) {
    int next[] = new int[needleArry.length + 1];
    next[0] = -1;
    int i = 0, j = -1;
    while (i < needleArry.length) {
      if (j == -1 || needleArry[i] == needleArry[j]) {
        i++;
        j++;
        next[i] = j;
      } else {
        j = next[j];
      }
    }
    return next;
  }

雖然說KMP算法已經十分好了,但實際在程序設計中很少直接kmp算法來直接求解,更多的會使用BM算法以及Sunday算法。因爲這兩個算法較之於KMP算法,它會更快。

Sunday算法

Sunday算法某種程度算法BM算法的改良,而且效果更好,因此主要考慮使用Sunday算法解決一下該問題。由於篇幅原因本片文章就不再詳解,感興趣的話可以看筆者的另一篇文章--《使用sunday算法解決字符串匹配問題》

參考

  1. https://www.zhihu.com/question/21923021

  2. https://www.jianshu.com/p/2e6eb7386cd3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章