[演算法] KMP
KMP演算法是在一段文字中,找出指定的子字串出現位置(substring search)的演算法
比較詳細的說明我就不闡述,把資料留在文末。
名詞定義一下:
在一段文字中的文字,假設叫做 text
要搜尋的子字串,假設叫做 pattern
在 text 中比對到的位置使用 i,
在 pattern 中比對的位置使用 j
Prerequisites
- 製作出 prefix table
什麼是 Prefix table
Prefix table 是這個 pattern 字串所有的子字串的最長前後綴的表
舉例:
pattern = ABCDABD
各個子字串的最長前後綴
子字串 | 前綴 (prefix) | 後綴 (prefix) | 最大公前後綴字串長度 |
---|---|---|---|
A | 空 | 空 | 0 |
AB | A | B | 0 |
ABC | A,AB | C,BC | 0 |
ABCD | A,AB,ABC | D,CD,BCD | 0 |
ABCDA | A,AB,ABC,ABCD | A,DA,CDA,BCDA | 1 |
ABCDAB | A,AB,ABC,ABCD,ABCDA | B,AB,DAB,CDAB,BCDAB | 2 |
這時候就可以把最大公前後綴字串長度變成程式在用的模式
pattern
A | B | C | D | A | B | D |
0 | 0 | 0 | 0 | 1 | 2 | 0 |
-1 | 0 | 0 | 0 | 0 | 1 | 2 |
為什麼要多下面
-1 0 0 0 0 1 2 ?
其實就是把 prefix table 往後移動一位,在最前面補上 -1
而且完整的 pattern 其實用不到,所以大部分的做法都會這樣做哦!
其實 prefix table 的用意就是說
假設在 pattern 的 j = 5時,所座落在的位置:B
也就是 ABCDABD
其實 prefix table 的用意就是說
假設在 pattern 的 j = 5時,所座落在的位置:B
也就是 ABCDABD
前面最大相同的字串長度一定至少有兩位是一樣的,也就是至少是 AB!
當把 prefix table 建立完成之後,剩下就比較單純了
假設 text = "ABC ABCDAB ABCDABCDABDE"
當把 prefix table 建立完成之後,剩下就比較單純了
假設 text = "ABC ABCDAB ABCDABCDABDE"
pattern = "ABCDABD"
i = 0, j = 0
text[0] == pattern[0] => A == A => 所以接著比對下一個 => i++; j++;
text[0] == pattern[0] => A == A => 所以接著比對下一個 => i++; j++;
.
.
.
i = 3, j = 3
text[3] != pattern[3] => ' ' !== D => 這時候看一下 prefix table 的第j (j=3) 個位置
=> prefix[3] = 0
=> 所以把 j 移動到 0, 也就是 i = 3, j = 0 繼續比較
text[3] != pattern[3] => ' ' !== D => 這時候看一下 prefix table 的第j (j=3) 個位置
=> prefix[3] = 0
=> 所以把 j 移動到 0, 也就是 i = 3, j = 0 繼續比較
i = 3, j = 0
text[3] != pattern[0] => ' ' !== A => 這時候看一下 prefix table 的第j (j=0) 個位置
=> prefix[0] = -1
=> 所以把 j 移動到 j = -1
=> j = -1 時就直接跳下一個
=> 直接跳下一個 => i++; j++
=> i = 4, j = 0
i = 4, j = 0 接下來就一直比對到 i = 10, j = 6,
text[10] != pattern[6] => ' ' != D => 這時候看一下 prefix table 的第j (j=6) 個位置
=> prefix[6] = 2
=> 所以把 j 移動到 2, 也就是 i = 10, j = 2 繼續比較
i = 10, j = 2
i = 10, j = 2
text[10] != pattern[2] => ' ' != C => 這時候看一下 prefix table 的第j (j=2) 個位置
=> prefix[2] = 0
=> 所以把 j 移動到 0, 也就是 i = 10, j = 0 繼續比較
i = 10, j = 0
text[10] != pattern[0] => ' ' != A => 這時候看一下 prefix table 的第j (j=0) 個位置
=> prefix[0] = -1
=> j = -1
=> i++; j++;
=> i = 11, j = 0
i = 11, j = 0
沿路比對到
i = 17, j = 6
i = 17, j = 6
text[17] != pattern[6] => C != D => 這時候看一下 prefix table 的第j (j=6) 個位置
text[17] != pattern[6] => C != D => 這時候看一下 prefix table 的第j (j=6) 個位置
=> prefix[6] = 2
=> 所以把 j 移動到 2, 也就是 i = 17, j = 2 繼續比較
這次就剛好
i = 17, j = 2 沿路比對到 i = 21, j = 6 一路都相等且 j 為最後一位
這時候就找出 pattern 的位置為 i - j => 21 - 6 = 15!
i = 17, j = 2 沿路比對到 i = 21, j = 6 一路都相等且 j 為最後一位
這時候就找出 pattern 的位置為 i - j => 21 - 6 = 15!
function prefixTable(pattern) {
let maxPrefixLength = 0;
let prefixTable = [0];
let i = 1;
while ( i < pattern.length ) {
if (pattern[i] === pattern[maxPrefixLength]) {
maxPrefixLength++;
prefixTable[i] = maxPrefixLength;
i++;
} else {
if (maxPrefixLength > 0) {
maxPrefixLength = prefixTable[maxPrefixLength-1];
} else {
maxPrefixLength = 0;
prefixTable[i] = maxPrefixLength;
i++;
}
}
}
return prefixTable;
}
function movePrefixTable(prefixTable) {
let newPrefix = [...prefixTable];
for (i = newPrefix.length -1; i > 0; i--) {
newPrefix[i] = newPrefix[i-1];
}
newPrefix[0] = -1;
return newPrefix;
}
function kmpSearch(text, pattern) {
let m = text.length;
let n = pattern.length;
let prefix = movePrefixTable(prefixTable(pattern));
let i = 0;
let j = 0;
while (i < m) {
if (text[i] === pattern[j] && j === (n-1)) {
return i - j;
}
if (text[i] === pattern[j]) {
i++; j++;
} else {
j = prefix[j];
if (j === -1) {
i++;
j++;
}
}
}
return -1
}
補充資料:
https://www.youtube.com/watch?v=dgPabAsTFa8
留言
張貼留言