首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >KMP字符串匹配

KMP字符串匹配

作者头像
一个架构师
发布于 2022-06-20 19:39:06
发布于 2022-06-20 19:39:06
1.5K0
举报

假设我们有这样一个要求,一个字符串S,一个匹配字符串P,我们想知道匹配串P是否被包含在字符串S中,如果包含那它在S的什么位置上?

解决这个问题最简单的方法就是暴力匹配,匹配串中的一个元素匹配到了,就接着向后匹配,如果匹配失败,就整体后移一位,再重新匹配.

举个例子,按如下2个字符串要求我们看下匹配过程是怎么样的.

字符串S: DABABCABABCABDB

匹配串P: ABCABD

匹配过程,如表格所示:

可见匹配过程中,字符串S的指针会不仅会右移,还会左7移,如第3次匹配过程;

整体匹配次数大致是n*m,其中n和m分别是字符串S和匹配串P的长度,时间复杂度也就是O(n*m),那有没有更好的方式去完成匹配呢?

仔细观察匹配过程,我们发现第4行匹配过程中,当匹配到D时,虽然不匹配成功,但是我们是可以知道[D]元素前面的2个元素是[AB]和匹配串的前缀字符[AB]是匹配的,那我们之间比较第3个元素[C]就可以了,就可以减少2次比较运算和字符串S指针的回溯.

也就是说在某一元素匹配不成功后,直接判断下一指定索引元素就可以达到目的,那存储这一指定元素位置的索引,我们称之为next[] 数组.

按这样的思路D.E.Knuth,J.H.Morris和V.R.Pratt 三人一起提出了一种新的算法,也就是著名的KMP算法.KMP算法的主要思想是:

1. 字符串指针不回溯

2. 当不匹配时,匹配串尽量少回溯,确定向右最大滑行距离

为达到这样的效果,要怎么做呢?

这里有必要介绍两个概念:真前缀和真后缀

字符串: book

真前缀:b, bo, boo

真后缀:k, ok, ook

还有一个记录偏移量的数组next[]

1. i=0, 固定值为 -1

2. i=1, 索引i对应真前缀A, 最长相同真前缀和真后缀长度为 0

3. i=2, 索引i对应真前缀AB, 最长相同真前缀和真后缀长度为 0

4. i=3, 索引i对应真前缀ABC, 最长相同真前缀和真后缀长度为 0

5. i=4, 索引i对应真前缀ABCA, 最长相同真前缀和真后缀长度为 1

6. i=5, 索引i对应真前缀ABCAB, 最长相同真前缀和真后缀长度为 2

7. i=6, 这里表示匹配结尾,没有实际意义

那我们重新来看下使用了KMP算法的匹配过程

1. A与D匹配失败,A对应next[0]值为-1,匹配串P整体后移一位,重新与字符串S匹配.

2. C与A匹配失败,C对应next[2]值为0,匹配串P将索引位置0的元素[A]移到不匹配位置,其他元素也随之移动,重新开始匹配

3. D与A匹配失败,D对应next[5]值为2,匹配串P将索引位置2的元素[C]移到不匹配位置,其他元素也随之移动,重新开始匹配

4. C与A匹配失败,C对应next[2]值为0,匹配串P将索引位置0的元素[A]移到不匹配位置,其他元素也随之移动,重新开始匹配

5. 匹配成功

总结一下,通过辅助数组next[],确定整体匹配过程中,匹配串P的某个元素该与字符串S匹配,避免字符串S的指针回溯;

利用辅助数组next[],确定匹配失败时,后续匹配串该如何移动和重新比较,减少比较次数;

时间复杂度为O(n+m),其中n和m分别是字符串S和匹配串P的长度.

留个疑问,注意观察第4行匹配过程,看看这个next算法是否就是最合理的呢?我们下一节在来讲这个next算法的优化.

注意: 真前缀和前缀的区别,真后缀和后缀的区别,

真前缀是不包括自身的,真后缀也是同理;

还是以字符book为例,

真前缀: b ,bo, boo

前缀: b ,bo, boo, book

真后缀:k, ok, ook

后缀:k, ok, ook, book

next方法:

代码语言:javascript
复制
    private static int[] getNext(char[] p) {
        int[] next = new int[p.length];
        next[0] = -1;
        int k = -1;
        int j = 0;
        while (j < p.length - 1) {
            //p[k]表示前缀,p[j]表示后缀
            if (k == -1 || p[j] == p[k]) {
                k++;
                j++;
                next[j] = k;
            } else {
                k = next[k];
            }
            System.out.println("in-next:" + Arrays.toString(next));
        }
        return next;
    }

kmpSearch方法:

代码语言:javascript
复制
 private static int kmpSearch(String source, String target) {
        int i = 0;
        int j = 0;
        int sLen = source.length();
        int pLen = target.length();
        char[] s = source.toCharArray();
        char[] p = target.toCharArray();
        int[] next = getNext(source.toCharArray());
        while (i < sLen && j < pLen) {
            //如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
            if (j == -1 || s[i] == p[j]) {
                i++;
                j++;
            } else {
                //如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
                //next[j]即为j所对应的next值
                j = next[j];
            }
        }
        if (j == pLen) {
            return i - j;
        } else {
            return -1;
        }
    }

main方法:

代码语言:javascript
复制
   public static void main(String args[]) {
        String str = "DABABCABABCABDB";
        System.out.println(Arrays.toString(getNext("ABCABD".toCharArray())));
        System.out.println(kmSearch(str, "ABCABD"));
    }
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档