KMP算法目的:盡快解決字符串匹配問題,時間復雜度為O(m+n),而常規(guī)的簡單匹配算法時間復雜度:O(m*n)
這個算法不太容易理解,而且網(wǎng)上很多關于KMP算法的文章讀起來很費勁娱节,以下,我按照自己的理解祭示,試著寫一篇易懂的算法解釋肄满。
1 關于模式的前后綴函數(shù)(next數(shù)組獲取)
首先,為了方便后面的描述质涛,先定義下:S表示原字符串稠歉,T表示目標字符串(模式串),關于字符串匹配蹂窖,就是在S中尋找T轧抗。
關于尋找字符串的前后綴,舉個例子:
字符串:abcab
前綴:a瞬测,ab横媚,abc纠炮,abca
后綴:bcab,cab灯蝴,ab恢口,b
“前綴”指除了最后一個字符以外,一個字符串全部頭部組合穷躁。
“后綴”指除了第一個字符以外耕肩,一個字符串全部尾部組合。
模式前后綴函數(shù)问潭,就是產生一個長度等于模式串T長度的數(shù)組猿诸,每個值為相應“部分匹配值”的數(shù)組。
“部分匹配值”就是“前綴”和“后綴”的最長的共有元素字符串的長度狡忙。以“ABCDABD”為例:
模式串 | A | B | C | D | A | B | D |
---|---|---|---|---|---|---|---|
部分匹配值(next) | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
- "A"的前綴和后綴都為空集梳虽,共有元素的長度為__ 0 __;
- "AB"的前綴為[A],后綴為[B],共有元素的長度為__ 0 __夏块;
- "ABC"的前綴為[A, AB],后綴為[BC, C]禀挫,共有元素的長度__ 0 __;
- "ABCD"的前綴為[A, AB, ABC]拓颓,后綴為[BCD, CD, D]语婴,共有元素的長度為__ 0 __;
- "ABCDA"的前綴為[A, AB, ABC, ABCD]录粱,后綴為[BCDA, CDA, DA, A]腻格,共有元素為"A",長度為__ 1 __啥繁;
- "ABCDAB"的前綴為[A, AB, ABC, ABCD, ABCDA]菜职,后綴為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB"旗闽,長度為__ 2 __酬核;
- "ABCDABD"的前綴為[A, AB, ABC, ABCD, ABCDA, ABCDAB],后綴為[BCDABD, CDABD, DABD, ABD, BD, D]适室,共有元素的長度為__ 0 __嫡意。
所以,對于字符串"ABCDABD"捣辆,會相應地產生一個數(shù)組array(0, 0, 0, 0, 1, 2, 0)蔬螟。這就是KMP算法關于next數(shù)組(也就是計算前后綴函數(shù))原理,它記錄的是模式串T子串(T[0 ... j] 0 < j < n)的最長前后綴元素長度的信息汽畴。
接下來旧巾,介紹KMP算法思想耸序。
2 KMP算法思想
第一次:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ||
---|---|---|---|---|---|---|---|---|---|---|---|
S | a | b | c | a | b | c | a | b | d | a | |
T | a | b | c | a | b | d | |||||
0 | 1 | 2 | 3 | 4 | 5 |
第二次:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ||
---|---|---|---|---|---|---|---|---|---|---|---|
S | a | b | c | a | b | c | a | b | d | a | |
T | a | b | c | a | b | d | |||||
0 | 1 | 2 | 3 | 4 | 5 |
傳統(tǒng)匹配算法中,每一輪匹配過后鲁猩,都會回溯到T[0]和S[i+1]的狀態(tài)位置開始下一輪的匹配坎怪。而上面的表圖運用了KMP算法,顯然兩次就能得出匹配信息:
這里先給出模式串T("abcabd")的next數(shù)組參照:
模式串 | a | b | c | a | b | d |
---|---|---|---|---|---|---|
部分匹配值(next) | 0 | 0 | 0 | 1 | 2 | 0 |
0 | 1 | 2 | 3 | 4 | 5 |
第一個表格中廓握,S[5]與T[5]匹配失敗時搅窿,T[0 ... 4]字符串最長“前-后綴”是"ab",它在T[0 ... 4]中對應的前綴是T[0 ... 1]隙券,后綴是T[3 ... 4] (前后綴相等)男应,既然T[0 ... 4]與S[0 ... 4]匹配成功,那么T[0 ... 1]必然與S[3 ... 4]完全匹配是尔。(先結合next獲取那段好好理解)
由此可以想到殉了,當S[i]與T[j]匹配失敗時开仰,如果我們知道T[0 ... j-1]最長“前-后綴”在T[0 ... j - 1]對應的前綴是T[0 ... m] (m = next[j-1] - 1)拟枚,那么我們可以直接將S[i]與T[m+1]對齊,開始下次匹配众弓,因為T[0 ... m]必然已經(jīng)與S[0 ... i - 1]的后綴匹配成功恩溅。避免了不必要的回溯
下面是KMP算法代碼:
def `kmp-matcher` (s: String, t: String): Int = {
val next = `init-next`(t)
val s_len = s.length
val t_len = t.length
var i = 0 /*記錄原字符串下標*/
var j = 0 /*記錄模式串下標*/
while (i < s_len && s_len - i > t_len - j) {
while (j < t_len /*注意先檢查下標越界*/ && s.charAt(i) == t.charAt(j)) {
i = i + 1
j = j + 1
}
/*
* 下面有兩種分支,完全匹配和匹配中斷
* 完全匹配:函數(shù)直接返回匹配時的坐標
* 匹配中斷:設置i谓娃,j下標脚乡,使其S[i]與T[NEXT[j-1]]對齊,進行下一次匹配
*/
if (j == t_len /*完全匹配滨达,此處直接返回此次匹配首位下標*/)
return i - t_len
else /*匹配中斷*/
j = next(j match {
case 0 => i = i + 1; j/*無“前-后綴”奶稠,直接將i下標加一匹配*/
case _ => j - 1
})
}
-1 /*無匹配項*/
}
上述代碼缺少\
init-next` `函數(shù)的實現(xiàn),也就是next數(shù)組的獲燃癖椤:
實際上锌订,next數(shù)組記錄的是模式串T的各個字串C[0 ... j] (0 < j < T.length)的最長“前-后綴”長度信息。
傳統(tǒng)暴力求解next數(shù)組顯然很低效画株,這里也運用KMP匹配的方法獲取next數(shù)組辆飘。也就是在模式串T自身上使用KMP匹配:
用兩個變量i和j掃描T,i將模式串看做S谓传,j將模式串看做T蜈项。每次增加i時,賦予next[i]合適的值续挟,也就是最長“前-后綴”的長度紧卒。
def `init-next` (s: String): Array[Int] = {
val len = s.length
val next = new Array[Int](len)
next(0) = 0 //首個元素最大前后綴元素:無,所以此處設置為0
var i = 1
var j = 0
while (i < len) {
if (s.charAt(i) == s.charAt(j)) {
j = j + 1
next(i) = j
i = i + 1
} else if (j == 0) {
next(i) = j
i = i + 1
} else {
j = next(j - 1)
}
}
next
}