字符串匹配——一文吃透KMP算法
字符串匹配是一个基本且简单的任务,如果字符串 S1 和 S2 ,在 S1 中寻找是否包含 S2 ,用暴力的方法可以是从 S1 的第一个字符开始与 S2 匹配,然后一个字符一个字符的向后挪动再做匹配,但是这样是非常浪费时间的,那我今天我们来看看KMP算法是怎么做的。
本文例子和思路来源于:
http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html
以及Jake Boxer的文章
- Markdown和扩展Markdown简洁的语法
- 代码块高亮
- 图片链接和图片上传
- LaTex数学公式
- UML序列图和流程图
- 离线写博客
- 导入导出Markdown文件
- 丰富的快捷键
KMP(Knuth-Morris-Pratt)算法
假设原字符串
S1
为:
待进行匹配的字符串 S2 为:
KMP算法匹配目标字符串过程
说明:在字符串比较中,一般是要求原始文本( S1 长度要大于 S2 的,所以本例也是这样);后续的过程展示中,黄色填充的区域代表所对比的字符不同,粉色则代表相同。
1.从开头匹配
首先,将
S1
与
S2
对齐,比较二者第一个字符,是
B
与
2.后移一位
由于第一次比较发现两个字符串第一个字符不同,那么
S2
后移一位,将
S2
的第1个字符
A
与
3.继续后移并比较
继续后移,发现
4.发现第一个相同的字符
再次后移一位后,发现
S2
的第一个字符
A
与
5.出现不同
不断比较
S1和S2
的后面的字符,发现
S1[4]到S1[9]
与
S2[0]到S1[5]
完全相同,但
S2[6]
是
D
,与
6.常规思路
常规思路下,应当将
S2
后移一位,比较
S2[0]与S1[5]
是否相同,然后重复前面的步骤,但是这是一个冗余、浪费的过程,因为对于
S1[5]到S1[9]
实际上已经参与过比较过程了,因此重新比较一次是浪费的。那么应该怎么做呢?
7.KMP算法思路
请看图6,当发现
S2
的字符
D
与对应位置的
8.部分匹配值的计算
介绍部分匹配值之前,先要介绍“前缀”和“后缀”的概念。
“前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
举个例子:字符串’sharlock’
前缀为:s , sh , sha , shar , sharl , sharlo , sharloc 共7个;
后缀为:harlock , arlock , rlock , lock , ock , ck , k 共7个。
部分匹配值就是字符串的每个子串的”前缀”和”后缀”共有元素中长度最长的这个长度值。
以 S2 (ABCDABD)为例:
-”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。
通过上述分析可以得到图8的部分匹配值表。这里我要说明一点,图8这个表很多人的理解有误区,这个表的形式是每个单一的字符(如 A,B,C 等)对应一个数字(部分匹配值),其实不是的,每个匹配值对应的是它这个位置的字符以及前面所有字符组成的子串所对应的部分匹配值。这个一定要理解好,不然你会觉得这个表很怪,具体的说,图8第五列的A,对应了数字5,其实质是说 S2 的子串之一“ABCDA”对应的部分匹配值是1,回顾部分匹配值的定义,其主语是字符串的“子串”,这点一定要好好理解,不然写代码的时候就会蒙逼了。
9.根据部分匹配值向后移动 S2
刚才说了,我们要确定的是 S2 需要向后移动几位,这个移动的位数计算方式如下:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
已知
S1
的空字符与
S2
的
D
不匹配时,前面六个字符”
10.继续根据部分匹配值后移 S2
因为图9中黄色位置
S1
的空字符与
S2
的
C
不匹配,因此
图10黄色区域中
S1
为空字符,
S2
对应的是
A
,所以说此时
对于图11中的 S1和S2 进行逐位比较,直到发现 C与D 不匹配。于是,移动位数 = 6(ABCDAB的长度) - 2( S2 子串ABCDAB的部分匹配值),继续将搜索词向后移动4位。得到图12的结果。
对于图12,逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位。
以上就是KMP算法的思想和流程。
“部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是2(”AB”的长度)。搜索词移动的时候,第一个”AB”向后移动4位(字符串长度-部分匹配值),就可以来到第二个”AB”的位置。
Python3实现KMP算法
# 普通匹配,返回从原字符串的第几位成功匹配
def naive_match(s, p):
m = len(s)
n = len(p)
for i in range(m-n+1):#起始指针i
if s[i:i+n] == p:
print ('match from %d'%i)
return i
return 'No match'
# KMP算法
## KMP
def kmp_match(s, p):
m = len(s); n = len(p)
cur = 0#起始指针cur
table = partial_table(p)
while cur<=m-n:
for i in range(n):
if s[i+cur]!=p[i]:
cur += max(i - table[i-1], 1)#有了部分匹配表,我们不只是单纯的1位1位往右移,可以一次移动多位
break
else:
return True
return False
## 部分匹配表
def partial_table(p):
prefix = set()
postfix = set()
ret = [0]
for i in range(1,len(p)):
prefix.add(p[:i])
postfix = {p[j:i+1] for j in range(1,i+1)}
ret.append(len((prefix&postfix or {''}).pop()))
return ret