字符串匹配之朴素算法(暴力匹配)与KMP算法及如何求next数组(动画示例)(Java)

一、朴素算法

    朴素算法就是最简单的暴力匹配算法,其步骤如下:

  1. 用两个指针 ij 分别指向主串和模式串的第一个字符。
  2. 如果当前主串和模式串的字符相等,则两个指针后移,i ++,j ++
  3. 如果当前主串和模式串的字符不相等,则 i 回到这一轮开始比较的字符的下一个字符 i - j + 1j 则归零 j = 0,指向模式串的第一个字符。
  4. 如果最终 j 等于模式串的长度,则返回主串第一次出现模式串的位置 i - j,否则返回 -1

    假设主串的长度为 m,模式串的长度为 n,则该算法的时间复杂度是 O(mn)

暴力匹配

	public static int violentSearch(String text,String pattern) {
    
    
		int i=0,j=0;
		while(i<text.length() && j<pattern.length()) {
    
    
			if(text.charAt(i)==pattern.charAt(j)) {
    
    
				i++;
				j++;   //两个字符串暂时相等,两个索引加一继续比较
			}else {
    
    
				i=i-j+1;   //不能直接加一
				j=0;   //两个字符串出现不相等的字符,模式串索引归零,主串索引指向原地方下一个字符
			}
		}
		if(j==pattern.length()) {
    
    
			return i-j;   //主串与模式串相等,返回B串第一次在A串出现的地方
		}
		return -1;
	}

二、KMP算法

    首先,用下面这个例子演示一下KMP算法是怎么工作的,如果看不懂可以先往下看。
KMP算法演示1
    在这里先解释一下什么是字符串的前缀和后缀。
    存在字符串A和B,如果A=BS,其中S是任意的非空字符串,那就称B为A的前缀。
    存在字符串A和B,如果A=SB,其中S是任意的非空字符串,那就称B为A的后缀。
    以“abababca”为例:

  1. a”的前缀和后缀都为空集,最大公共前后缀长度为0
  2. ab”的前缀为[a],后缀为[b],最大公共前后缀长度为0
  3. aba”的前缀为[a,ab],后缀为[ba,a],最大公共前后缀为“a”,长度为1
  4. abab”的前缀为[a,ab,aba],后缀为[bab,ab,b],最大公共前后缀为“ab”,长度为2
  5. ababa”的前缀为[a,ab,aba,abab],后缀为[baba,aba,ba,a],最大公共前后缀为“aba”,长度为3
  6. ababab”的前缀为[a,ab,aba,abab,ababa],后缀为[babab,abab,bab,ab,b],最大公共前后缀为“abab”,长度为4
  7. abababc”的前缀为[a,ab,aba,abab,ababa,ababab],后缀为[bababc,ababc,babc,abc,bc,c],最大公共前后缀长度为0
  8. abababca”的前缀为[a,ab,aba,abab,ababa,ababab,abababc],后缀为[bababca,ababca,babca,abca,bca,ca,a],最大公共前后缀为“a”,长度为1。

    这时可以引入KMP算法的核心部分,一个被称为部分匹配表(Partial Match Table)的数组,在使用KMP算法进行匹配的时候我们需要用这个表来减少匹配的次数,我们可以将它命名为next数组。其实,“abababca”的next数组的值就是[0,0,1,2,3,4,0,1]。如果有 next [ j ]=k ,则表示0~j这个字符串的最大公共前后缀长度为k。例如,next [5]=4,“ababab”的最大公共前后缀长度为4。
    如果用上面的方法将字符串的前后缀都写出来,肯定可以将一个字符串的部分匹配表也写出来,但是有点麻烦。那么现在我们用其他方法来求一下“abababca”的next数组。

  1. 建立一个next数组,长度等于模式串的长度,同时让next[0]=0。建立两个指针 ij,分别指向模式串pattern的第二个字符和第一个字符。(实际上 ij 指向同一个字符串i指向子串的后缀,j指向子串的前缀。但是我们可以看做是两个字符串在匹配。)
    在这里插入图片描述
  2. 此时 i=1j=0pattern[ i ] != pattern[ j ],失配。由于 j=0,不能再向左移了,所以将next[1]=0。保持 j 不动,i ++,相当于下面的字符串向后移一位。
    在这里插入图片描述
    在这里插入图片描述
  3. 此时 i=2j=0pattern[ i ] == pattern[ j ],匹配,将next[2]=1(这个1表示子串“aba”的最大公共前后缀为“a”,长度为1)。i ++j ++
    在这里插入图片描述
  4. 此时 i=3j=1pattern[ i ] == pattern[ j ],匹配,将next[3]=2(这个2表示子串“abab”的最大公共前后缀为“ab”,长度为2)。i ++j ++
    在这里插入图片描述
  5. 此时 i=4j=2pattern[ i ] == pattern[ j ],匹配,将next[4]=3(这个3表示子串“ababa”的最大公共前后缀为“aba”,长度为3)。i ++j ++
    在这里插入图片描述
  6. 此时 i=5j=3pattern[ i ] == pattern[ j ],匹配,将next[5]=4(这个4表示子串“ababab”的最大公共前后缀为“abab”,长度为4)。i ++j ++
    在这里插入图片描述
  7. 此时 i=6j=4pattern[ i ] != pattern[ j ],失配。注意这个时候 j!=0j 左边0 ~ j-1的子串必定等于 i-j ~ i-1的子串(如果两个子串不相同,j 肯定就来不了现在这个位置了。也就是说,如果 j!=0,那么0 ~ j-1的子串必定等于 i-j ~ i-1的子串)。我们看到 j 前面的索引是3,next[3]=2,那么“abab”的最大公共前后缀为“ab”(ab是前缀也是后缀),长度为2。换句话说,由于本次比较已经将0 ~ j-1的子串和 i-j ~ i-1的子串进行了比较,而0 ~ j-1的子串等于 i-j ~ i-1的子串,同时,它们有最大公共前后缀“ab”。那么在下次比较中,就可以跳过最大公共前后缀“ab”(上面那个子串是后缀(下标4、5),下面那个子串是前缀(下标0、1)。即不用比较最大公共前后缀“ab”,因为它们已经比较过了,它们是相等的),让c(下标为6)与a(下标为2)比较。j=next[j-1](赋值前j=4,赋值后j=2),即 j 前移到2。(简单的说,j 跳到最大公共前缀的下一个字符)(个人的理解是,这就是为什么代码里j=next[j-1]要怎么写的原因)
    在这里插入图片描述
  8. 此时 i=6j=2pattern[ i ] != pattern[ j ],失配。显然,0 ~ j-1的子串等于 i-j ~ i-1的子串。我们看到 j 前面的索引是1,next[1]=0,那么“ab”没有最大公共前后缀,长度为0。那么我们就必须让c(下标为6)与a(下标为0)比较。j=next[j-1](赋值前j=2,赋值后j=0),即 j 前移到0。
    在这里插入图片描述
  9. 此时 i=6j=0pattern[ i ] != pattern[ j ],失配。由于 j=0,不能再向左移了,所以将next[6]=0。保持 j 不动,i ++,相当于下面的字符串向后移一位。
    在这里插入图片描述
  10. 此时 i=7j=0pattern[ i ] == pattern[ j ],匹配,将next[7]=1(这个1表示子串“abababca”的最大公共前后缀为“a”,长度为1)。i ++j ++。那么,到这里,next数组就求出来了。
    在这里插入图片描述
        看一下完整的过程:
    在这里插入图片描述
        现在我们再看一个例子:
    在这里插入图片描述
        我们只关注当 i=8j=5pattern[ i ] != pattern[ j ],失配的时候。
        显然,这个时候下标3 ~ 7这个子串与下标0 ~ 4这个子串是相等的。我们看到 j 前面的索引是4,next[4]=2,那么“aabaa”的最大公共前后缀为“aa”(aa是前缀也是后缀),长度为2。所以下一次比较可以跳过“aa”(不用比较“aa”),j=next[j-1](赋值前j=5,赋值后j=2),将 j 前移到2,让a(下标为8)与b(下标为2)比较。
        此时i=8j=2pattern[ i ] != pattern[ j ],失配。我们再看到b(下标为2)前面的a(下标为1)的值为1(next[1]=1),那么“aa”的最大公共前后缀为“a”,长度为1。所以下一次比较可以跳过“a”,j=next[j-1](赋值前j=2,赋值后j=1),将 j 前移到1,让a(下标为8)与a(下标为1)比较。这个时候,pattern[ i ] == pattern[ j ],所以next[8]=2
        现在我们就可以写出求next数组的代码了,现在估计可以理解代码了:
	public static int[] getNext(String pattern) {
    
       //获取next数组
		int[] next = new int[pattern.length()];
		next[0]=0;   //初始化为0
		for(int i=1,j=0;i<pattern.length();i++) {
    
    
			while(j>0 && pattern.charAt(i)!=pattern.charAt(j)) {
    
    
				j=next[j-1];
			}
			if(pattern.charAt(i)==pattern.charAt(j)) {
    
       //前后缀字符相等
				j++;   //0~i这个字符串的最大公共前后缀的最大长度+1
			}
			next[i]=j;   //该值表示0~i这个字符串的最大公共前后缀的最大长度
		}
		return next;
	}

    其实KMP算法的过程和求next数组的过程类似,只要理解next数组的求解过程其它的问题就没这么难了。我们再看一下下面如何利用KMP算法匹配字符串:
在这里插入图片描述

	public static int kmp(String text, String pattern) {
    
    
		int[] next = getNext(pattern);// 计算next数组
		for(int i=0,j=0;i<text.length();i++) {
    
    
			while(j>0 && text.charAt(i)!=pattern.charAt(j)) {
    
    
				j=next[j-1];
			}
			if(text.charAt(i)==pattern.charAt(j)) {
    
    
				j++;
			}
			if(j==pattern.length()) {
    
       //匹配成功
				return i-j+1;
			}
		}
		return -1;
	}

    假设主串的长度为 m,模式串的长度为 n,则该算法的时间复杂度是 O(m+n),空间复杂度为 O(n)

三、参考文章和视频

1. 汪都能听懂的KMP字符串匹配算法【双语字幕】(本文的例子参考了这个视频,真的讲的很好,推荐!!!)
2. 如何更好的理解和掌握 KMP 算法?海纳的答案(知乎)
3. KMP算法详解-彻底清楚了(转载+部分原创)(详细)
4. KMP算法最浅显理解——一看就明白

如有错误请大家指出了(ง •̀_•́)ง (*•̀ㅂ•́)و

猜你喜欢

转载自blog.csdn.net/H_X_P_/article/details/104667522