串之KMP模式匹配算法笔记

这应该算是《大话数据结构》这本书看到现在第一个需要想想的算法,准备认真的整理整理思路,不能一开始就掉队……

KMP的目标

先借用《大话数据结构》书中图片:
朴素模式匹配过程
首先定义长度长的字符串为主串 S[ ],需匹配的为 T[ ]。图中1-6是朴素模式匹配的步骤,但是可以发现,第1步之后第2步中的比较其实是没有必要的,因为在第1步后可以得出 T [ 0 ] ≠ T [ 1 ] a n d S [ 1 ] = T [ 1 ] ⇒ T [ 0 ] ≠ S [ 1 ] T[0]\ne T[1] and S[1] = T[1] \Rightarrow T[0] \ne S[1] T[0]=T[1]andS[1]=T[1]T[0]=S[1],也就是说第2步其实没有必要进行。同理,第3、4、5步也是如此,这也就是说假设在匹配前对 T[] 进行处理,则可以避免朴素模式匹配的2-5步。
这也是书中所说的”KMP模式匹配算法就是为了让这没有必要的回溯不发生“。这时候问题便集中在如何告诉在 T[] 匹配不成功后应该以哪一个元素再和 S[] 进行比较,也就是 T[] 指针下一位该指到哪里?
KMP要解决的问题

NEXT数组

T [ j ] → N e x t T[j]\rightarrow Next T[j]Next 和 S[] 没有关系,因为这是当一元素匹配错误时T指针之后的指向,下图是几种不同的T串形式。
在这里插入图片描述
可以看出NEXT取决于当前字符之前的串的前后串之间的关系和当前字符无关,可以得到:
在这里插入图片描述
书中之后举例来归纳规律,其实和我的灵魂画相类似,由此便可以得出next数组代码:

def get_next(T:str) -> list:
    i,j = 1, 0            # 双指针,i > j 
    L = len(T)
    Next = [0] * L
    while i < L - 1:
        if T[i] == T[j]:  # 相等的时候i+1 的next就是 j+1,一定注意0-j此时该条件都满足
            i += 1
            j += 1
            Next[i] = j   # 因为是当前字符之前串的关系,所以在判断字符间关系后两个指针同时移动然后+1
        elif j != 0:
            j = Next[j]   # 如果字符不同,则j进行回溯(好好想想为什么**)
        else:
            i += 1        # 假如j回溯到初始位置字符仍不相等,则i移动
    return Next

为什么回溯呢?
假设 T [ i ] ≠ T [ j ] T[i] \ne T[j] T[i]=T[j] 时有 T [ i − k ] = T [ l − k ] ( l < j , k = 0 , 1 , . . . , l ) T[i-k] = T[l-k] (l <j,k=0,1,...,l) T[ik]=T[lk](l<jk=0,1,...,l)
因为 j ≠ 0 j \ne 0 j=0 ,且$ j += 1$ 执行条件为 T [ i ] = T [ j ] T[i] = T[j] T[i]=T[j] T [ i − k ] = T [ j − k ] ( k = 1 , 2 , . . . , j ) T[ i-k] = T[j - k](k = 1,2,...,j) T[ik]=T[jk](k=1,2,...,j)
必然有 T [ i − k ] = T [ j − k ] ( k = 1 , 2 , . . . , l ) T[i-k]=T[j-k](k=1,2,...,l) T[ik]=T[jk](k=1,2,...,l)
由此可得: T [ l − k ] = T [ j − k ] ( k = 1 , 2 , . . . , l ) T[l-k]=T[j-k](k = 1,2,...,l) T[lk]=T[jk](k=1,2,...,l)
所以 l = N e x t ( j ) l=Next(j) l=Next(j)
在这里插入图片描述
模式匹配主函数代码:

def index_KMT(S:str,T:str)->int:
    Next = get_next(T)
    i_max, j_max = len(Next), len(S)
    i, j = 0, 0
    while j < j_max and i < i_max:  # i == i_max说明匹配成功,j == j_max说明S串中没有T串
        if T[i] == S[j]:
            i += 1
            j += 1
        elif i == 0:    # 两字符串不相等且T串指针指向首位,移动S串
            j += 1
        else:
            i = Next[i]    # 两字符串不相等,T串回溯
    if i == i_max:
        return j - i_max
    else:
        return -1

从上面的代码中可以看出来,KMP只有当模式和主串之间存在许多“部分匹配”时候才会具有优势,否则返回结果和朴素模式匹配没有差距。

KMP的改进

KMP算法实际上也是有缺陷的,从书中图片中可以明显看出来:
在这里插入图片描述
然后书上讲的改良对我来说太难了……然后就自己想想,为什么会出现还有没有必要的步骤呢?其实是步骤1中 j = 5 j=5 j=5 时的判断结果没有利用,之前的KMP都不会去利用当前字符的判断结果,所以说改进也应该是在这个当中做文章。

def get_next(T:str) -> list:
    i,j = 1, 0
    L = len(T)
    Next = [0] * L
    while i < L - 1:
        if T[i] == T[j]:
            i += 1
            j += 1
            ##########改良############
            if T[i] == T[j]:  
                Next[i] = Next[j]
            else:
                Next[i] = j
            ##########################
        elif j != 0:
            j = Next[j]
        else:
            i += 1
    return Next

在上述代码中,在 T [ i ] = T [ j ] T[i] = T[j] T[i]=T[j] 满足后看 T [ i + 1 ] = T [ j + 1 ] T[i+1] = T[j+1] T[i+1]=T[j+1] 是否满足,满足的话说明在当前字符判断不匹配后随之 n e x t [ i ] next[i] next[i] 也不匹配,此时便赋值 N e x t [ i ] = N e x t [ j ] Next[i] = Next[j] Next[i]=Next[j] ,因为该循环为不断迭代,故也就不需要进行类似$ Next[i] = Next[Next[j]]?$ 这样的判断了。以 T = ′ a b a b a a a b a ′ T = 'ababaaaba' T=ababaaaba 为例,原版输出结果为 [0, 0, 0, 1, 2, 3, 1, 1, 2] 改良输出结果为 [0, 0, 0, 0, 0, 3, 1, 0, 0] ,从中可以明显看出二者区别。

总结

这个算法为什么会这样思考呢?
朴素模式匹配中没有利用任何之前进行的匹配,所以计算过程很繁琐,如何避免呢?一定是想办法避免重复匹配,这里有点像最小栈,需要一个辅助数列来避免重复的比较。
而**为什么只和T串有关呢?**因为在整个匹配过程中,我们只能知道的是等于或者不等于,我们是无法获得S串的具体值的。
此时当知道我们需要为T串加入辅助列表时,后面很多就可以观察到了。这里面比较有意思的是实际上next数组的寻找用到了双指针,而在链表中像链表倒数第k个节点环形链表等等也都用到了双指针,他们有什么共同点吗?
单链链表中由于无法获得前节点,所以利用双指针来获得链表节点的位置关系;同样,在next数组构建中假如直接对某一字符进行判断,获取向前多少字符是重复的很低效,在迭代过程中判断时双指针可以获得串尾和串头之间关系也就能对字符串重复进行判断了。这或许就是一个比较牵强的解释吧?

KMP算法是一种字符串匹配算法,用于在一个文本S内查找一个模式P的出现位置。它的时间复杂度为O(n+m),其中n为文本的长度,m为模式的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本为S="ababababca",模式为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值