注意事項:
-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
QBAQABAB QBAQABAB
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
ABAQABAB
|||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
移動格數 = 「相同區」長度 - 「次相同區」長度
= 3 - 1
= 2
移動前 移動後
$i 01234567 01234567
ABAQABAB ABAQABAB
|||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
ABAQABAB ABAQABAB
|||X
ABAB ABAB
$j 0123 0123
$i = 3 比對失敗 從 $i = 1 繼續比對。
$i 改變了、向後退了
5.「相同區」長度為1,「次相同區」長度一定是0
移動前 移動後
A A
A A
長度太短,無法去找「次相同區」
「次相同區」長度為0
移動格數 = 「相同區」長度 - 「次相同區」長度
= 1 - 0
= 1
6.別的例子
移動前 移動後 錯誤移法(上下不同)
ABB ABB ABB
ABB ABB ABB
「相同區」長度為3
「次相同區」長度為0
移動格數 = 「相同區」長度 - 「次相同區」長度
= 3 - 0
= 3
移動3格之後,即將參與比對的索引值$j 為0
$j 等於 「次相同區」長度
7.別的例子
移動前 移動後 錯誤移法(上下不同)
ABAA ABAA ABAA
ABAA ABAA ABAA
「相同區」長度為4
「次相同區」長度為1
移動格數 = 「相同區」長度 - 「次相同區」長度
= 4 - 1
= 3
移動3格之後,可以跳過1格不比對
「次相同區」長度,等於跳過不比對的長度
即將參與比對的索引值$j 為1
$j 等於 「次相同區」長度
8.別的例子
移動前 移動後 錯誤移法(上下不同)
ABAB ABAB ABAB
ABAB ABAB ABAB
「相同區」長度為4
「次相同區」長度為2
移動格數 = 「相同區」長度 - 「次相同區」長度
= 4 - 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 2
ACAQAC ACAQAC
確認是否可以沿用長度6的起點
7 ACAQACA ACAQACA
ACAQACA ACAQACA
長度越長,次相同區的尋找範圍就越長
「次相同區」的起點,不需要從「尋找範圍」的最左邊找起
「長度7」比「長度6」多1字
用多出來的那1字,即「長度7的最後1字」
去比對 $Key[$len]
確認「長度7」的次相同區,是不是「長度6」的次相同區「加長版」
它們「次相同區」起點是不是一樣的
如果它們「次相同區」起點是一樣的
「長度7的$len」等於「長度6的$len」加1
---
長度 尋找範圍 移動後 次相同區長度 ($len)
7 ACAQACA ACAQACA 3
ACAQACA ACAQACA
確認是否可以沿用長度7的起點
8 ACAQACAC ACAQACAC
ACAQACAC ACAQACAC
「長度8」不可以沿用「長度7」的起點
在長度7的「次相同區」裡面移動、尋找「次次相同區」
ACA ACA
ACA ACA
把長度7的「次相同區長度」 ($len 其值為 3)
當作$f[]的索引值
得到 新$len
即 $len = $f[$len]
新$len = $f[3] = 1
不可以沿用「長度7」的起點
ACAQACAC
ACAQACAC
確認是否可以沿用「新起點」 以「長度8的最後1字」比對 $Key[$len]
ACAQACAC
ACAQACAC
如果新起點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
AACAAQAACAAC AACAAQAACAAC
X X
AACAAC AACAAC
$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"
}
沒有留言:
張貼留言