KMP算法(python实现)

一.串的匹配问题

        有两个字符串,分别为文本串(主串)T和模式串(子串)P,问题要求是在文本串中找到模式串出现的位置。

二.朴素匹配算法

        暴力匹配,检查主串的每一个子串是否和目标子串相等。将主串的第一位和子串的第一位对齐,检查对应位置是否相同。如果遍历过程中发现某个位置匹配不上,则将子串的第一位与主串的第二位对齐(主串指针回溯到第二位,子串指针回溯到第一位),重新检查。后续操作相同,直至在主串中找到子串的位置。

        朴素模式的时间复杂度分析。设主串的长度为n,子串的长度为m,那么最好的情况就是第一次遍历就找到了子串,复杂度为O(m);最坏的情况在所有的n-m+1次检查中,都在最后一个字符才发现对不上,也就是O((n-m+1)m)=O(nm-m^2+m),由于一般都是m远小于n,所以时间复杂度近似于O(nm)。

        代码如下:

def strStr(self,haystack:str,needle:str)->int:
    for i in range(len(haystack)):
        if haystack[i:i+len(needle)]==needle:
            return i
    return -1


三.KMP匹配算法

        KMP算法分为两个部分,第一部分是求出子串的前缀表,第二部分是根据子串的前缀表移动子串指针和主串进行匹配。

(1)一些需要了解的概念

1.串的前缀和后缀

        前缀是不包括最后一个字符的子串。如abc的前缀为a、ab。

        后缀是不包括第一个字符的子串。如abc的后缀为bc、c。

2.最长相等前后缀

        在所有既是前缀又是后缀的子串中最长的子串,称为串的最长相等前后缀。

        如ababa的前缀有:a、ab、aba、abab;后缀有baba、aba、ba、a。那么a和aba都是ababa的相等前后缀,其中aba的长度较大,所以是ababa的最长相等前后缀。

3.前缀表

        串的前缀表的长度和串的长度相同。串str的前缀表next的第i个元素next[i]表示子串str[0:i]的最长相等前后缀的长度。

如ababa的前缀表为[0,0,1,2,3]:

i=0,子串a只有一个元素,没有前缀和后缀,所以next[0]=0;

i=1,子串ab前缀为a;后缀为b,没有相等前后缀,所以next[1]=0;

i=2,子串aba前缀为ab、b;后缀为ba、a,最长相等前后缀是a,所以next[2]=1;

i=3,子串abab前缀为a、ab、aba;后缀为bab、ab、b,最长相等前后缀是ab,所以next[3]=2;

i=4,子串ababa前缀为a、ab、aba、abab;后缀为baba、aba、ba、a,最长相等前后缀是aba,所以next[4]=3。

(2)前缀表在KMP算法中的作用

        在朴素模式匹配算法中,当发现匹配失败后,主串指针需要回溯到起始位置的后一位重新开始下一轮匹配,这就导致我们在当前这一轮失败的匹配中得到的关于主串的信息没有被很好的利用:

        如主串是abcabeabcbac,子串是abcabc,在第一轮匹配时其实已经知道主串的第二位是b,第三位是c,它们和子串的第一位a匹配不上,那么第二轮和第三轮中将子串的a和主串的b和c对齐的匹配其实完全没有必要,因为一定会失败。

        在KMP算法中,不需要回溯主串的指针,只需要根据前缀表让子串指针回溯到相应位置即可。因为如果匹配失败,那么失败的位置一定是在后缀之后的那个字符,只需要将子串指针回溯到前缀之后的那个字符就可以了。

        如在abcabeabcbac和abcabc的第一轮匹配失败后,主串指针指向e,子串指针指向第二个c。失败的位置就在abcab的后缀ab后面的字符c,由于abcab有最长相等前后缀ab,所以将子串指针回溯到前缀ab的后面,指向c,从这里开始下一轮的匹配即可。

image-20250308171458219

        具体回溯的公式是j=next[j-1]。在上面的例子中,失败时子串指针j=5,由于子串abcabc的前缀表是[0,0,0,1,2,3],所以将其回溯到next[j-1]=next[4]=2处,也就是指向第一个c,开始下一轮匹配。

image-20250308171522589

(3)如何求前缀表

        前缀表的第一个值next[0]=0是确定的,因为单个字符没有前后缀。下面的过程是已知next[i]=j时求next[i+1]。

        此时的字符串如下图所示。1和2是子串str[0:i]的最长前缀和最长后缀,它们的长度都是j,所以j就是指向前缀末尾的指针(不包括在前缀中)。

image-20250308164939726

        此时需要判断str[j]和str[i+1]是否相等,如果相等,那么子串str[0:i+1]的最长前缀和最长后缀就可以在str[0:i]的最长前缀和最长后缀的基础上往后再加上一位,就有next[i+1]=j+1。

        如果不相等,就需要将j指针回溯。由于next[j-1]代表的是str[0:j-1]的最长相等前后缀的长度,也就是下图中的3的长度,所以next[j-1]就是3末尾的指针。又由于1和2是str[0:i]的最长相等前后缀,3和4、5和6分别是1和2的最长相等前后缀,所以3和6是相等的。

image-20250308164912374

        那么要找到str[0:i+1]的最长相等前后缀,现在就需要判断str[next[j-1]]和str[i+1]是否相等,如果相等,那么str[0:i+1]的最长前缀就是3+str[next[j-1]],最长后缀就是6+str[i+1]。这个对比的操作和上面判断str[j]和str[i+1]相同,所以需要将指针j回溯为next[j-1]。

        如果str[next[j-1]]和str[i+1]还是不相等,那就继续回溯,直至相等或者j=0。

(4)KMP匹配算法的代码

        按照LeetCode第28题的要求写的:

def strStr(self,haystack:str,needle:str)->int:    
    #定义子串的前缀表
    next=[0]*len(needle)
    #定义求解子串前缀表的函数
    def getNext(next:List[int],s:str):
        #初始化前缀末尾指针j和前缀表第一个位置next[0]
        j=0
        next[0]=0
        #遍历子串
        for i in range(1,len(s)):
            #不相等时:循环回溯前缀末尾指针j
            while j>0 and s[j]!=s[i]:
                j=next[j-1]                
            #相等时:前缀指针j后移一位,给next[i]赋值(在后面赋值)
            if s[j]==s[i]:
                j+=1
            #赋值
            next[i]=j
            
    #求子串的前缀表
    getNext(next,needle)
    #主串和子串指针
    i,j=0,0
    #利用前缀表匹配主串和子串
    while i<len(haystack):
        #对应位置相等就往后移动,这里只是子串指针移动,主串指针移动在循环中
        if haystack[i]==needle[j]:
            j+=1
        #对应位置不相等就回溯子串的指针
        elif j>0:
            i-=1#保持主串指针在这一轮不变,避免错过主串当前字符
            j=next[j-1]
        #若子串指针走到了子串末尾说明在主串中找到了子串
        if j==len(needle):
            return i-j+1
        #主串指针往后移动一位
        i+=1
    #没有找到返回-1
    return -1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值