KMP算法超详解与其应用

KMP算法是由D.E.Knuth,J.H.Morris和V.R.Pratt三位大佬提出的一种改进的字符串匹配算法。

什么是字符串匹配呢?看下面的例子

假设我们有两个字符串,str1、str2
str1 = "ababababcbac"
str2 = "ababc"
求问:str2是否是str1的一个子串

类似上面这样的问题,就是我们常说的字符串匹配问题,这里需要注意一个点:子串(一定连续)和子序列(不一定连续)的区别!
这样的问题怎么解决呢?首先我们可以想到的就是使用暴力匹配的方法了,如下:

private static int KMP(String str1, String str2) {
	String str1 = "ababababcbac";
	String  str2 = "ababc";
	if (str1 == null || str2 == null) {
	           return -1;
	}
	if (str1.length() == 0 && str2.length() == 0) return 0;
	if (str1.length() == 0 || str2.length() == 0 || str1.length() < str2.length()) {
	           return -1;
	}
	int res = -1;
	for (int i = 0; i < str1.length(); i++) {
          int j = 0;
           int tmp = i;
           for (j = 0; j < str2.length(); j++) {
               if (str1.charAt(tmp) != str2.charAt(j)) {
                   break;
               }
               tmp++;
           }
           if (j == str2.length()) {
               res = tmp - j;
               break;
           }
     }
	 return res;
  }

当我们第一轮配的时候,会在下标为4的时候匹配失败;然后开始第二轮匹配,str1会从第二个字符开始与str2的第一个字符开始进行匹配,也就是每一次匹配失败,str1都只会后移一位,并且str2都要从头开始与str1进行匹配,不难看出这样的时间复杂度是O(N * M)。匹配过程(标红的地方表示匹配失败):
在这里插入图片描述

那么有没有什么可以优化的地方呢?
我们继续来看这个例子:
首先对于str2中的c这个字符来说,它前面的字符刚好可以分为ab、ab两个相等的子串,我们先叫前半部分为字符c的最长前缀,后半部分叫做字符c的最长后缀(最长前缀和最长后缀是完全一样的,前后只是为了区分一个从前往后,一个从后往前)。现在的问题是我们匹配到c的时候匹配不上了,按照暴力匹配的思想我们只能一位一位的往后移动并进行匹配。这样到我们能够匹配成功,需要移动str1的下标指针5次才行。其实我们可以只需要移动3次即可,即上面的步骤1、3、5。为什么呢?我们来看下面这张图:
在这里插入图片描述看完图片之后,又有新的疑问产生了,难道在i’-j之间不会存在一个位置能够使str2完全匹配上吗? 接着来看下面这张图片:
在这里插入图片描述
到这,可以看看返回去看看利用这种思路是不是只需要1,3,5就可以了。除此之外,我们也不难发现,KMP算法中最重要的就是上面重复出现的最长前缀和最长后缀了,那么我们怎么来维护这个最长前缀和最长后缀的信息呢?嘿嘿嘿,是不是觉得用个map不就好了,key是下标,value是最长前缀和最长后缀,感觉自己是个天才,哈哈哈~
但是,通过前面的匹配过程我们不难发现,其实不管是最长前缀还是最长后缀,我们实际上并没有用到具体的最长前缀和最长后缀的内容,仅仅是用到了它们的长度。所以呀,我们仅仅需要用一个数组来保存一下最长前缀和最长后缀的长度即可。我们称这个数组为next数组。

现在我们来看看这个next数组怎么求呢?

  1. 我们规定某一个字符的最长前缀不能包含最后一个字符(即某一个字符的前一个字符),最长后缀不能包含第一个字符,
  2. 规定第一个字符的next数组的值为-1,第二个字符的next数组的值为0
    下面我们以aaaaa,ababc两个字符串为例来看一下它们的next数组
aaaaa
[-1, 0, 1, 2, 3]
0位置   规定是 -1
1位置   规定是 0
2位置   前面为  aa  根据上面的第1条,前后缀都是字符a  ==>  所以next数组对应的值为1
3位置   前面为  aaa  同理根据第1条,前后缀都是aa  ==> next数组  2
4位置   前面为  aaaa  同理,前后缀都是 aaa  ==> next数组 3

ababc
[-1, 0, 0, 1, 2]

怎么来求next数组呢?
当我们要求i(i >= 2)位置的next数组的值的时候,我们可以根据i-1位置的next数组值来求,只需要判断i-1位置字符是否与i-1位置的最长前缀的后一个字符相等。

  • 如果相等,那么i位置的next数组的值就是i-1位置next数组的值加1,如下图:
    在这里插入图片描述
  • 如果不相等,那么我们直接跳到m位置(也就是i-1位置的next数组的值),接着比较i-1与m的最长前缀的下一个位置是否相等,如果相等,i位置的next数组的值就为m的next数组的值加1。那么,为什么直接就跳到m位置了呢?
    在这里插入图片描述
    首先要说明的是,这根蓝色的线没有实际意义,它会有很多种可能,可能刚好分割为红色和绿色,也可能红色和绿色中间还有字符,也可能红色和绿色是相交的。我们肯定要让红色区域和蓝色区域相等,又由于绿色和蓝色肯定是相等的,所以肯定就有红色和绿色是相等的。既然红色和绿色是相等的,那我们就可以推断出来红色和绿色其实就是m位置的最长前缀和最长后缀。所以我们接着比较m的最长前缀的后一个字符和i-1位置的字符是否相等。直到我们找到next数组的值为-1的时候就代表i位置不存在最长前缀和最长后缀。

KMP算法实现

public static int[] getNextArray(char[] arr) {
    if (arr.length == 1) {
        return new int[] {-1};
    }
    int[] next = new int[arr.length];
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    // 最长前缀值
    int cn = 0;
    while (i < arr.length) {
        if (arr[i-1] == arr[cn]) {
            // i-1位置的字符和i-1的最长前缀的下一个字符相同,那么i位置的最长前后缀就是i-1的+1;
            next[i++] = ++cn;
        } else if (cn > 0) {
            // 这就是比对不上的时候,往前跳
            cn = next[cn];
        } else {
            // 跳到不能再往前跳了
            next[i++] = 0;
        }
    }
    return next;
}


public static int getIndexOf(String s, String m) {
    if (s == null || m == null || s.length() < 1 || m.length() < 1) {
        return -1;
    }
    char[] str1 = s.toCharArray();
    char[] str2 = m.toCharArray();
    int i = 0;
    int j = 0;
    int[] nextArray = getNextArray(str2);
    while (i < str1.length && j < str2.length) {
        if (str1[i] == str2[j]) {
            i++;
            j++;
        } else if (nextArray[j] == -1) {
            // str2已经是第一个字符了,但是依旧匹配不上,所以只能str1的指针往后移一位
            i++;
        } else {
            j = nextArray[j];
        }
    }
    return j == str2.length ? i - j : -1;
}

KMP算法的应用:

  1. 给定一个原始串,要求使用这个原始串构成一个最短字符串,且这个最短字符串必须包含两个原始串,这两个原始串起始位置不能相同
    eg:abcabc =》 abcabcabc
    解法:求一个原始串的next数组,且多求一位,如:
    abcabc =》 [-1, 0, 0, 0, 0, 0, 3]
    第一次使用的后三位和第二次使用的前三位即重合,也就是拼出来的最短串

  2. 给定两棵树,判断第二棵树是否是第一棵数的一棵子树:
    将两棵树序列化为字符串
    t1 => str1
    t2 => str2
    null也要用特定的符号占位,这样是可以唯一确定一棵树的,以及一个节点结束之后也要用一个特殊符号标识
    如果str2是str1的一个子串就可以确定t2是t1的一棵子树
    在这里插入图片描述
    第二棵树是和黄色的框框住的子树一样的,而绿色的框是不对的,因为绿色的框还有一个右儿子节点没框进去,所以不是一棵完整的子树。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值