字符串匹配算法-KMP算法

BF算法

提到字符串匹配,最先想到的就是模式串与主串一个字符一个字符的进行匹配,当遇到不匹配的字符时,模式串则往后移动一位,继续从模式串的第一位开始匹配,这种匹配算法也被称为 BF算法,即“暴力匹配算法”。时间复杂度为O(n*m),其中n、m表示主串与模式串的长度。

BM算法、KMP算法都是在BF算法的基础之上进行优化,即在匹配的过程中,希望跳过更多的字符。

KMP算法思路

既然字符串在比对失败的时候,我们已经知道读过哪些字符了,有没有可能避免“跳回下一个字符再重新匹配”的步骤呢?

KMP算法的基本思路就是当遇到一个不匹配的字符时,利用已经遍历过的模式串,来避免暴力算法中的“回退(backup)”的步骤。换句话说就是不希望递减主串的指针,让其永远向前移动。

举个栗子:

在 主串-ABABABCAA 中 匹配 模式串-ABABC

 当匹配时,发现子串的末尾字符与主串对应字符并不匹配,但主串的后缀与子串的前缀相匹配,则可以跳过两个字符,将子串往后移动两位,继续匹配。

那我们怎么知道当每次遇到不匹配字符时该跳过多少个字符呢?

这里就要用到KMP算法中定义的next数组了。 

举个栗子:

这里我们先不管next数组时如何生成的,先来看看他的功能和用途。

KMP算法当遇到字符不匹配的情况时, 会去看最后一个匹配的字符它所对应的next数值。

在这个例子中,最后一个匹配的字符对应的next数值为2,于是我们则将子串向后移动两位,使其在主串中跳过两个字符,就可以继续往后匹配。

 next数组元素的数值,则代表子串可以“跳过匹配”的字符个数。

由于可以不用回退指针,只需要一次主串的遍历就可以完成匹配,效率自然会比暴力算法高很多。

public int kmpSearch(char[] txt, char[] patt){ // txt代表主串,patt代表模式串
		int[] next =buider_nexts(patt); // 假设已经计算出了next数组
		int i = 0; // 主串中的指针
		int j = 0; // 子串中的指针
		while (true){
			if (i == txt.length) return -1;
			if (txt[i] == patt[j]) { // 字符匹配,指针后移一位继续匹配
				i++;
				j++;
			} else if (j > 0) { // 字符不匹配,则根据next数值跳过子串前几个字符的匹配
				j = next[j-1];
			}else { // 子串的第一个字符就不匹配,则直接后移一位
				++i;
			}
			if (j == patt.length) return i-j; // 如果j已经达到子串末尾,则匹配成功,返回匹配的起始位置
		}
	}

 注意主串的指针 i 永不递减,这也是KMP算法的精髓。

next数组的生成

之前讲到,next数组中的数值代表在匹配失败的时候,子串中可以跳过的匹配的字符个数。但为什么可以这么做呢?

通过前面的例子可以发现,最后匹配的两个AB与跳过的最前面的两个AB是一样的。

换句话说,对于子串的前四个字符,它们拥有一个共同的前缀和后缀AB,长度为2。

next数组的本质就是寻找子串中“相同前后缀的长度”,并且一定是最长前后缀。 

举个栗子:

 在这个子串中,虽然前缀与后缀的A是相同的,但并不是最长的,而ABA才是最长的相同前后缀,因此next数值为3。

但是要注意,我们要找的前后缀不能是子串本身,如果跳过了子串本身长度的字符个数,则没有意义了。

next数组的计算

以子串ABABC为例,计算next数组。

  • 对于第一个字符,显然不存在相同前后缀,next为0
  • 对于前二个字符,没有相同前后缀,next为0
  • 对于前三个字符,有相同前后缀A,next为1
  • 对于前四个字符,有相同前后缀AB,next为2
  • 对于前五个字符,没有相同的前后缀,next为0

但是算法该如何实现呢?

我们可以使用for循环暴力求解,但是效率太低。其实这里可以采用递推的方式快速求解next数组,它的巧妙之处是会不断利用已知信息来避免重复运算。

举个栗子:

假设已知当前共同前后缀,且长度为2,继续向下匹配则有两种情况。

1.如果下一个字符依然相同,则构成了一个更长的共同前后缀,且长度为之前的长度+1。

2.如果下一个字符不相同,则需要寻找是否存在更短的相同前后缀。

那如何找到更短的相同前后缀呢?

在前面的计算中已经知道在这个不匹配字符B的前面有一个长度为3的相同前后缀,我们则可以直接在左边寻找共同的前后缀,而左边的最长前后缀通过查表可以得到长度为1,则又回到了最初的步骤,检查下一个字符是否相同,如果相同则可以构建一个更长的前后缀,长度+1即可。

对比动画了解:

 

KMP算法next数组生成代码: 

private static int[] buider_nexts(char[] patt) {
	int[] next = new int[patt.length]; // next数组,且第一个元素为0
    next[0] = 0;
	int prefix_len = 0; // 当前公共前后缀的长度
	int i = 1;
	while (i < patt.length){
		if (patt[prefix_len] == patt[i]){ // 字符匹配,则将prefix_len+1,存入对应next数组中
			prefix_len++;
			next[i] = prefix_len;
		}else{
			if (prefix_len == 0){	// 如果不存在相同前后缀则直接把next设为0
				next[i] = 0;
				i++;
			}else{
				prefix_len = next[prefix_len-1];	// 字符不匹配则直接查表看看存不存在更短的共同前后缀
			}
		}
	}
	return next;
}

 完整代码:

public int kmpSearch(char[] txt, char[] patt){
		int[] next =buider_nexts(patt); // 假设已经计算出了next数组
		int i = 0; // 主串中的指针
		int j = 0; // 子串中的指针
		while (true){
			if (i == txt.length) return -1;
			if (txt[i] == patt[j]) { // 字符匹配,指针后移一位继续匹配
				i++;
				j++;
			} else if (j > 0) { // 字符不匹配,则根据next数值跳过子串前几个字符的匹配
				j = next[j-1];
			}else { // 子串的第一个字符就不匹配,则直接后移一位
				++i;
			}
			if (j == patt.length) return i-j; // 如果j已经达到子串末尾,则匹配成功,返回匹配的起始位置
		}
	}

	private int[] buider_nexts(char[] patt) {
		int[] next = new int[patt.length]; // next数组,且第一个元素为0
		next[0] = 0;
		int prefix_len = 0; // 当前公共前后缀的长度
		int i = 1;
		while (i < patt.length){
			if (patt[prefix_len] == patt[i]){ // 字符匹配,则将prefix_len+1,存入对应next数组中
				prefix_len++;
				next[i] = prefix_len;
                i++;
			}else{
				if (prefix_len == 0){	// 如果不存在相同前后缀则直接把next设为0
					next[i] = 0;
					i++;
				}else{
					prefix_len = next[prefix_len-1];	// 字符不匹配则直接查表看看存不存在更短的共同前后缀
				}
			}
		}
		return next;
	}

猜你喜欢

转载自blog.csdn.net/weixin_53922163/article/details/132755827