2020-08-24

KMP 字串搜尋演算法

注意事項:

-eq 不區分大小寫。如果有需要,請改用 -ceq
PS C:\> 'a'  -eq  'A'
True
PS C:\> 'a'  -ceq  'A'
False

在 $Text 裡面找 $Key ( 1個字 )

$Text = 'ABAQABAB'
$Key = 'B'

for ( $base = 0 ; $base -lt $Text.Length ; $base++ ){
    if ( $Key -eq $Text[$base] ){ break }
}

if ($base -eq $Text.Length){
    '沒找到'
}else{
    "$Key$base"
}


在 $Text 裡面找 $Key ( 2個字 or 2個字以上)

$Text = 'ABAQABAB'
$Key = 'ABAB'

$baseEnd = $Text.Length - ($Key.Length - 1)
for ( $base = 0 ; $base -lt $baseEnd ; $base++ ){

    for ($j = 0 ; $j -lt $Key.Length ; $j++){
        if ( $Key[$j]  -ne  $Text[$base + $j] ){ break }
    }

    if ($j -eq $Key.Length){ break }
}

if ($base -eq $baseEnd){
    '沒找到'
}else{
    "$Key$base"
}


把 第一層 for 改成 while

$Text = 'ABAQABAB'
$Key = 'ABAB'

$base = 0       # edit 1
$baseEnd = $Text.Length - ($Key.Length - 1)
while ($base -lt $baseEnd){   # edit 2

    for ($j = 0 ; $j -lt $Key.Length ; $j++){
        if ( $Key[$j]  -ne  $Text[$base + $j] ){ break }
    }

    if ($j -eq $Key.Length){ break }
    $base += 1       # edit 3
}

if ($base -eq $baseEnd){
    '沒找到'
}else{
    "$Key$base"
}
    

KMP 字串搜尋演算法 - 如何移動?

一開始就比對失敗:
   比對          移動後
$i  01234567    01234567
   QAQABAB    QBQABAB
    X
    ABAB         ABAB
$j   0123         0123

   $i = 1          $i = 2
   $j = 0          $j = 0

   一開始就比對失敗,此時的 $j 是 0
   看到 $j 是 0,就移動1格。

   移動1格的方法: $i++$j 不變(仍是0)

比對成功幾字之後,然後才比對失敗:

   比對
$i  01234567
   ABAABAB
   |||X
   ABAB
$j  0123


1.$j 索引3比對失敗
2.前面3個字是相同的,「相同區」長度為3,內容為ABA
3.「相同區」長度 = 比對失敗索引值($j)

  如果在索引1比對失敗,那麼「相同區」長度就是1
  如果在索引2比對失敗,那麼「相同區」長度就是2

4.在「相同區」裡面找移動之後的「次相同區」(上下要相同)
   移動前   移動後     錯誤移法(上下不同)
   ABA   ABA     ABA
   ABA     ABA    ABA

   「次相同區」長度為1,內容為A
   移動格數 = 「相同區」長度 - 「次相同區」長度
         =- 1
         = 2

   移動前         移動後
$i  01234567    01234567
   ABAABAB    ABAABAB
   |||X          |
   ABAB          ABAB
$j  0123          0123

   比對失敗位置      繼續比對位置
   $i = 3         $i = 3
   $j = 3         $j = 1

   移動2格之後,並且跳過1格不比對
   「次相同區」長度,等於「跳過不比對」的長度

   原本比對失敗的 舊$j 為3
   即將參與比對的 新$j 為1
   新$j = 「次相同區」長度
   知道「次相同區」長度,就可得知 新$j

   下次比對時,參與比對的 $i 沒變(仍和上次一樣)
   $j 變小了,藉此達成「右移」的效果

   因為 $i 沒有變、不會向後退:
   可以不使用 $base + $j 的方式來取得 $i
   不再關注「移動幾格」
   而是關注「次相同區」長度

   普通移法的 $i 會向後退: $i  移動前         移動後    01234567    01234567    ABAABAB    AAQABAB    |||X    ABAB         ABAB $j  0123         0123    $i = 3 比對失敗     從 $i = 1 繼續比對。                 $i 改變了、向後退了 5.「相同區」長度為1,「次相同區」長度一定是0    移動前   移動後    A     A    A      A    長度太短,無法去找「次相同區」        「次相同區」長度為0    移動格數 = 「相同區」長度 - 「次相同區」長度         =- 0         =6.別的例子    移動前   移動後      錯誤移法(上下不同)    ABB   ABB      ABB    ABB      ABB    ABB    「相同區」長度為3    「次相同區」長度為0    移動格數 = 「相同區」長度 - 「次相同區」長度         =- 0         = 3    移動3格之後,即將參與比對的索引值$j 為0    $j 等於 「次相同區」長度 7.別的例子    移動前    移動後      錯誤移法(上下不同)    ABAA   ABAA     ABAA    ABAA      ABAA    ABAA    「相同區」長度為4    「次相同區」長度為1    移動格數 = 「相同區」長度 - 「次相同區」長度         =- 1         = 3    移動3格之後,可以跳過1格不比對    「次相同區」長度,等於跳過不比對的長度    即將參與比對的索引值$j 為1    $j 等於 「次相同區」長度 8.別的例子    移動前    移動後      錯誤移法(上下不同)    ABAB   ABAB     ABAB    ABAB     ABAB    ABAB    「相同區」長度為4    「次相同區」長度為2    移動格數 = 「相同區」長度 - 「次相同區」長度         =- 2         = 2    移動2格之後,可以跳過2格不比對    「次相同區」長度,等於跳過不比對的長度    即將參與比對的索引值$j 為2    $j 等於 「次相同區」長度

KMP 字串搜尋演算法 - $base 「移幾格」版

$Text = 'ABAQABAB'
$Key = 'ABAB'

$f = @(0) * $Key.Length
<#
 $f 是一個陣列,它的長度和 $Key 一樣
 索引值 為 比對失敗位置(也等於相同區長度)
 $f[ 比對失敗位置 ] 得到 「次相同區」長度
 $f[ 舊$j ] 得到 新$j
 $f[0] = -1  這個值在本程式裡不會使用到
 $f[1] = 0  相同區長度是 1   次相同區長度一定是 0

 此處設定 $f陣列 其他元素內容
#>

$base = 0
$baseEnd = $Text.Length - ($Key.Length - 1)
$j = 0
while ($base -lt $baseEnd){

    for ( ; $j -lt $Key.Length ; $j++){ # 這裡沒有設定 $j 的初始值
        if ( $Key[$j]  -ne  $Text[$base + $j] ){ break }
    }

    if ($j -eq $Key.Length){ break } # 找到了
    if ($j -eq 0){$base += 1; continue} # 一開始就比對失敗
    $base += $j -  $f[$j]      # 移幾格?  「相同區」長度 - 「次相同區」長度
    $j = $f[$j]    # 設定 新$j 。 「次相同區」長度、用來跳過不比對

}

if ($base -eq $baseEnd){
    '沒找到'
}else{
    "$Key$base"
}


KMP 字串搜尋演算法 - 「$i vs $j」版

$Text = 'ABAQABAB'
$Key = 'ABAB'

$f = @(0) * $Key.Length
<#
 $f 是一個陣列,它的長度和 $Key 一樣
 $f[舊$j ] 得到 新$j
 此處設定 $f 元素內容
#>

$i = 0
$j = 0
$ans = -1
while ($i -lt $Text.Length){ # 此處和前例不同
    if ($Text[$i] -eq $Key[$j]){ # 比對成功
        $i++
        $j++

        if ($j -eq $Key.Length){ # 找到了
            $ans = $i - $j # 注意:要用減法
            break
        }
    }else{
        if ($j -ne 0){ # 有「相同區」
            $j = $f[$j] # 以「次相同區」長度 設定 新$j。
                        # $i 不變
        
        }else{# 一開始就比對失敗。$j是0、不用設定
            $i++
        }
    }
}

if ($ans -eq -1){ # 此處和前例不同
    '沒找到'
}else{
    "$Key$ans"
}


$f[ ] 的種類

第一種:
  $f[ 比對失敗位置 ] 得到 「下次比對位置」
  $f[ 相同區長度 ]   得到 「次相同區長度」

  找到 $Key 之後,此時,$j 等於 $Key.Length
  要繼續找下一個符合的 $Key,
  不可以用 $f[$j] 得到 新$j  (下次比對位置)
  因為 $j 超出索引值範圍

  $f[0] = -1

  本篇教學使用這種

  相關網站:
  Knuth-Morris-Pratt algorithm
  [TIL] 有關字串搜尋的演算法: KMP - kkdai.github.io

第二種:   $f[ 比對失敗位置 - 1 ] 得到 「下次比對位置」   $f[ 相同區的最後1個索引值 ] 得到 「次相同區長度」   找到 $Key 之後,此時,$j 等於 $Key.Length   要繼續找下一個符合的 $Key,可以用:   $f[ $j - 1 ] 得到 新$j (下次比對位置)   $f[0] = 0   相關網站:   GeeksforGeeks KMP C++ source code   字符串匹配的KMP算法 - 阮一峰的网络日志   KMP算法詳解. 詳細介紹KMP(Knuth-Morris-Pratt)字串尋找算法 | by CHEN TSU PEI | NLP-trend-and-review | Medium   [分享] KMP(Knuth–Morris–Pratt algorithm) - 看板 b99902HW - 批踢踢實業坊
第三種:   $f[ 比對失敗位置 - 1 ] 得到 次相同區的最後1個字的索引值   $f[ 相同區的最後1個字的索引值 ] 得到 次相同區的最後1個字的索引值                   如果沒有次相同區,得到 -1   找到 $Key 之後,此時,$j 等於 $Key.Length   要繼續找下一個符合的 $Key,可以用:   $f[ $j - 1 ] 得到 次相同區的最後1個字的索引值   $f[0] = -1   相關網站:   KMP 字串比對演算法 | Mr. Opengate   演算法筆記 - String Searching   資結筆記 - KMP演算法 - j2492104的創作 - 巴哈姆特   [理工] [演算法]-KMP algorithm - 看板 Grad-ProbAsk - 批踢踢實業坊

如何得知 $f[ ] 是哪一種?

1.確認 $j 的意思
    觀察「比對時的程式碼」
    例如:$Text[$i] -eq $Key[$j]
    
    確認 $j 是不是「參與比對的索引值」

2.查看 $f[ ] 的使用情形
    如果 $j 是「參與比對的索引值」
    
    第一種:  $j = $f[$j]
    第二種:  $j = $f[$j - 1]
    第三種:  $j = $f[$j - 1] + 1

在「相同區」裡找「次相同區」

$Key = 'ACAQACACQ'

各種長度 的「相同區」

長度  尋找範圍     移動後      次相同區長度 ($len)
6   ACAQAC   ACAQAC      
    ACAQAC       ACAQAC     

             確認是否可以沿用長度6的起點
7   ACAQACA  ACAQAC
    ACAQACA      ACQACA

長度越長,次相同區的尋找範圍就越長
「次相同區」的起點,不需要從「尋找範圍」的最左邊找起

「長度7」比「長度6」多1字
用多出來的那1字,即「長度7的最後1字」
去比對 $Key[$len]
確認「長度7」的次相同區,是不是「長度6」的次相同區「加長版」
它們「次相同區」起點是不是一樣的
如果它們「次相同區」起點是一樣的
「長度7的$len」等於「長度6的$len」加

---

長度  尋找範圍      移動後       次相同區長度 ($len)
7   ACAQACA   ACAQACA      
    ACAQACA       ACAQACA

              確認是否可以沿用長度7的起點
8   ACAQACAC  ACAQACA
    ACAQACAC      ACAACAC

「長度8」不可以沿用「長度7」的起點

在長度7的「次相同區」裡面移動、尋找「次次相同區」
ACA   AC
ACA     CA

把長度7的「次相同區長度」 ($len 其值為 3)
當作$f[]的索引值
得到 新$len$len = $f[$len]
新$len = $f[3] = 1


不可以沿用「長度7」的起點
ACAQACA
    ACAACAC

確認是否可以沿用「新起點」   以「長度8的最後1字」比對  $Key[$len]
ACAQAC
      AQACAC



如果新起點ok:
  「長度8」的次相同長度,等於 新$len 加 1

如果新起點不ok:
  如果新$len不為0,就再一次$len = $f[$len]
  重複上述動作

  如果新$len已經是0,就不用再 $len = $f[$len]
  「長度8」的次相同長度,就是0

設定 $f[ ]

# $f[相同區長度] 得到 「次相同區」長度
# $f[舊$j] 得到 新$j

# KMP 只有在 $Key長度大於等於 3 才會比普通找法快
if ($Key.Length -lt 3){throw '$Key 長度太小'}

#$f 是一個陣列,它的長度和 $Key 一樣
$f = @(0) * $Key.Length

$f[0] = -1
$f[1] = 0

$j = 2 # $j 是相同區長度。它大於等於2,才可以去找「次相同區」
$len = $f[1] # 把 「相同區長度1」的「次相同區長度」,存到 $len

while ($j -lt $Key.Length){# $j 是比對失敗位置,它小於 $Key.Length

    # 現在要從「相同區」裡面,找「次相同區」
    # 「相同區」裡有許多位置,可能是「次相同區」的起點
    
    # $j的「相同區」長度,比 ($j-1) 「相同區」長度多1
    # 如果 ($j-1)相同區內的某位置,不能當作「次相同區」起點
    # 那麼 $j相同區 也不能使用那個位置,來當作「次相同區」起點
    
    # 於是,我們不從$j「相同區」裡的最左邊開始找起
    # 而是從($j-1)「次相同區」的起點開始找「次相同區」

    # $f[$j-1]是 ($j-1)的「次相同區」長度,等於 $len
    # $len 是($j-1)「次相同區」長度,不是起點
    # ($j-1)的「次相同區」內容為 $Key[0] 到 $Key[$len-1]
    # $j相同區 比 ($j-1)相同區多1字
    # 用 「$j 相同區的最後1個字」 比對 $Key[$len]
    # 來確定此起點是否合適

    if ($Key[$j-1] -eq $Key[$len]){ # ($j-1)「次相同區」起點合適
        $len++
        $f[$j] = $len
        $j++

    }else{ # ($j-1)「次相同區」起點不合適

        if ($len -ne 0){
            # ($j-1)的「次相同區長度」不等於0
            # 那麼就在($j-1)「次相同區」裡面
            # 尋找「次次相同區」的「起點」
            # 把「次相同區長度」作為 $f[]的索引值
            # 取得「次次相同區」長度

            # 這裡並沒有去改變 $j 的值
            
            # 在while的新一回合
            # 會測試  $Key[$j-1] -eq $Key[次次相同區長度]
            # 確認「次次相同區」的「起點」是否合適

            $len = $f[$len]
        
        }else{ # $len -eq 0
            # 可能1:
            # ($j-1)的次相同區長度是0 
            # $j 相同區 的最後1字 不等於 $Key[0]
            #
            # 可能2:
            # ($j-1)的次相同區長度不是0
            # 但「次次相同區」or「次次次相同區」or 「次次次次相同區」是0
            # $j 相同區 的最後1字 不等於 $Key[0]
            $f[$j] = 0
            $j++
        }
    }
}

預先知道:移動後,馬上就比對失敗


   比對            移動後
$i 0123456789     0123456789
  AACAAAACAAC   AACAAAACAAC
       X              X
  AAAA            AAAA
$j 012345            012345

  比對失敗位置: $j = 5   「相同區」:AACAA   「次相同區」:AA   「次相同區」長度:2   如果「次相同區」後面的字,和比對失敗位置的字相同   即,如果 $Key[2] 等於 $Key[5]   移動後的第一次比對,馬上就比對失敗

設定 $f[ ] 改良版

if ($Key.Length -lt 3){throw '$Key 長度太小'}

#$f 是一個陣列,它的長度和 $Key 一樣
$f = @(0) * $Key.Length

$f[0] = -1
$f[1] = 0

$j = 2 
$len = $f[1]

while ($j -lt $Key.Length){
    if ($Key[$j-1] -eq $Key[$len]){
        $len++

        if ($Key[$j] -ne $Key[$len]){ # ok
            $f[$j] = $len

        }else{ # 移動後,馬上就比對失敗

                #在製表時,就先設定到最適合的次相同區長度
                #避免正式比對時,多做額外的比對
            $f[$j] = $f[$len]
        }
        $j++

    }else{
        if ($len -ne 0){
            $len = $f[$len]

        }else{ # $len -eq 0

            $f[$j] = 0
            $j++
        }
    }
}


KMP 字串搜尋演算法 - 最終版

$Text = 'ABAQABAB'
$Key = 'ABAB'

#======= 設定 $f[] ========
if ($Key.Length -lt 3){throw '$Key 長度太小'}

#$f 是一個陣列,它的長度和 $Key 一樣
$f = @(0) * $Key.Length

$f[0] = -1
$f[1] = 0

$j = 2 
$len = $f[1]

while ($j -lt $Key.Length){
    if ($Key[$j-1] -eq $Key[$len]){
        $len++

        if ($Key[$j] -ne $Key[$len]){
            $f[$j] = $len
        }else{
            $f[$j] = $f[$len]
        }
        $j++

    }else{
        if ($len -ne 0){
            $len = $f[$len]

        }else{ # $len -eq 0

            $f[$j] = 0
            $j++
        }
    }
}

#======= 開始尋找 ========
$i = 0
$j = 0
$ans = -1
while ($i -lt $Text.Length){
    if ($Text[$i] -eq $Key[$j]){
        $i++
        $j++

        if ($j -eq $Key.Length){
            $ans = $i - $j
            break
        }
    }else{
        if ($j -ne 0){
            $j = $f[$j]
        
        }else{
            $i++
        }
    }
}

if ($ans -eq -1){
    '沒找到'
}else{
    "$Key$ans"
}

沒有留言:

張貼留言