KMP算法(Python实现)--从不懂到秒懂

KMP算法

1.普通比较算法

首先我们先来了解普通的比较算法
KMP算法(Python实现)--从不懂到秒懂_第1张图片
从左到右一个一个匹配,先从第一位比较,能完全匹配则返回匹配位置,反之子串向右移动一位,继续匹配,直到匹配主串结束。
如下图:KMP算法(Python实现)--从不懂到秒懂_第2张图片
根据以上可以得到以下代码(暴力匹配,逻辑简单):

/**
 * 暴力破解法
 * @param ts 主串
 * @param ps 模式串
 * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1
 */
public static int bf(String ts, String ps) {
    char[] t = ts.toCharArray();
    char[] p = ps.toCharArray();
    int i = 0; // 主串的位置
    int j = 0; // 模式串的位置
    while (i < t.length && j < p.length) {
       if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
           i++;
           j++;
       } else {
           i = i - j + 1; // 一旦不匹配,i后退
           j = 0; // j归0
       }
    }
    if (j == p.length) {
       return i - j;
    } else {
       return -1;
    }
}

这种方法太过于笨拙,我们可以看到明明第二位已经匹配,而且和第一位明显不同,但是计算机还是要匹配一次,这就造成了计算复杂度的提升,所有聪明的人研究出了KMP算法,来避免重复匹配。
== 暴力破解 的时间难度是O(n*m)==

2.KMP算法

参考下图

KMP算法(Python实现)--从不懂到秒懂_第3张图片
核心思维:利用已经部分匹配这个信息,保持i指针不变,通过修改J指针,让模式串移动到有效位置。换句话说,在某个字符与主串不匹配的时候,我们要知道指针移动到哪里
KMP算法(Python实现)--从不懂到秒懂_第4张图片
我们可以看到C和B不匹配了,这时候我们该怎么做?
KMP算法(Python实现)--从不懂到秒懂_第5张图片
我们可以直接将 j 指针移动到第二位,因为我们可以明显看到前两位都是一样的,以上可得:最前面的 K 个字符和之前的最后 K 个字符是一样的
P[0~K-1] == P[j-k ~ j-1]
PS:我在此将已经匹配的 K 个字符可以看做最大重复前缀
综上所述,我们需要一个新的指标来指示我们该移动到哪个有效位置,所以就推出NEXT数组,他的作用是可以指定在失配(匹配失败)之后, j 指标该移动的位置

以下是公式验证前缀相同的证明:
(可看可不看,因为图片上肉眼一眼就能看出来)
因为:
当T[i] != P[j]时
有T[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1]
必然:T[i-k ~ i-1] == P[0 ~ k-1]
我们可以看出,在失配之后,直接将j 移动到 k 值,就可以获得有效匹配。
使用next数组表示:
next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置
KMP算法(Python实现)--从不懂到秒懂_第6张图片
例如图上,已经匹配成功A、B、A三位,但是B和C没有匹配成功,这时候引进next[k]=j
让子串从P[next[k] = j ] 开始匹配,子串继续从i= 0开始匹配,匹配上则继续,反之则把j+1赋给next[k] ,然后重复以上操作,直至最后母串所有字符结束一次匹配。
时间复杂度O(n+m)

比较容易理解的一种方法,最大前/后缀
首先简单说下前后缀的概念,

"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;
"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

KMP算法(Python实现)--从不懂到秒懂_第7张图片
部分匹配的实质是,有时候,字符串头部和尾部会有重复,比如,“ABCDAB”之中有两个“AB”,它的部分匹配值就是2,(”AB”的长度)搜索词移动的时候,第一个“AB”向后移动四位(字符串长度-部分匹配值),就到了第二个匹配“AB”的位置
下附代码:


#coding=utf-8

def kmp(mom_string, son_string):
    # 传入一个母串和一个子串
    # 返回子串匹配上的第一个位置,若没有匹配上返回-1
    test = ''
    if type(mom_string) != type(test) or type(son_string) != type(test):
        return -1
    if len(son_string) == 0:
        return 0
    if len(mom_string) == 0:
        return -1
    # 求next数组
    next = [-1] * len(son_string)
    if len(son_string) > 1:  # 这里加if是怕列表越界
        next[1] = -1
        i, j = 1, 0
        while i < len(son_string) - 1:  # 这里一定要-1,不然会像例子中出现next[8]会越界的
            if j == -1 or son_string[i] == son_string[j]:
                i += 1
                j += 1
                next[i] = j
            else:
                j = next[j]

    # kmp框架
    m = s = 0  # 母指针和子指针初始化为0
    while (s < len(son_string) and m < len(mom_string)):
        # 匹配成功,或者遍历完母串匹配失败退出
        if s == -1 or mom_string[m] == son_string[s]:
            m += 1
            s += 1
        else:
            s = next[s]

    if s == len(son_string):  # 匹配成功
        return m - s
    # 匹配失败
    return -1


# 测试
mom_string = 'abababaababcad'
son_string = 'aababcad'

print(kmp(mom_string,son_string))

我感觉应该可以把next数组做到kmp内层,就是不知道效率如何,欢迎大佬指点。

你可能感兴趣的:(算法探究)