【408数据结构】KMP算法和烦人的手算next数组

简单的模式匹配算法

      最常见最基础的模式匹配算法也称为暴力匹配算法,他的主要思想是:首先定义两个计数变量 i 和 j 来表示主串S和模式串T当前时刻正在比较的字符位置。 首先将主串S的第一个字符和模式串T的第一个字符比较,若相等,则比较下一个位置的字符,若不相等,则重新将主串S的下一个字符与模式串T的第一个字符相比较,一次类推,直到在主串S中找到一串连续的字符与模式串T完全相等,则匹配成功,若直到最后也没有找到,则匹配失败。

def naive_pattern_matching(text, pattern):  
    n = len(text)  
    m = len(pattern)  
      
    # 遍历主字符串  
    for i in range(n - m + 1):  
        j = 0  
        # 遍历模式字符串  
        while j < m and text[i + j] == pattern[j]:  
            j += 1  
        # 如果完整遍历了模式字符串,则找到了匹配  
        if j == m:  
            return i  
      
    # 如果遍历结束仍未找到匹配,返回-1  
    return -1

       他的效率低,时间复杂度较高,最坏时间复杂度是O(mn),其中n和m分别是主串和模式串的长度。并且,它没有利用已匹配的信息。

 KMP算法

       观察上图可以看出,实际上第二趟是没必要进行比较的,因为模式串的第一个字符‘a’和第二个字符‘b’是不相等的,在第一趟比较时我们就可以确定主串的第二个字符是‘b’,就没有必要在第二趟时再将‘a’和‘b’进行一次比较。直接将模式串滑动到对应的主串位置进行比较即可。并且,我们发现,模式串的滑动只与它本身有关而不予主串有关。

       首先,我们来理解三个概念:

       前缀:除了字符串的最后一个字符外,所有的头部子串。

       后缀:除了字符串的第一个字符外,所有的尾部子串。

       部分匹配值: 前缀和后缀的最长相等前后缀长度。

       以‘abaabc’为例:

‘a’的前缀后缀都为空,最长相等前后缀长度为0。

‘ab’的前缀为{a},后缀为{b},交集为空,最长相等前后缀长度为0。

‘aba’的前缀为{a,ab},后缀为{a,ba},交集为{a},最长相等前后缀长度为1。因为前缀和后缀有最长的相等的元素‘a’,‘a’的长度为1。

‘abaa’的前缀为{a,ab,aba},后缀为{a,aa,baa},交集为{a},最长相等前后缀长度为1。

‘abaab’的前缀为{a,ab,aba,abaa},后缀为{b,ab,aab,baab},交集为{ab},最长相等前后缀长度为2。

‘abaabc’的前缀为{a,ab,aba,abaa,abaab},后缀为{c,bc,abc,aabc,baabc},交集为空,最长相等前后缀长度为0。

因此,‘abaabc’部分匹配值为001120

然后我么们就可以利用这个部分匹配值来进行计算了,对比上图可以看出,第一趟匹配时,‘a’和处于第六位置的‘c’匹配不上,所以,已经匹配的有5个字符,因此,我们找到模式串的部分匹配值中的第五位,也就是2,然后利用下面公式

移动位数 = 已匹配字符数 - 对应部分的部分匹配值

5-2=3,于是,我可可以将模式串向后移动三位,重新进行匹配,即我们可以直接进行上图的第四趟匹配,最终匹配成功。

我们可以看出,在某一位置出错,我们需要去找到它前一位置对应的部分匹配。为了使用方便,我们可以将部分匹配值整体右移一位,在最左侧补一个‘-1’,舍去最右侧的数字,这样,匹配失败的元素对应位置对照自己的部分匹配值即可。即将‘abaabc’部分匹配值转变为-100112。我们就把转换后的数组称为next数组。

  • 使用-1进行填充是因为当模式串第一个字符都没匹配上时需要将模式串整体向右移动一位,不需要计算模式串移动的位数。
  • 将部分匹配值最右侧的数字舍去是因为永远不会用到它

求next数组python实现 

def compute_next(pattern):  
    """  
    计算模式串pattern的next数组  
    :param pattern: 模式串  
    :return: next数组  
    """  
    # next数组长度比模式串长度多1,为了索引0能够统一处理  
    next_array = [-1] * len(pattern)  
    # next[0]固定为-1  
    j = 0  # j为next数组计算时前一个相同前后缀的结尾位置  
    i = 1  # i为当前位置  
      
    while i < len(pattern):  
        if pattern[i] == pattern[j]:  
            # 当前字符与前缀中对应字符相同,next[i]等于j+1  
            next_array[i] = j + 1  
            i += 1  
            j += 1  
        elif j != 0:  
            # 当前字符与前缀中对应字符不相同,且j不是0,需要利用之前的next数组来回溯  
            j = next_array[j - 1]  
        else:  
            # 当前字符与前缀中对应字符不相同,且j为0,则没有前后缀匹配,直接赋值next[i]为0  
            next_array[i] = 0  
            i += 1  
      
    return next_array  

 KMP算法python实现

def kmp_search(text, pattern):  
    """  
    使用KMP算法在文本中搜索模式串  
    :param text: 文本串  
    :param pattern: 模式串  
    :return: 模式串在文本串中第一次出现的起始索引,如果不存在则返回-1  
    """  
    next_array = compute_next(pattern)  
    i = 0  # i是文本串的索引  
    j = 0  # j是模式串的索引  
  
    while i < len(text) and j < len(pattern):  
        if text[i] == pattern[j]:  
            # 如果当前字符匹配,则同时移动i和j  
            i += 1  
            j += 1  
        elif j != 0:  
            # 如果不匹配且j不是0,则根据next数组回溯j  
            j = next_array[j - 1]  
        else:  
            # 如果不匹配且j是0,则移动i  
            i += 1  
  
    # 如果j等于模式串的长度,则说明找到了模式串  
    if j == len(pattern):  
        return i - j  
    else:  
        # 否则,模式串不在文本串中  
        return -1  

手算next数组方法

 方法一

       先求出部分匹配值,再进行右移,具体例子在上面讲解部分匹配值时已经讲过,这里不再赘述。我们要注意,在面对实际题目时,我们 一定要看清楚题目要求,是以-1开始,还是以0 开始,如果以0开始,就是在以-1开始的基础上给每个数字加1。

对于求abaabc的next数组,我们得到的next数组是-100112,同时加一后得到的next数组是011223,这两个结果都是正确的。

方法二 

       首先,对于模式串T,next数组第一位为0,第二位为1,之后T的每一位字符y求解next值时:根据它的前一位字符x和x的next值来求解:判断字符x和字符T[x.next](T的下标是1,2,3,4而不是从0开始)是否相等,若相等,则字符y的next值就是字符x的next值加一;若不相等,则接着找字符T[x.next]对应的next值对应的字符即T[T[x.next].next],与x相比较,直至找到某个位置的next值对应的T的字符相等,则取这个位置的next值加一作为y的next值,若直到第一个字符都没有与字符x相等的,则y的next值为1。

这句话太抽象,举个简单的例子。例如求abaabc的next数组。

1、初始

模式串abaabc
下标123456
next数组01

2、求第三位的next值:因为a的前一位是b,b的next值为1,所以对应的字符T[1]就是a,a不等于b,a就是第一位字符,再往前就没有了,所以第三位a的next值为1。

模式串abaabc
下标123456
next数组011

3、求第四位的next值:因为a的前一位是a,a的next值为1,所以对应的字符T[1]就是a,a等于a,所以第四位a的next值为2 = 1(第三位a的next值为1)+1 。

模式串abaabc
下标123456
next数组0112

4、求第五位的next值:因为b的前一位是a,a的next值为2,所以对应的字符T[2]就是b,a不等于b,再看b的next值为1,所以对应的T[1]就是a,a等于a,所以第五位b的next值为2 = 1(第二位b的next值为1)+1

模式串abaabc
下标123456
next数组01122

4、求第六位的next值:因为c的前一位是b,b的next值为2,所以对应的字符T[2]就是b,b等于b,所以第六位c的next值为3 = 2(第五位b的next值为1)+1

模式串abaabc
下标123456
next数组011223

所以,abaabc的next数组是011223。与我们用另一种方法求得的结果相同。

真题实例

Tababaaababaa
下标123456789101112

1、初始

Tababaaababaa
next01

2、求第三位。第二位是b,next值为1,对应T[1]为a,a不等于b,再往前没有字符了,所以第三位next值为1 = 0+1

Tababaaababaa
next011

3、求第四位。第三位是a,next值为1,对应T[1]为a,a等于a,所以第四位next值为2 = 1+1

Tababaaababaa
next0112

4、求第五位。第四位是b,next值为2,对应T[2]为b,b等于b,所以第五位next值为3 = 2+1

Tababaaababaa
next01123

5、求第六位。第五位是a,next值为3,对应T[3]为a,a等于a,所以第六位next值为4 = 3+1

Tababaaababaa
next011234

6、求第七位。第六位是a,next值为4,对应T[4]为b,a不等于b,b的next值为2,T[2]是b,a不等于b,b的next值为1,T[1]是a,a等于a,所以第七位next值为2 = 1+1

Tababaaababaa
next0112342

7、求第八位。第七位是a,next值为2,对应T[2]为b,a不等于b,b的next值为1,T[1]是a,a等于a,所以第八位next值为2 = 1+1

Tababaaababaa
next01123422

剩下的几个可以自己尝试一下,都是一样的套路,所以最后的结果就选择C。

nextval数组

       nextval数组是对next数组的进一步优化,特别是在处理有大量重复字符的字符串时效果明显,例如我们在求解字符串aaaabaab的next数组时,结果是01234123,而模式串的前四个字符相同,若在第四个字符模式串与主串失配,则会一次与前面三个进行比较,但结果肯定还是失配,因为这四个字符相同,第四个字符失配,模式串移动后必然会使前三个字符也失配,所以这样的比较毫无意义。

为了优化这个问题,我们引入了nextval数组,如果P[j] == P[next[j]](即当前字符与其next位置上的字符相同),则nextval[j]会尝试找到一个不同的字符位置,而不是简单地使用next[j]的值。这是通过递归地查找nextval[next[j]]来实现的,直到找到一个不同的字符或nextval值变为0。

nextval修正代码如下:

def compute_nextval(P):  
    """  
    计算字符串P的nextval数组  
    :param P: 字符串P  
    :return: nextval数组  
    """  
    m = len(P)  
    # 初始化next数组  
    next = [0] * m  
    # 初始化next数组的第一个元素为-1(或0,取决于具体实现,这里采用0)  
    next[0] = 0  
    # 计算next数组  
    k = -1  
    j = 0  
    while j < m - 1:  
        if k == -1 or P[j] == P[k]:  
            j += 1  
            k += 1  
            if P[j] != P[k]:  
                next[j] = k  
            else:  
                next[j] = next[k]  
        else:  
            k = next[k]  
  
    # 基于next数组计算nextval数组  
    nextval = [0] * m  
    nextval[:] = next[:]  # 初始化为next数组  
    j = 0  
    while j < m - 1:  
        if nextval[j] == 0:  
            j += 1  
        else:  
            if P[j] != P[nextval[j]]:  
                nextval[j] = nextval[j]  
            else:  
                nextval[j] = nextval[nextval[j]]  
            j += 1  
  
    return nextval  

如果需要我们手算时,我们来观察next数组,第一个字符的next值不变,从第二个字符开始,字符x回应的next值为next,观察字符T[next]是否与x相同,若相同,则将x的next值修改为字符T[next]对应的next值;否则x的next值不变。

例如:串‘ababaaababaa’的nextval数组值为?

我们上面已经求出他的next数组为0112342234456

Tababaaababaa
next011234223456
nextval0

第二个字符b的next值为1,对应T[1]是a,b不等于a,所以第二位的nextval值就是他的next值为1

Tababaaababaa
next011234223456
nextval01

同理,第三个字符a的next值为1,对应T[1]是a,a等于a,所以将第三位的nextval值改为0,以此类推,你先算一算,先别往下滑

所以最终的答案是010104210104

你算对了吗?❛‿˂̵✧

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值