[数据结构与算法]KMP的理解

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表:

index0123456
next[index]-1000012

上面的next表我们可以直接看到一个特殊的现象。next的元素2前是1,1前是0。探其原因,是前一个字符的真子串的真后缀都删除了最后一个字符(真子串变短一位),因此前一项的next是后一项减一,当然这一普遍现象只对于大于零的元素而言。

死循环的原因及解决(例举)

此外为什么上表中0处的next为 -1呢?这是为了消除潜在的死循环。
如aab这个子串如果使用子串(包含自己这个元素的子串)作为判断的话显然可以得到next=[0,1,0]如果在第二个a处匹配失败的话,我们需要跳转到1处,而此时index就是1,这就进入了死循环 ;同理如果在第一个a处匹配失败,我们又在0处死循环了。

模式串aab
index012
next[index]010

因此我们使用真子串作为得到next数组的依据规定next[0]=-1
如果心中有疑惑,不妨拿出纸和笔来,设一个主串带入上述错误试一试。

跳转出现的现象

分两种情况

  1. 当前next为0或-1,这时没有什么现象,就把模式串指针移到0位即可。
  2. 当前next不为0,这时候一定会有一个有趣且重要的现象,当前指针前一个元素一定和跳转后指针前一个元素相同,这也是next数组以及算法所实现的核心所在----真子串的前后缀相等的最大长度就是当前索引下的next元素。
    下面做一个简单KMP算法的动画演示。
    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做的动画演示,都花费了不少心血,但也让我得到了不少的收获。所以我觉得这是有意义的,最后的最后,希望能给有需要的同学提供帮助!

本文为原创文章,转载请标明出处!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值