字符串匹配是在目标字符串target
中查找与字符串pattern
相匹配的起始索引位置的算法,字符串匹配算法中,Knuth-Morris-Pratt算法(简称KMP)是比较常用的算法之一。下边通过一个例子来介绍KMP匹配算法。
对暴力匹配的改进
为了引入KMP算法的优势,首先来看低效的暴力匹配方法。现在需要在字符串target
中匹配字符串pattern
,二者的值如下:
1 | target = "ABCABCDABCDABDE"; |
使用暴力匹配方法,需要从target
的第一个字符开始,依次与pattern
匹配:
(注:以下代码段中|
表示已匹配且匹配成功,x
表示已匹配且匹配失败,?
表示即将匹配)
1 | #1 012345678901234 |
逐个比较字符,直到遇到不同:
1 | #2 012345678901234 |
终止当前的匹配,将target
中游标位置右移1,开始新的一轮比较:
1 | #3 012345678901234 |
由于首个字符不一致,会继续跳过,直到target
中首个字符匹配时:
1 | #4 012345678901234 |
继续刚才的方法,直到全部匹配成功或出现不一致:
1 | #5 012345678901234 |
此时,如继续暴力匹配的方法,会进入#6
所示的状态:
1 | #6 012345678901234 |
这是一种极其低效的方法。实际上,在刚才一轮的比较过程中,回到#5
所示状态,可以看到参与比较的target
中第7-8个字符AB
,与pattern
的起始部分相匹配,而target
的第4-6个字符BCD
与pattern
的起始部分不匹配,这些信息对于计算程序是可知的。如果在进行逐个字符比较的过程中,可以依据这些信息,就能够在开始下次比较时,跳过第4-6个字符,直接将游标移到第7个字符,且从target
第9个字符开始比较,即#7
所示的状态,则会大大降低比较次数:
1 | #7 012345678901234 |
KMP算法即按照这一思路来实现,它会在比较过程中,记录比较信息,进而在下次比较时,跳过target
中已经参与过比较的字符,并计算出新的target
和pattern
中游标的位置,进而大大提高效率。
KMP算法
在KMP算法中,引入局部匹配表(partial match table)的概念,局部匹配表根据匹配字符串pattern
生成,是一个长度与pattern
相等的整数数组。此处先接着刚才的例子介绍KMP算法的原理,下节介绍局部匹配表如何生成。
对于pattern
字符串ABCDABD
,局部匹配表T = [-1,0,0,0,0,1,2]
,即:
1 | pat : A B C D A B D |
在每次匹配出现匹配失败时,target
中的游标不会回退到上次开始匹配的位置+1处,而是继续向后,并且会根据局部匹配表计算出pattern
中游标的位置。
回到刚才#5
所示的状态,出现匹配失败时,target
中游标索引i=9
,pattern
中游标索引位置k=6
,
1 | #5 012345678901234 |
KMP算法使用以下公式计算下次匹配开始时游标的位置(T
表示局部匹配表数组),当出现target[i]!=pattern[k]
时,下一次的匹配比较将在target[i]和pattern[T[k]]
之间进行:
1 | if(k==0) |
结合本文的例子,对于#5
的状态,则会计算出下次匹配时,有i=9
以及 k=2
,将此状态记为#8
1 | #8 012345678901234 |
对比发现与之前期望的#7
所示状态相同。
使用这种方法对于长度分别为m
的target
字符串,计算的时间复杂度为O(m)
。
初始化局部匹配表
构建局部匹配表的算法,对于长度为n
的字符串pattern
,时间复杂度为O(n)
。使用局部匹配表,可以使target
中的字符最多参与一次匹配比较。在字符串pattern
内部进行预匹配,查找和记录可能的匹配位置,保存为回退点,用于在出现不匹配时回退,以避免无用的比较。
定义T[0]= -1
,除T[0]
外后边的T[j]=k
的意义可以简单看成是:pattern
中下标为j
的字符(不含本身)的前面有最多k
个字符与pattern
起始的k
个字符相同。
依然使用之前的例子:
1 | pattern = "ABCDABD"; |
对于pattern
对应的局部匹配表,先确定前缀(prefix)和后缀(suffix)的概念,前缀是字符串中从第一个字符开始取到的子字符串,字符串的前缀长度必须小于原字符串。如字符串"HIJKL"
有前缀"H"
、"HI"
、 "HIJ"
、 "HIJK"
。后缀的定义与此相似,方向相反。
- 首先有
T[0] = -1
; - 接下来确定
T[1]
的值,比较整个字符串的前缀与pattern[0]
的后缀,pattern[0]="A"
无后缀,因此有T[1] = 0
; - 确定
T[2]
的值,比较整个字符串的前缀与pattern[0-1]
的后缀,pattern[0-1]
有后缀"B"
,但与pattern
的前缀不一致,因此T[2] = 0
; - 继续到
T[3]
,比较整个字符串的前缀与pattern[0-2]
的后缀,没有匹配的部分,所以T[3] = 0
; - 继续到
T[4]
,比较整个字符串的前缀与pattern[0-3]
的后缀,没有匹配的部分,所以T[4] = 0
; - 继续到
T[5]
,比较整个字符串的前缀与pattern[0-4]
的后缀,pattern[0-4] = "ABCDA"
,有后缀"A"
与整个字符串的前缀"A"
相同,相同部分长度为1,因此有T[5] = 1
。 - 最后到
T[6]
,比较整个字符串的前缀与pattern[0-5]
的后缀,pattern[0-5] = "ABCDAB"
,有后缀"AB"
与整个字符串的前缀"AB"
相同,相同部分长度为2,因此有T[6] = 2
。 - 比较结束,得到
T = [-1,0,0,0,0,1,2]
。
局部匹配表,又称为模式值或模式数组(next[]
)、失效函数(failure function)等,有若干种相似的定义方式,对应的KMP算法实现也稍有不同。计算局部匹配表的一种方法,及KMP算法的一种c++实现见下节。
KMP算法c++实现
1 | /* |
REFERENCE
https://en.wikipedia.org/wiki/Knuth–Morris–Pratt_algorithm
http://www.ituring.com.cn/article/59881
http://www.sanfoundry.com/cpp-program-implement-kruth-morris-patt-algorithm-kmp/