KMP算法

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

案例

假设主串(下文中我们称作T)为:a b a a c a b a b c a c
模式串(下文中我们称作W)为:a b a b c
目的是在T中找寻是否存在W。

传统的字符串匹配方法

一个字母一个字母进行暴力破解。例如第一次进行匹配,当匹配到第四个字幕时,发生了不匹配,则将W向右移动一位,继续这个操作,直到完全匹配。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个效率是极低的,每一次都是简单的往后移动一位。
最坏的情况下是当T为aaaaaab,W为aaab,这时每次操作都需要将aaa进行匹配一遍。

KMP算法

前期准备工作 构建prefix table

当我们使用KMP算法时,需要计算一个前缀表prefix table。
前缀是指除了最后一个字符以外,一个字符串的全部头部组合;
后缀 是指除了第一个字符以外,一个字符串的全部尾部组合。
所以ababc的前缀表如下:
在这里插入图片描述
然后我们把每一行都当成一个独立的字符串,寻找每个字符串的公共前后缀。
举例:拿abab这个字符串。这个字符串的长度为4。
在这里插入图片描述
这样abab这个字符串的公共前后缀长度为2,然后在前缀表中标明:
在这里插入图片描述
同理对于aba这个字符串,他的公共前后缀长度为1.
在这里插入图片描述
对于字符串ab来说,他的公共前后缀长度为0.
对于字符串a来说,他的公共前后缀长度也是0.
一般情况下,我们会在最前面补一个-1。这样我们就可以构建出一个前缀表:
在这里插入图片描述
我们这么写:
在这里插入图片描述
我们将W的每一个字符的索引也添加上:
在这里插入图片描述

利用前缀表匹配

当匹配到索引是3时:
在这里插入图片描述
这时出现了不匹配的现象,那么在匹配失败的情况下,查看对应的前缀表值是1,表示将索引是1的元素与当前匹配失败的元素对齐:
在这里插入图片描述
对齐调整后,仍然匹配失败,继续调整
在这里插入图片描述
在这里插入图片描述
然后我们继续移动:
在这里插入图片描述
然后继续移动,因为a对应的前缀表是-1,遇到-1时,直接右移一位,不需要匹配。
在这里插入图片描述
直到完全匹配上。

当T中不只有P一个匹配时

也就是说说把T中所有P找出来,就需要继续往后移动。也就是说,将最后一个前缀表对应的索引值进行移动:
在这里插入图片描述
然后继续往后匹配,发现已经到头了。

这样就找到了一段完全匹配的字符串。

极端情况

T:
如果使用暴力匹配则会做很多无用功。
如果使用KMP时,构建索引表:
在这里插入图片描述
然后进行匹配:
在这里插入图片描述
当发生匹配失败时,找到对应的前缀表的索引3,将索引3移动到失败位置:
在这里插入图片描述
当移动到指定位置时,就不需要比较之前的元素了,直接从匹配失败的元素开始往后比较。
在这里插入图片描述
这样就成功了。
这里最重要的步骤就是构建前缀表。

KMP算法代码实现

上面我们介绍了KMP算法的原理,主要分为两部分,第一部分是构建生成Prefix Table,第二部分是根据Prefix Table完成搜索。

案例

匹配串:A B A B C A B A A
Prefix Table如下:
在这里插入图片描述
我们可以发现一个规律:
当第七行的最长公共前后缀长度是1时,如果要将第八行的公共前后缀变为2,那么就一定要在第八行A的最后面添加B,判断依据是第七行A后面的B;
而第八行的最长公共前后缀长度是2时,如果要将第九行的公共前后缀变为3,那么就一定要在第九行最后面添加A,判断依据是第八行AB后面的A。
假设:在这里插入图片描述
已经知道第5位的公共前后缀长度是1,那么如何判断第6位是多少?
答案:因为第5位的公共前后缀长度是1,那么判断第6位的字母是不是B,如果是B,那么第6位的公共前后缀长度是2
在这里插入图片描述
但是, 如果不相等时,如上图,也就是说如何根据第3位的公共前后缀来判断第4位的公共前后缀呢?错误做法: 是将第3位的公共前后缀长度2,A与C不相等,则C的公共前后缀长度为0,虽然结果是对的,但是如何根据第7位的公共前后缀判断第8位的公共前后缀呢?
在这里插入图片描述
如果按照上面的做法,B与A不相等,所以第8位是0,答案是不对的,应该是1。
整个过程如下所示:
在这里插入图片描述
构建前缀表的代码如下:

public class KMP {

    public static void prefix_table(char pattern[], int prefix[], int n){
        prefix[0] = 0;
        int i = 1;
        int length = 0; //最长公共前后缀长度
        while(i < n) {
//            System.out.println("pattern[i]:" + pattern[i] + ",pattern[length]:" + pattern[length]+"," + (pattern[i] == pattern[length]));
            if(pattern[i] == pattern[length]){
                length ++;
                prefix[i] = length;
                i++;
            }else {
                if(length>0){
                    length = prefix[length-1];
                }else {
                    prefix[i] = 0;
                    i++;
                }
            }
        }
    }

    public static void main(String[] args) {
        String str = "ABABCABAA";
        char[] pattern = str.toCharArray();
        int[] prefix_table = new int[9];
        int n = 9;
        prefix_table(pattern, prefix_table, n);
        for(int i:prefix_table){
            System.out.print(i);
        }
    }
}

程序打印输出结果为:001201231
构建完成前缀表之后,为了方便后面的匹配过程,把结果进行移位:

    //为了方便后面的匹配,对前缀表进行移位
    static void move_prefix_table(int[] prefix, int n) {
        for (int i = n - 1; i > 0; i--) {
            prefix[i] = prefix[i - 1];
        }
        prefix[0] = -1;
    }

规定:n表示pattern的长度,m表示text原始字符串的长度。i表示text当前匹配位置,j表示pattern当前匹配位置。
在这里插入图片描述
在这里插入图片描述
当匹配成功之后,如下图,需要继续对P[j]进行prefix操作,
在这里插入图片描述
在这里插入图片描述
完整代码如下:

public class KMP {

    public static void prefix_table(char pattern[], int prefix[], int n) {
        prefix[0] = 0;
        int i = 1;
        int length = 0; //最长公共前后缀长度
        while (i < n) {
//            System.out.println("pattern[i]:" + pattern[i] + ",pattern[length]:" + pattern[length]+"," + (pattern[i] == pattern[length]));
            if (pattern[i] == pattern[length]) {
                length++;
                prefix[i] = length;
                i++;
            } else {
                if (length > 0) {
                    length = prefix[length - 1];
                } else {
                    prefix[i] = 0;
                    i++;
                }
            }
        }
    }

    //为了方便后面的匹配,对前缀表进行移位
    static void move_prefix_table(int[] prefix, int n) {
        for (int i = n - 1; i > 0; i--) {
            prefix[i] = prefix[i - 1];
        }
        prefix[0] = -1;
    }

    static void kmp_search(char[] text, char[] pattern) {
        int n = pattern.length;
        int m = text.length;
        int[] prefix = new int[n];
        prefix_table(pattern, prefix, n);
        move_prefix_table(prefix, n);
        int i = 0;
        int j = 0;

        // text[i],    len(text)    = m
        // pattern[j], len(pattern) = n
        while (i < m) {

            if(j == n-1 && text[i] == pattern[j]){
                System.out.println("Found pattern at " + (i-j));
                j = prefix[j];
            }

            if (text[i] == pattern[j]) {
                i++;
                j++;
            } else {
                j = prefix[j];
                if (j == -1) {
                    j++;
                    i++;
                }
            }
        }

    }

    public static void main(String[] args) {
        String pattern_str = "ABABCABAA";
        String text_str = "ABABABCABAABABABAB";
        char[] pattern = pattern_str.toCharArray();
        char[] text = text_str.toCharArray();
        /*int[] prefix_table = new int[9];
        int n = 9;

        prefix_table(pattern, prefix_table, n);
        move_prefix_table(prefix_table, n);
        for (int i : prefix_table) {
            System.out.print(i);
        }*/
        kmp_search(text, pattern);

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值