# KMP 算法的定义K M P KMP K M P 算法常用于在一个文本串 S S S 内查找一个模式串 P P P 出现的位置,算法流程如下:
假设现在文本串 S S S 匹配到 i i i 位置,模式串匹配到 j j j 位置:
如果 j = -1
,或者当前字符匹配成功( s[i] == p[j]
),都令 i++, j++
,然后继续匹配下一个字符 如果 j != -1
,并且当前字符匹配失败( s[i] != p[j]
),则令 i i i 不变, j = next[j]
。此操作意味着当匹配失败时,模式串 P P P 相对于文本串 S S S 向右移动了 j - next[j]
位 # next 数组的含义表示当前字符之前的字符串中,有多大长度的相同前后缀。例如如果 next[j] = k
,则代表 j j j 之前的字符串中有最大长度为 k k k 的相同前后缀 此也意味着在某个字符匹配失败时,该字符对应的 n e x t next n e x t 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到 next[j]
的位置) 如果 next[j]
等于 0 0 0 或 − 1 -1 − 1 ,则跳到模式串的开头字符,若 next[j] = k
且 k > 0
,则代表下次匹配跳到 j j j 之前的某个字符,而不是跳到开头,且具体跳过了 k k k 个字符
# KMP 算法的步骤# 寻找前缀后缀最长公共元素长度对于 p = p0 p1 ... pj-1 pj
,寻找模式串 P P P 中长度最大且相等的前缀和后缀,如果存在 P 0 , P 1 , . . . , P k − 1 = P j − k , = P j − k , P j − k + 1 , . . . , P j P_0,~P_1,~...,~P_{k-1}~=~P_{j-k},~=P_{j-k},~P_{j-k+1},~...,~P_j P 0 , P 1 , . . . , P k − 1 = P j − k , = P j − k , P j − k + 1 , . . . , P j ,那么在包含 P j P_j P j 的模式串中有最大程度为 k + 1 k+1 k + 1 的相同前缀后缀 例如,如果给定的模式串 P P P 是 a b a b abab a b a b ,那么它的各个字串的前缀后缀公共元素的最大长度为:
模式串 a a a b b b a a a b b b 最大前缀后缀公共元素长度 0 0 0 0 0 0 1 1 1 2 2 2
# 求 next 数组n e x t next n e x t 数组考虑的是除当前字符外的最长相同前后缀串长度 ,所以求得各个前缀后缀的公共元素最大长度后,只要稍作变形即可,将步骤一中求得的值整体右移一位,初始值赋为 − 1 -1 − 1
模式串 a a a b b b a a a b b b n e x t next n e x t 数组− 1 -1 − 1 0 0 0 0 0 0 1 1 1
next[i]
表示第 i i i 个字符前,有长度为 next[i]
的相同前缀后缀字符串
# 根据 n e x t next n e x t 进行模式匹配如果匹配失败,模式串向右移动的位数为: j - next[j]
换言之,当模式串的后缀 P j − k , P j − k + 1 , . . . , P j − 1 P_{j-k},~P_{j-k+1},~...,~P_{j-1} P j − k , P j − k + 1 , . . . , P j − 1 跟文本串 S i − k , S i − k + 1 , . . . , S i − 1 S_{i-k},~S_{i-k+1},~...,~S_{i-1} S i − k , S i − k + 1 , . . . , S i − 1 匹配成功,但 P j P_j P j 和 S i S_i S i 匹配失败时,因为 next[j] = k
,相当于在不包含 P j P_j P j 的模式串中有最大长度为 k k k 的相同前缀后缀 即 P 0 , P 1 , . . . , P k − 1 = P j − k , = P j − k , P j − k + 1 , . . . , P j P_0,~P_1,~...,~P_{k-1}~=~P_{j-k},~=P_{j-k},~P_{j-k+1},~...,~P_j P 0 , P 1 , . . . , P k − 1 = P j − k , = P j − k , P j − k + 1 , . . . , P j ,故令 j = next[j]
,从而让模式串右移 j - next[j]
位,使得模式串的前缀 P 0 , P 1 , . . . , P k − 1 P_0,P_1,~...,~P_{k-1} P 0 , P 1 , . . . , P k − 1 对应着文本串 S i − k , S i − k + 1 , . . . , S i − 1 S_{i-k},~S_{i-k+1},~...,~S_{i-1} S i − k , S i − k + 1 , . . . , S i − 1 ,然后继续让 P k P_k P k 和 S i S_i S i 匹配
# 递推计算 next 数组# 再次介绍 next 数组的含义如果对于 k k k ,已有 P 0 , P 1 , P k − 1 = P j − k , P j − k + 1 , . . . , P j − 1 P_0,~P_1,~P_{k-1}~=~P_{j-k},~P_{j-k+1},~...,~P_{j-1} P 0 , P 1 , P k − 1 = P j − k , P j − k + 1 , . . . , P j − 1 相当于 next[j] = k
。 这意味着什么呢?究其本质, next[j] = k
代表 P 0 → P j − 1 P_0~\to~P_{j-1} P 0 → P j − 1 的模式字串中,有长度为 k k k 的相同前缀和后缀。 有了这个 n e x t next n e x t 数组,在 KMP 匹配中,当模式串中 j j j 处的字符继续跟文本串匹配失败时,下一步用 n e x t [ j ] next[j] n e x t [ j ] 处的字符继续跟文本串匹配,相当于模式串向右移动 j - next[j]
位
已知 n e x t [ 0 , . . . , j ] next[0,~...,~j] n e x t [ 0 , . . . , j ] ,如何求出 n e x t [ j + 1 ] next[j~+~1] n e x t [ j + 1 ] 呢 ? 对于 P P P 的前 j + 1 j+1 j + 1 个序列字符:如果 p[k] == p[j]
,则 next[j + 1] = next[j] + 1 = k + 1
。 如果 p[k] != p[j]
,如果此时的 p[next[k]] == p[j]
,则 next[j + 1 = next[k] + 1
,否则继续递归前缀索引 k = next[k]
,而后重复此过程。相当于在字符 P j + 1 P_{j+1} P j + 1 之前不存在长度为 k + 1 k+1 k + 1 的前缀 P 0 , P 1 , . . . , P k P_0,~P_1,~...,~P_k P 0 , P 1 , . . . , P k 跟后缀 P j − k , P j − k + 1 , . . . , P j − 1 , P j P_{j-k},~P_{j-k+1},~...,~P_{j-1},~P_j P j − k , P j − k + 1 , . . . , P j − 1 , P j 相等。那么是否可能存在另一个值 t + 1 ≤ k + 1 t+1~\le~k+1 t + 1 ≤ k + 1 ,使得长度更小的前缀 P 0 , P 1 , . . . , P t − 1 , P t P_0,~P_1,~...,~P_{t-1},~P_t P 0 , P 1 , . . . , P t − 1 , P t 等于长度更小的后缀 P j − 1 , P j − t + 1 , . . . , P j − 1 , P j P_{j-1},~P_{j-t+1},~...,~P_{j-1},~P_j P j − 1 , P j − t + 1 , . . . , P j − 1 , P j 呢 ?如果存在,那这个 t + 1 t+1 t + 1 便是 n e x t [ j + 1 ] next[j+1] n e x t [ j + 1 ] 的值,此相当于利用已经求得的 n e x t next n e x t 数组( n e x t [ 0 , . . . , k , . . . , j ] next[0,~...,~k,~...,~j] n e x t [ 0 , . . . , k , . . . , j ] )进行 P P P 串前缀跟 P P P 串后缀的匹配。 # 如何理解上面这个抽象的描述情况 1: P k = P j P_k~=~P_j~ P k = P j 模式串 A \color{red}A A B \color{red}B B C \color{green}C C D D D A \color{red}A A B \color{red}B B C \color{green}C C E E E 相同前后缀长度 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 0 0 0 n e x t next n e x t 值− 1 -1 − 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 ? ? ? 索引 P 0 P_0 P 0 P_ P k P_k P k P_ P_ P_ P j P_j P j P_
因为 P k = P j = ′ C ′ P_k~=~P_j~=~'C' P k = P j = ′ C ′ ,所以 next[j + 1] = next[j] + 1 = k + 1
(可以看出 next[j + 1] = 3
) 代表字符 E E E 前的模式串中,有长度为 k + 1 k+1 k + 1 的相同前后缀字串
情况 2: P k ≠ P j P_k~\not=~P_j P k = P j 模式串 A \color{red}A A B \color{red}B B C \color{green}C C D D D A \color{red}A A B \color{red}B B D \color{blue}D D E E E 相同前后缀长度 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 0 0 0 0 0 0 n e x t next n e x t 值− 1 -1 − 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 ? ? ? 索引 P 0 P_0 P 0 P_ P k \color{green}P_k P k P_ P_ P_ P j \color{blue}P_j P j
P k ≠ P j P_k~\not=~P_j P k = P j 说明 P 0 , P k − 1 , P k ≠ P j − k , P j − 1 , P j P_0,~P_{k-1},~P_k~\not=~P_{j-k},~P_{j-1},~P_j P 0 , P k − 1 , P k = P j − k , P j − 1 , P j 。换言之,字符 E E E 前能有多大长度的相同前缀后缀呢 ? 很明显,因为 ′ C ′ ≠ ′ D ′ 'C'~\not=~'D' ′ C ′ = ′ D ′ ,即字符 E E E 前没有长度为 k + 1 k+1 k + 1 的相同前缀后缀,也就不能简单地令 next[j + 1] = next[j] + 1
所以,我们只能寻找长度更短的相同前缀后缀 结合上表来看,如果能在前缀 P 0 , P k − 1 , P k P_0,~P_{k-1},~P_k P 0 , P k − 1 , P k 中不断递归前缀索引 k' = next[k]
,找到一个字符 P k ′ P_{k'} P k ′ 也为 ′ D ′ 'D' ′ D ′ ,代表 P k ′ = P j P_{k'}~=~P_j P k ′ = P j ,且满足于 P 0 , P k ′ − 1 , P k ′ ≠ P j − k ′ , P j − 1 , P j P_0,~P_{k'-1},~P_{k'}~\not=~P_{j-k'},~P_{j-1},~P_j P 0 , P k ′ − 1 , P k ′ = P j − k ′ , P j − 1 , P j ,则最大相同的前缀后缀长度为 k ′ − 1 k'-1 k ′ − 1 ,从而 next[j + 1] = k' + 1 = next[k'] + 1
如果没有这样的 k ′ k' k ′ 满足 P k ′ = ′ D ′ P_{k'}~=~'D' P k ′ = ′ D ′ 的话,则代表没有相同的前缀后缀, next[j + 1] = 0
# 为何递归 k = next_k 就能找到长度更短的前缀后缀 ?这归根到 n e x t next n e x t 数组的含义。我们拿前缀 P 0 , P k − 1 , P k P_0,~P_{k-1},~P_k P 0 , P k − 1 , P k 和后缀 P j − k , P j − 1 , P j P_{j-k},~P_{j-1},~P_j P j − k , P j − 1 , P j 匹配,如果 P k ≠ P j P_k~ \not= P_j P k = P j ,下一步就是用 p [ n e x t [ k ] ] p[next[k]] p [ n e x t [ k ] ] 去和 P j P_j P j 匹配,如果还是不匹配,则需要寻找长度更短的相同前缀后缀,即用 p [ n e x t [ n e x t [ k ] ] ] p[next[next[k]]] p [ n e x t [ n e x t [ k ] ] ] 去和 P j P_j P j 匹配。此过程相当于模式串的自我匹配,所以不断递归 k = next[k]
,直到要么找到长度更短的相同前缀后缀,要么没有长度更短的相同前缀后缀。
# 代码
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #include <stdio.h> #include <string.h> void KMPSearch (char * pat, char * txt) { int M = strlen (pat); int N = strlen (txt); int next[M]; computeNextArray (pat, M, next); int i = 0 ; int j = 0 ; while (i < N) { if (pat[j] == txt[i]) { i++; j++; } if (j == M) { printf ("Found pattern at index %d\n" , i - j); j = next[j - 1 ]; } else if (i < N && pat[j] != txt[i]) { if (j != 0 ) { j = next[j - 1 ]; } else { i++; } } } } void computeNextArray (char * pat, int M, int * next) { int len = 0 ; next[0 ] = 0 ; int i = 1 ; while (i < M) { if (pat[i] == pat[len]) { len++; next[i] = len; i++; } else { if (len != 0 ) { len = next[len - 1 ]; } else { next[i] = 0 ; i++; } } } } int main () { char txt[] = "ABABABAB" ; char pat[] = "AB" ; KMPSearch (pat, txt); return 0 ; }
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <stdio.h> #include <string.h> const int N = 1e6 + 10 ;char s[N], p[N];int next[N];void creatNext (char *s, int *next) { next[0 ] = 0 ; int len = 0 , i = 1 ; while (i < strlen (s)) { if (s[i] == s[len]) next[i++] = ++len; else { if (len != 0 ) len = next[len - 1 ]; else next[i++] = 0 ; } } } void kmp (char *s, char *p) { int i = 0 , j = 0 , n = strlen (s), m = strlen (p); while (i < n) { if (s[i] == p[j]) i++, j++; if (j == m) { printf ("%d " , i - j); j = next[j - 1 ]; } else if (i < n && s[i] != p[j]) { if (j != 0 ) j = next[j - 1 ]; else i++; } } } int main () { scanf ("%s%s" , s, p); creatNext (p, next); kmp (s, p); }