KMP算法介绍

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。 

暴力解法

目标串:ABACABAB

模式串:ABAB  

利用表格的形式,阐释暴力解法的思路,每次取目标串的一个字符与模式串的字符进行对一一对比,只要相等,则比较下一位。

若不相等,则从目标串的下一次位置开始。 

目标串

A

B

A

C

A

B

A

B

 

第一次

A

B

A

B

 

 

 

 

×

第二次

 

A

B

A

B

 

 

 

×

第三次

 

 

A

B

A

B

 

 

×

第四次

 

 

 

A

B

A

B

 

×

第五次

 

 

 

 

A


A

B

 

代码如下,最容易让人理解的思路。但是空间复杂度为O(m*n).

def Brute_Force(target_str, model_str):
    """
    :param target_str:目标串 
    :param model_str: 模式串
    :return: 首次匹配位置
    """

    target_len = len(target_str)
    model_len = len(model_str)

    # 循环次数为目标串长度-模式串长度
    for i in range(target_len - model_len + 1):
        # 变量K的作用,遍历模式串 
        k = 0
        for j in range(i, i + model_len):
            # print("i", i, "j", j, "target_str", target_str[j], "model_str", model_str[k])
            # 不匹配
            if target_str[j] != model_str[k]:
                break
            else:
                k += 1
        else:
            return i

改变版,匹配,减少循环次数,只使用一次循环。

def Brute_Force_upgrade(target_str, model_str):
    target_len = len(target_str)
    model_len = len(model_str)
    # 循环用上面代码中循环一样 
    for i in range(target_len - model_len + 1):
        # 比较字符时候,取同模式串长度一样的目标串
        # 满足输出首次匹配位置
        if target_str[i:i + model_len] == model_str:
            return i

 普通求解的方式,思路容易理解,但是循环比较的次数比较多,每次都是从本次目标串循环的下一次位置,从新开始,这样会浪费许多的循环比较。

目标串:ABABBBAAABABABBA

模式串:ABABABB

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

 

主串

A

B

A

B

B

B

A

A

A

B

A

B

A

B

B

A

T/F

1

A

B

A

B

A

 

 

 

 

 

 

 

 

 

 

 

×

2

 

A

 

 

 

 

 

 

 

 

 

 

 

 

 

 

×

3

 

 

A

B

A

 

 

 

 

 

 

 

 

 

 

 

×

4

 

 

 

 

 

A

 

 

 

 

 

 

 

 

 

 

×

5

 

 

 

 

 

 

A

B

 

 

 

 

 

 

 

 

×

6

 

 

 

 

 

 

 

 

A

B

A

B

A

B

B

 


 分析此目标串和模式串:

第一次比较:比较到第五个位置,出错

第二次比较:比较到第二个位置,出错

第三次比较:比较到第五个位置,出错

......

第六次比较:成功匹配

 仔细比较目标串的第五个元素的前面四个元素,ABAB,有重叠的部分。并且都是在目标串的第五个位置匹配失败。

因此前边的几次比较都是无效的比较。

故,我们想优化比较的次数。

引入next数组。 

接下来讲解KMP算法

KMP算法的重点是求解next数组。

next数组手动求解

next数组是在模式串上寻找的一个数组,先给出模式串ABABABB的next数组。接下来详细介绍求解过程。

下标

1

2

3

4

5

6

7

模式串

A

B

A

B

A

B

B

next[t]

0

1

1

2

3

4

5

 next数组就是为了解决,目标串指针移动问题而引入的。

next数组,寻找模式串当前位置的重复子串。

不做过多的讲解,为什么要引入这个next数组,用最简单的方法,让你们学会怎么求解next数组。 

比较子串中左右两部分最长的重叠部分

(左部分记为:L【忽略子串的最后一个位置】, 右部分记为:R【忽略子串的第一个位置】) 

当下标=1时,初始化next数组,next[0]=0。
当下标=2时,对应的子串为A,L=null,R=null,因此没有重复的串内容(本身重复不属于),故长度L=0,next[2]=L+1=1。
当下标=3时,对应的子串未AB,L=A,R=B,因此没有重复的串内容,故长度L=0,next对应next[3]=L+1=1。
当下标=4时,对应的子串未ABA,L=AB,R=BA,重复子串为两端的A,故长度L=1,next对应next[3]=L+1=2。
当下标=5时,对应的子串未ABAB,L=ABA,R=BAB,重复子串为两端的AB,故长度L=2,next对应next[3]=L+1=3。
当下标=6时,对应的子串未ABABA,L=ABAB,R=BABA,重复子串为两端的ABA,故长度L=3,next对应next[3]=L+1=4。
当下标=7时,对应的子串未ABABAB,L=ABABA,R=BABAB,重复子串为两端的ABAB(本身重复不符合要求),故长度L=4,next对应next[3]=L+1=5。

 在来一个例子,加深理解。求解方式同上一样。

模式串:ABABBBAA

下标   i

1

2

3

4

5

6

7

8

模式串

A

B

A

B

B

B

A

A

next[t]

0

1

1

2

3

1

1

2

 

next数组代码求解

根据手动求解的思路,每次比较当前下标元素与t

def getNext(substrT):
    next_list = [0 for i in range(len(substrT))]
    # 初始化下标,j指向模式串
    # t记录位置,便于next数组赋值
    j = 1
    t = 0
    while j < len(substrT):
        # 第一次比较 t等于0 直接进行赋值,每次长度自增1
        # 之后进行的比较  判断字符是否相等, python数组下标从0开始,因为均-1
        if t == 0 or substrT[j - 1] == substrT[t - 1]:
            # 长度+1
            next_list[j] = t + 1
            j += 1
            t += 1
        else:
            # 此时的-1 同上边的-1操作是一样
            t = next_list[t - 1]
    return next_list

 理解代码最好的办法,就是手动的执行一遍这个流程。 

kmp算法的核心,next数组已经求解出来,只要在优化我们之前写的暴力解法,减少比较的次数。

def KMP(target_str, model_str):
    next = getNext(model_str)
    # 主串计数
    i = 0
    # 模式串计数
    j = 0
    while (i < len(target_str) and j < len(model_str)):
        if (target_str[i] == model_str[j]):
            i += 1
            j += 1
        elif j != 0:
            # 调用next数组
            j = next[j - 1]
        else:
            # 不匹配主串指针后移
            i += 1
    if j == len(model_str):
        return i - len(model_str)
    else:
        return -1

 重点在于next数组的求解,手动按照代码流程,写几个next数组,肯定有助于加深对KMP算法的理解。 

在求解next数据的时候 还要一些瑕疵,等待后续更新。

模式串:AAAAAAB

 

1

2

3

4

5

6

7

模式串

A

A

A

A

A

A

B

next[j]

0

1

2

3

4

5

6