[LeetCode] #28 Implement strStr()

#28 Implement strStr()

Question

Implement strStr().

Returns the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Thinking

假設今天我們要比對兩組字串,分別是主字串(haystack)、子字串(needle)

當中最簡單便是使用暴力字串比對法(Naive String-Matching Algorithm),即主字串每個字元都作為子字串的起頭,再利用迴圈一一巡覽完為止,其概念如下圖

若主字串長度為n、子字串長度為m,則此方法的時間複雜度為O((n-m+1)*m),實際Code則如下

public class Solution {
    public int StrStr(string haystack, string needle) {
        
        if (needle.Length == 0) return 0;
        if (haystack.Length < needle.Length) return -1;
        
        var currentHaystackIndex = 0;
        var currentNeedleIndex = 0;
        while(currentHaystackIndex < haystack.Length 
             && currentNeedleIndex < needle.Length)
        {
            if (haystack[currentHaystackIndex] == needle[currentNeedleIndex])
            {
                currentHaystackIndex++;
                currentNeedleIndex++;
            }
            else
            {
                currentHaystackIndex = 
                    currentHaystackIndex - currentNeedleIndex + 1;
                currentNeedleIndex = 0;
            }
        }
        return (currentNeedleIndex == needle.Length) 
               ? currentHaystackIndex - needle.Length 
               : -1;
    }
}

但許多人認為這樣的比對效率是非常低的,為什麼呢?

  1. 首先,子字串「ABCABE」中的needle[0]、needle[1]、needle[2]均不相等,因此在概念圖的步驟(1)比對過後,很明顯發現(2)(3)是可以省略的
  2. 再仔細看,needle[0]與needle[3]是相等的(needle[1]與needle[4]亦是),在步驟(1)中,needle[3]已經與主字串的對應位置haystack[3]比對過且是相等的,那就代表與needle[3]相等的needle[0]下一回合也不需要再與haystack[3]比對了,因此步驟(4)第一組字元比對也是可以省略的

最後我們得到下圖

在暴力字串比對法中,haystackIndex是會不斷來回變動的,而從上圖簡化後發現這些變動其實是不必要的,因此科學家們把腦筋動到needleIndex上,這種只專注於needleIndex變動的方法便是Knuth-Morris-Pratt演算法(KMP Algorithm)

當發現主字串與子字串字元比對不相等時,在haystackIndex不變動的情況下,我們希望能將needleIndex退回可能的位置,而不是直接從頭(needle[0])開始比對。那該怎麼做呢?首先,我們需要建立一個陣列next,該陣列是用來儲存needleIndex的變化,其推導如下

needleIndex 0 1 2 3 4 5
needle A B C A B E
next[needleIndex] -1 0 0 0 1 2
  1. next[0],沒有前後綴能比較,為例外情況,我們定義為-1
  2. next[1],前綴為「A」,但它沒有後綴,因此為0
  3. next[2]、next[3],同上皆沒有後綴,因此為0
  4. next[4],比較第一個字元與最後一個字元,可找到前綴「A」與後綴「A」,一個字元相等,因此為1
  5. next[5],可找到最大前綴「AB」與後綴「AB」,兩個字元相等,因此為2

可以發現,next陣列值的計算方式是找出相等的最大前綴與後綴詞,找出來的字元數代表著進行下一次比對時與needle[0]的初始距離,這距離其實就是needleIndex應退回到的位置(ex:距離為1時,代表從needle[1]開始)。若遇到-1的例外情況,那代表needleIndex已經退無可退,是時候將haystackIndex再往下推移了

若主字串長度為n、子字串長度為m,則此方法的時間複雜度為O(n+m),實際Code如下

public class Solution {
    public int StrStr(string haystack, string needle) {
        
        if (needle.Length == 0) return 0;
        if (haystack.Length < needle.Length) return -1;
        
        var currentHaystackIndex = 0;
        var currentNeedleIndex = 0;
        var next = GetNext(needle);
        while(currentHaystackIndex < haystack.Length 
             && currentNeedleIndex < needle.Length)
        {
            if (currentNeedleIndex == -1 || 
               haystack[currentHaystackIndex] == needle[currentNeedleIndex])
            {
                currentHaystackIndex++;
                currentNeedleIndex++;
            }
            else
            {
                currentNeedleIndex = next[currentNeedleIndex];
            }
        }
        return (currentNeedleIndex == needle.Length) 
               ? currentHaystackIndex - needle.Length 
               : -1;
    }
    
    private int[] GetNext(string needle)
    {
        var index = 0;
        var indexValue = -1;
        var next = new int[needle.Length];
        next[index] = indexValue;
        while(index < needle.Length - 1)
        {
            if (indexValue == -1 || needle[index] == needle[indexValue])
            {
                index++;
                indexValue++;
                next[index] = indexValue;
            }
            else
            {
                indexValue = next[indexValue];
            }
        }
        return next;
    }
}

不過有人發現KMP演算法仍有改良的空間。如前圖步驟(1)中,我們得知haystack[4]與needle[4]不相等,也知道子字串「ABCABE」存在前後綴「AB」,依據這些條件,步驟(2)其實可以再更進一步簡化成下圖

我們需要建立一個新的next陣列,推導如下

needleIndex 0 1 2 3 4 5
needle A B C A B E
nextOld[needleIndex] -1 0 0 0 1 2
nextNew[needleIndex] -1 0 0 -1 0 2
  1. 先計算出nextOld
  2. nextNew[0],為例外情況,我們一樣定義為-1
  3. nextNew[1],因為nextOld[1]值為0,所以比較needle[0]的字元「A」與needle[1]的字元「B」,不相等則照舊,因此為0
  4. nextNew[2],因為nextOld[2]值為0,所以比較needle[0]的字元「A」與needle[2]的字元「C」,不相等則照舊,因此為0
  5. nextNew[3],因為nextOld[3]值為0,所以比較needle[0]的字元「A」與needle[3]的字元「A」,相等則被nextNew[0]的值取代,因此為-1
  6. nextNew[4],因為nextOld[4]值為1,所以比較needle[1]的字元「B」與needle[4]的字元「B」,相等則被nextNew[1]的值取代,因此為0
  7. nextNew[5],因為nextOld[5]值為2,所以比較needle[2]的字元「C」與needle[5]的字元「E」,不相等則照舊,因此為2

概念其實很簡單,若nextOld[a]的值b所指向的字元(needle[b])與needle[a]的字元相同,則nextNew[a]的值應該被nextNew[b]的值取代;反之nextNew[a]的值就是nextOld[a]的值。這樣的作法可以更精準定位到needleIndex應退回的位置,進而再減少運算次數,有了這概念,最後的解法如下

My C# Solution

public class Solution {
    public int StrStr(string haystack, string needle) {
        
        if (needle.Length == 0) return 0;
        if (haystack.Length < needle.Length) return -1;
        
        var currentHaystackIndex = 0;
        var currentNeedleIndex = 0;
        var next = GetNext(needle);
        while(currentHaystackIndex < haystack.Length 
             && currentNeedleIndex < needle.Length)
        {
            if (currentNeedleIndex == -1 ||
               haystack[currentHaystackIndex] == needle[currentNeedleIndex])
            {
                currentHaystackIndex++;
                currentNeedleIndex++;
            }
            else
            {
                currentNeedleIndex = next[currentNeedleIndex];
            }
        }
        return (currentNeedleIndex == needle.Length) 
               ? currentHaystackIndex - needle.Length 
               : -1;
    }
    
    private int[] GetNext(string needle)
    {
        var index = 0;
        var indexValue = -1;
        var next = new int[needle.Length];
        next[index] = indexValue;
        while(index < needle.Length - 1)
        {
            if (indexValue == -1 
               || needle[index] == needle[indexValue])
            {
                index++;
                indexValue++;
                if (needle[index] != needle[indexValue])
                {
                    next[index] = indexValue;
                }
                else
                {
                    next[index] = next[indexValue];                
                }
            }
            else
            {
                indexValue = next[indexValue];
            }
        }
        return next;
    }
}

Reference:《大話資料結構》Chapter 05 字串