Python实现KMP算法

完全理解

在字符串匹配算法中,KMP算法之所以差不多可以做到O(N)的复杂度,关键就在于消除了主指针回溯,从而可以节省大量的时间。

例如想要对abcdabceabce进行匹配,那么暴力算法如下表所示,每次需要对比4个字符,总共对比5次。

a b c d a b c e
1 a b c e
2 a b c e
3 a b c e
4 a b c e
a b c e

然而我们一眼就能看出,这个d根本就不在abce里面,故若能存储一些其他信息,或许可以一下子跳过这个d

a b c d a b c e
1 a b c e
a b c e

但有的时候也不能多跳,比如想要从abcabcabc中匹配cab,比较好的方案大致如下

a b c a b c a b c
1 c a b
c a b

所以问题的关键在于,凭什么上面那个案例,可以直接跳过d,而下面这个案例,就只能不偏不倚地正好跳过两个。

小结一下发现两条规律,设txt为一个长文本,需要在txt中找到一个str,设当前比对的字符是ch,则有两条简单的规则

ch不在str中 str跳过这个ch
ch刚好是str[0] str调到这个ch的位置

那么接下来,如果ch在str中,但不是str[0],应该怎么考虑?

当然不能直接跳过,因为str中可能存在重复序列,比如从abababc中匹配ababc,那么最好的方案应该是

a b a b a b c
1 a b a b c
a b a b c

也就是说,对于ababc这样的串,由于a的位置不同,当我们得到一个新的a的时候,将要采取不同的决策。

扫描二维码关注公众号,回复: 13790101 查看本文章

在下图中,圆圈中表示当前的匹配情况,箭头表示一个新来的字符,箭头指向表示接下来将要跳转的位置。感叹号表示不是某个字符。

b
a
b
c
!a
!b
a
!a!c
a
ab
aba
abab
ababc

看到这里是不是有种恍然大明白的感觉,这就是所谓的状态机吧,而且这个状态机的构建过程也和txt毫无关系。换句话说,只要在匹配txt之前,对str做一下自我匹配,就可以得到一个这样的状态机。

而状态机中的状态,其实代表着将要匹配的字符串中的指针位置,退回到a意味着指针指向0;前进的时候指针加1;当abab接收一个a退回到aba这步时,意味着指针从5退回到3,如下表所示。

a ab aba abab ababc
a 0 0 0 3
b 0 0 0 0
c 0 0 0 0 成功
其他 0 0 0 0

是不是又恍然大悟了,所谓的状态机其实就是个矩阵。

接下来我们要做的就是生成这个状态矩阵。

粗略的实现

还是考虑从txt中找str,第一步建立str的状态矩阵

test = "ababcdabadc"    #python中str是关键字,所以改个名
length = len(test)
#创建用于存储状态的字典
status = {
    
    s:[0 for _ in range(length)] for s in set(test)}
for ch in status:
    for i in range(length):
        for j in range(i+1):
            if test[i-j:i]+ch == test[0:j+1]:
                status[ch][i] = j+1

得到

b [0, 2, 0, 4, 0, 0, 0, 8, 0, 4, 0]
d [0, 0, 0, 0, 0, 6, 0, 0, 0, 10, 0]
c [0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 11]
a [1, 1, 3, 1, 3, 1, 7, 1, 9, 1, 1]

对于字符串ababcdabadc而言,最开始指针为0,这个时候如果来了一个a,则指针跳转到a[0]=1,说明此时处于a的状态;这个时候再来一个b,则指针跳转到b[1]=2,说明此时处于ab的状态,以此类推。

在制作了str的状态矩阵之后,就可以非常方便地进行字符串比对了。

txt = "ababcdabasdcdasdfababcdabadc"
test = "ababcdabadc" 
KMP(txt,test)

def KMP(txt,test):
    status = setStatus(test)
    length = len(test)
    keySet = set(status.keys())
    match = []
    j = 0
    for i in range(len(txt)):
        s = txt[i]
        j = status[s][j] if s in keySet else 0
        if j==length:
            match.append(i-length+1)
    return match

def setStatus(test):
    length = len(test)
    #创建用于存储状态的字典
    status = {
    
    s:[0 for _ in range(length)] for s in set(test)}
    for ch in status:
        for i in range(length):
            for j in range(i+1):
                if test[i-j:i]+ch == test[0:j+1]:
                    status[ch][i] = j+1
    return status

匹配矩阵的优化

一般来说,除了丧心病狂的aaaaaaa这种字符串,其他字符串的匹配矩阵一般都很稀疏,这意味着我们进行了大量的无用比对。所以其匹配矩阵的求解过程可以粗略地优化一番,至少可以拿掉最外层的循环。

def setStatus(test):
    length = len(test)
    #创建用于存储状态的字典
    status = {
    
    s:[0 for _ in range(length)] for s in set(test)}
    for i in range(length):
        for j in range(i+1):
            if test[i-j:i] == test[0:j]:
                ch = test[j]
                status[ch][i] = j+1
    return status

现在就只剩下两层循环,看上去清爽一些,但不明真相的吃瓜群众还是很介意 O ( N 2 ) O(N^2) O(N2)的复杂度。为了更加清爽,再来考察一下匹配矩阵的特点

b [0, 2, 0, 4, 0, 0, 0, 8, 0, 4, 0]
d [0, 0, 0, 0, 0, 6, 0, 0, 0, 10, 0]
c [0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 11]
a [1, 1, 3, 1, 3, 1, 7, 1, 9, 1, 1]

首先,大部分非零值都是递增的。比如d中的非零值6,11c中的非零值5,11。而一旦出现了一个变小的值,那么这个值一定曾经出现过,比如b[9]=4,这个4就曾经出现过。

如果把索引降低,则索引必然重复作为一条原则,那么显然01也可以纳入这条原则,b,c,d中的0都在第一位出现过;而a中不存在0,因为a[0]=1,其最小值就只能是1。

或者说,a[i]要么为i+1,要么为曾经出现过的值。

既然如此,对于一个长度为N的字符串str,我可以从str中截取出前M个字符进行后向的匹配。例如"ababcdabadc",先匹配a,得到匹配成功的一组位置;然后再对这些位置匹配ab,以此类推,直到匹配位置为0位置。

猜你喜欢

转载自blog.csdn.net/m0_37816922/article/details/124063981
今日推荐