字符串匹配问题:给定字符串 s t r str str 和 s u b s t r substr substr ,若 s u b s t r substr substr 是 s t r str str的子串,返回子串所在位置,若不是子串,返回-1。
假设 s t r = ′ B B C A B C D A B A B C D A B C D A B D E ′ str='BBC ABCDAB ABCDABCDABDE' str=′BBCABCDABABCDABCDABDE′, s u b s t r = ′ A B C D A B D ′ substr='ABCDABD' substr=′ABCDABD′,下面以此为例,分别说明暴力法、KMP算法解决字符串匹配问题的思想。
朴素字符换匹配算法(暴力)
-
首先,比较字符串 s t r str str和搜索词 s u b s t r substr substr的第一个字符, ′ B ′ 'B' ′B′与 ′ A ′ 'A' ′A′不匹配,字符串 s t r str str后移一位。
-
直到字符串 s t r str str中有一个字符,与搜索词 s u b s t r substr substr的第一个字符相同为止。
-
字符串与搜索词同时后移一位,比较下一个字符是否相同。
-
直到字符串有一个字符,与搜索词对应的字符不相同为止。
-
将搜索词整个后移一位,再从搜索词第一个字符逐个比较。
-
以此类推。
时间复杂度: O ( m ∗ n ) O(m*n) O(m∗n),其中 m = l e n ( s t r ) , n = l e n ( s u b s t r ) m=len(str),n=len(substr) m=len(str),n=len(substr)。
KMP字符串匹配算法
KMP算法的思想是,利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。为利用已经匹配的信息,KMP算法针对搜索词,计算出一张部分匹配表。
1. 创建部分匹配表
首先给出概念:
- 前缀:指除了最后一个字符以外,一个字符串的全部头部组合。
- 后缀:指除了第一个字符以外,一个字符串的全部尾部组合。
- 部分匹配值:"前缀"和"后缀"的最长的共有元素的长度。
部分匹配表 p a r t i a l _ m a t c h _ t a b l e partial\_match\_table partial_match_table是与搜索词 s u b s t r substr substr长度相同的数组, p a r t i a l _ m a t c h _ t a b l e [ i ] partial\_match\_table[i] partial_match_table[i]表示搜索词的子串 s u b s t r [ : i + 1 ] substr[:i+1] substr[:i+1](不包含 i+1 位)的部分匹配值。
搜索词
s
u
b
s
t
r
=
′
A
B
C
D
A
B
D
′
substr='ABCDABD'
substr=′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。
2. 利用部分匹配表进行字符串匹配
暴力法遇到不匹配的字符时,将整个字符串向后移动一位,从搜索词的第一个字符重新匹配。KMP算法中给出了部分匹配表,根据不匹配字符的部分匹配值,即可利用已匹配信息,移动搜索词:搜索词移动维数=已匹配字符数-最后一个匹配字符的部分匹配值。
- 空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此搜索词向后移动的位数为:6-2=4。
- 空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(“AB”),对应的"部分匹配值"为0。所以将搜索词向后移位数 = 2 - 0。
- 空格与A不匹配,继续后移一位。
- 逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
- 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。
Python代码
def KMP(s, sub):
m = len(s);
n = len(sub)
cur = 0 # 起始指针cur
table = partial_match_table(sub)
while cur <= m - n: # 只去匹配前m-n个
for i in range(n):
if s[i + cur] != sub[i]:
cur += max(i - table[i - 1], 1) # 有了部分匹配表,我们不只是单纯的1位1位往右移,可以一次移动多位
break
else: # for 循环中,如果没有从任何一个 break 中退出,则会执行和 for 对应的 else,只要从 break 中退出了,则 else 部分不执行。
return True
return False
# 部分匹配表
def partial_match_table(sub):
'''''partial_table("ABCDABD") -> [0, 0, 0, 0, 1, 2, 0]'''
prefix = set()
postfix = set()
ret = [0]
for i in range(1, len(sub)):
prefix.add(sub[:i])
postfix = {sub[j:i + 1] for j in range(1, i + 1)}
ret.append(len((prefix & postfix or {''}).pop()))
return ret
print(partial_match_table("ABCDABD"))
print(KMP("ABCDAB ABCDABCDABDE", "ABCDABD"))
时间复杂度: O ( m + n ) O(m+n) O(m+n),其中 m = l e n ( s t r ) , n = l e n ( s u b s t r ) m=len(str),n=len(substr) m=len(str),n=len(substr)。
for else语句(Python)
- 当迭代对象完成所有迭代后且此时的迭代对象为空时,如果存在else子句则执行else子句,没有则继续执行后续代码;
- 如果迭代对象因为某种原因(如带有break关键字)提前退出迭代,则else子句不会被执行,程序将会直接跳过else子句继续执行后续代码。
for i in range(10):
if i == 5:
print 'found it! i = %s' % i
else:
print 'not found it ...'
输出:
found it! i = 5
not found it …
for i in range(10):
if i == 5:
print 'found it! i = %s' % i
break
else:
print 'not found it ...'
输出:
found it! i = 5
算法思想:字符串匹配的KMP算法
代码:字符串匹配的kmp算法 及 python实现
Python中for循环搭配else的陷阱