KMP算法分析--java

本文深入解析KMP算法,包括暴力匹配算法的局限性,KMP算法的原理、next数组的生成与应用,以及详细的代码实现。通过实例演示如何高效地进行字符串匹配。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

应用场景-字符串匹配问题:

字符串匹配问题:: 有一个字符串 str1= ""WWE QWERQW QWERQWERQWRT"",和一个子串 str2="QWERQWR" 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1。

暴力匹配算法:

如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:

  1. 如果当前字符匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个字符
  2. 如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
  3. 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。(不可行!)
  4. 暴力匹配算法实现.
package com.atguigu.kmp;

public class ViolenceMatch {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//测试暴力匹配算法
		String str1 = "WWE QWERQW QWERQWERQWRT";
		String str2 = "QWERQWR";
		int index = violenceMatch(str1, str2);
		System.out.println("index=" + index);

	}

	// 暴力匹配算法实现
	public static int violenceMatch(String str1, String str2) {
		char[] s1 = str1.toCharArray();
		char[] s2 = str2.toCharArray();

		int s1Len = s1.length;
		int s2Len = s2.length;

		int i = 0; // i索引指向s1
		int j = 0; // j索引指向s2
		while (i < s1Len && j < s2Len) {// 保证匹配时,不越界

			if(s1[i] == s2[j]) {//匹配ok
				i++;
				j++;
			} else { //没有匹配成功
				//如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。
				i = i - (j - 1);
				j = 0;
			}
		}
		
		//判断是否匹配成功
		if(j == s2Len) {
			return i - j;
		} else {
			return -1;
		}
	}

}

上面是暴力算法都是一步步的移动的,时间复杂度 

KMP算法 

其实大体KMP归结于:第一步找出公共部分子字符串前缀后缀公共部分,形成next数组,第二部根据next数组移动,其实所谓的移动就是移动子字符串首字母与比较的字符串里的对应字母位置这样跳转。

第一步是寻找前缀后缀最长公共元素长度:前缀不包含最后一个元素,后缀不包含第一个函数,运用高中知识集合来排列出来,

如果给定的模式串是:“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:

 也就是说我们找前缀和后缀公共部分有多少,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为

 因为模式串中首尾可能会有重复的字符,故可得出下述结论:失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值。

下面,咱们就结合之前的《最大长度表》和上述结论,进行字符串的匹配。如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:

1. 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:
 

2. 继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据《最大长度表》可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 - 2 = 4 位。
 

3. 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 - 0 =2 位。
 

4. A与空格失配,向右移动1 位。
 

5. 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 - 2 = 4 位。
 

6. 经历第5步后,发现匹配成功,过程结束。
 

    通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。

由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:

    而且,根据这个表可以得出下述结论

失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
    上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。

    给定字符串“ABCDABD”,可求得它的next 数组如下:

    把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。

    换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:

    根据最大长度表求出了next 数组后,从而有

失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值
    而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:

根据《最大长度表》,失配时,模式串向右移动的位数 = 已经匹配的字符数 - 失配字符的上一位字符的最大长度值
而根据《next 数组》,失配时,模式串向右移动的位数 = 失配字符的位置 - 失配字符对应的next 值
其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字符不计数),而失配字符对应的next 值 = 失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。
    所以,你可以把《最大长度表》看做是next 数组的雏形,甚至就把它当做next 数组也是可以的,区别不过是怎么用的问题。
代码如下:代码里也做了详细原理解释

/**
 * @author 江河
 * @date 2019-06-28 10:55
 */
public class KMPAlogrithm {

  public static void  main(String[] args) {
    KMPAlogrithm kmp = new KMPAlogrithm();
    String a = "QWERQWR";
    String b = "WWE QWERQW QWERQWERQWRT";
    int[] next = kmp.getNext(a.toCharArray());
    for(int i = 0; i < next.length; i++){
      System.out.println(a.charAt(i)+"    "+next[i]);
    }
    int res = kmp.indexOf(b, a);
    System.out.println(res);
  }
  public int indexOf(String source, String pattern) {
    int i = 0, j = 0;
    char[] src = source.toCharArray();
    char[] ptn = pattern.toCharArray();
    int sLen = src.length;
    int pLen = ptn.length;
    int[] next = getNext(ptn);
    while (i < sLen && j < pLen) {
      // 如果j = -1,或者当前字符匹配成功(src[i] = ptn[j]),都让i++,j++
      if (j == -1 || src[i] == ptn[j]) {
        i++;
        j++;
      } else {
        /**
         *  如果j!=-1且当前字符匹配失败,则令i不变,j=next[j],即让pattern模式串右移j-next[j]个单位
         *  也就是让j=next[j]的值对应当前的i,且next[j-1]前面的值对应到i前面几个匹配对应的
         *  例如:
         * source: WWE QWERQW QWERQWERQWRT
         * pattern:    QWERQWR
         *  这时候现在i=10对应source空格值,j=6对应R不等,来到else里j=next[6]=2
         *  pattern向右移动j-next[j]=4个单位即i=10对应的值为空格但是j值变成里next[2]=E如下
         *source: WWE QWERQW QWERQWERQWRT
         *pattern:        QWERQWR
         * 如上就对应上原理的讲解里,白话相当于如果不匹配就跳pattern的第一个元素Q往下移动。
         */

        j = next[j];

      }
    }
    if (j == pLen)
      return i - j;
    return -1;
  }

  /**
   * 下面是实现部分
   * @param p
   * @return
   */
  public int[] getNext(char[] p) {
    int pLen = p.length;
    int[] next = new int[pLen];
    int k = -1;
    int j = 0;
    next[0] = -1;// next数组中next[0]为-1
    while (j < pLen - 1) {
      if (k == -1 || p[j] == p[k]) {
        k++;
        j++;
        next[j] = k;
      } else {
        k = next[k];
      }
    }
    return next;
  }
}

 

这里博客是讲解最详细的,就是感觉太详细太多了 https://blog.youkuaiyun.com/v_july_v/article/details/7041827

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值