KMP的原理及优势
KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next数组实现
根据百科的回答,我们能够得到算法的原理大概是利用已知的信息,减少重复冗余的访问主串。因此,KMP算法与Brute-Force算法最大的不同在于主串索引是不会回溯的,因此我们可以带着这个条件来设计我们的算法。
算法设计
next数组
next数组是这个算法比较精髓的部分,它里面装着匹配失败后模式串指针该要跳转的索引,因此得到这个数组便是这个算法最重要也是最难得一部分。
next数组预备知识
首先说明一个概念,真子串是不包含自己这个元素的前一段字符所组成的串如:
“abcdef"这个字符串,‘e‘的子串是"abcde”,而真子串是"abcd",
这是个比较重要的概念,因为如果定义不一样的话最后得到的next数组也可能会有比较大的区别。当然了,一方面也是为了防止死循环(下一个标题举例说明)。
在理解了真子串这个概念后,我们可以开始理解真前缀和真后缀的概念。就是串的前(0~(i-1))个字符组成叫真前缀(end=(i-1)表示前缀不包括自己这个串,所以叫做真前缀),后缀与前缀同理,不过有一点需要注意的是不要把后缀与 回文 混淆了,真后缀的字符顺序不会变。(next数组的得到标题下举例说明)
next数组的得到
这次我们用"ABCDABD"来举例,我们求第二个’D’的next值。首先列出他的所有真前后缀。
前缀 | “A” | “AB” | “ABC” | “ABCD” | “ABCDA” |
---|---|---|---|---|---|
后缀 | “B” | “AB” | “DAB” | “CDAB” | “BCDAB” |
我们取前后缀完全相同的长度为next的元素值,下标则是这个字符在字符串的下标(从0开始)。
我们显然可以看到第二个’D’的next值为2,因为"AB"==“AB”.所以next[6] = 2。
同理我们便可以得到"ABCDABD"的next表:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
next[index] | -1 | 0 | 0 | 0 | 0 | 1 | 2 |
上面的next表我们可以直接看到一个特殊的现象。next的元素2前是1,1前是0。探其原因,是前一个字符的真子串的真后缀都删除了最后一个字符(真子串变短一位),因此前一项的next是后一项减一,当然这一普遍现象只对于大于零的元素而言。
死循环的原因及解决(例举)
此外为什么上表中0处的next为 -1呢?这是为了消除潜在的死循环。
如aab这个子串如果使用子串(包含自己这个元素的子串)作为判断的话显然可以得到next=[0,1,0]如果在第二个a处匹配失败的话,我们需要跳转到1处,而此时index就是1,这就进入了死循环 ;同理如果在第一个a处匹配失败,我们又在0处死循环了。
模式串 | a | a | b |
---|---|---|---|
index | 0 | 1 | 2 |
next[index] | 0 | 1 | 0 |
因此我们使用真子串作为得到next数组的依据;规定next[0]=-1。
如果心中有疑惑,不妨拿出纸和笔来,设一个主串带入上述错误试一试。
跳转出现的现象
分两种情况
- 当前next为0或-1,这时没有什么现象,就把模式串指针移到0位即可。
- 当前next不为0,这时候一定会有一个有趣且重要的现象,当前指针前一个元素一定和跳转后指针前一个元素相同,这也是next数组以及算法所实现的核心所在----真子串的前后缀相等的最大长度就是当前索引下的next元素。
下面做一个简单KMP算法的动画演示。
原理的理解
根据上面的各种现象及其它们提供的信息,我们大概就可以得出KMP算法的原理了——将前缀移到后缀的位置,使二者重合利用了后缀所匹配得到的信息,将前缀移到后缀的位置,使二者重合,使得主串指针不回溯而走冤枉路。
代码
class KMP:
"""
KMP 算法
实现模式串匹配功能
打开文件获取主串并查找模式串出现的所有位置。
Attributes:
modeStr: 模式串, 需要匹配的字符串
mainStr: 主串, 被匹配的字符串
next: next数组, 存放匹配失败后模式串将要跳转的下标
lastNext: next的推广 多出来的next用于成功匹配后模式串下标跳转位置
modeLen:模式串长度, 创建next的大小,以及方法中循环的终止条件
"""
modeStr = ''
mainStr = ''
next = []
lastNext = 0
modeLen = 0
def __init__(self, str):
"""
类的初始化
为模式串及相应的数据初始化赋值
Args:
self:
str:模式串输入
Returns:None
"""
self.modeStr = str
self.modeLen = len(str)
self.next = [0 for _ in range(self.modeLen)]
def getNext(self):
"""
获得模式串的next数组
next数组是模式串的 当前元素的 真子串的前后缀 相同的最大长度
根据上述条件求得next数组
Args:
self:
Returns:None
改进:将next数组多加一位(本代码使用lastNext),将整个模式串作为作为真字串获得lastNext,
用于多次查找。当成功匹配后,模式串下标指向的位置。
"""
for i in range(0, self.modeLen+1):
if i == 0:
self.next[i] = -1
continue
for lenAffix in range(0,i):
if self.modeStr[:lenAffix] == self.modeStr[i-lenAffix:i]:
if i != self.modeLen:
self.next[i] = lenAffix
else:
self.lastNext = lenAffix
else:
break
print('-'*35)
print("next数组为:\n"+'-'*(7+4*self.modeLen))
print('|index', end='|')
for i in range(self.modeLen):
print('%3d'% i, end='|')
print("\n|next:", end='|')
for i in self.next:
print('%3d'% i, end='|')
print('\n'+'-'*(7+4*self.modeLen))
def match(self, mStr):
"""
模式串匹配
利用next数组找到模式串在主串中的位置并返回
Args:
self:
mStr: 主串
Returns:模式串在主串的位置
"""
i = 0; j = 0
index = []
while i < len(mStr):
if j == -1 or j < self.modeLen and mStr[i] == self.modeStr[j]:
i += 1
j += 1
else:
if j == self.modeLen:
i = i - self.modeLen + 1
j = self.lastNext
index.append(i)
else:
j = self.next[j]
return index
def getMainStr(self, fileName):
"""
入口函数
读取参数中的文件作为主串,找到所有匹配的位置并打印
Args:
self:
fileName: 主串存放的文件名
Returns:None
"""
self.getNext()
numRow = 0
numColumn = 0
count = 0
# 下面的路径更改为自己的路径
with open(r'E:\My projects\Data Structures and Algorithm\KMP\\' + fileName, 'r') as f:
print("\nRow\t\tColumn\n"+'-'*15)
for line in f:
if len(line) >= self.modeLen:
numRow += 1
numColumn = self.match(line)
for i in numColumn:
count += 1
print('%4d\t\t%2d' % (numRow,i))
f.close()
print('-'*15 + '\n'+'共找到%3d条匹配信息。'% count)
modeStr = ''
while modeStr == '':
modeStr = input('请输入模式串:')
a = KMP(modeStr)
a.getMainStr(input('请输入文件名: '))
总结
第一次的博客贡献给了KMP算法,花费了我不少时间,从开始构思,到实现代码,以及ppt做的动画演示,都花费了不少心血,但也让我得到了不少的收获。所以我觉得这是有意义的,最后的最后,希望能给有需要的同学提供帮助!
本文为原创文章,转载请标明出处!