字符串经典匹配算法之KMP算法

背景知识回顾

之前的文章中讲了字符串匹配问题中的单模匹配问题,即从一段文本串中找到另一个字符串是否出现过。母串(文本串)是指要从哪个字符串中查找,模式串指的是要查找哪个字符。之后介绍了暴力匹配算法,即从文本串中逐位向后和模式串比较。

本文介绍KMP匹配算法

KMP算法理论介绍

1. 模式串逐位平移比较能否改为跳位比较

假设母串和模式串的比较遇到如下情况:
在这里插入图片描述

将母串用S表示,模式串用T表示。

母串比较到了x位置,模式串比较到了索引为12的位置y, 此时发现失配。

正常来说可以将模式串向后平移一位,重新进行逐位比较。

而模式串向后平移一位,文本串的x位置之前仍能匹配成功,应该等价于 T [ 0 : 10 ] = = T [ 1 : 11 ] T[0 : 10] == T[1 : 11] T[0:10]==T[1:11]。其中T为模式串。

同理,将模式串向后平移两位,文本串的x位置之前仍能匹配成功,应该等价于 T [ 0 : 9 ] = = T [ 2 : 11 ] T[0 : 9] == T[2 : 11] T[0:9]==T[2:11]

将模式串向后平移三位,文本串的x位置之前仍能匹配成功,应该等价于 T [ 0 : 8 ] = = T [ 3 : 11 ] T[0 : 8] == T[3 : 11] T[0:8]==T[3:11]

将模式串向后平移 k k k位,文本串的x位置之前仍能匹配成功,应该等价于 T [ 0 : ( 11 − k ) ] = = T [ k : 11 ] T[0 : (11-k)] == T[k : 11] T[0:(11k)]==T[k:11]

而对于 T [ 0 : ( 11 − k ) ] = = T [ k : 11 ] T[0 : (11-k)] == T[k : 11] T[0:(11k)]==T[k:11]是否成立,在比较之前就是可以预先计算好的。例如上图的例子 k k k最小等于7时, T [ 0 : 4 ] = = T [ 7 : 11 ] T[0 : 4] == T[7 : 11] T[0:4]==T[7:11] 才是成立的。这就意味着如果向后将模式串向后平移1位,2位,…, 6位都是不可能匹配成功的。

平移 k = 7 k=7 k=7 位以后,只需要看文本串此时的位置 x x x 和模式串平移后对应的位置 b b b 是否相等即可。

如果 x x x b b b相等,那么就让x的下一位和b的下一位继续比较,直到不匹配或者模式串都匹配完;

如果 x x x b b b不相等,那么此时模式串 T T T 0 0 0 4 4 4 ( a d a d e ) (adade) (adade)就代替了之前的 T [ 0 : 11 ] T[0:11] T[0:11], 即 T [ 0 : 4 ] T[0:4] T[0:4]中再找到可以向右平移的最小步数(新的k)。

上面分析的过程中对于T的前11位,寻找 T [ 0 : ( 11 − k ) ] = = T [ k : 11 ] T[0 : (11-k)] == T[k : 11] T[0:(11k)]==T[k:11]等式成立的最小的 k k k,也叫寻找模式串的**最长公共前缀( T [ 0 : ( 11 − k ) ] T[0 : (11-k)] T[0:(11k)])和后缀(**T[k : 11])。

所以问题的关键就成了寻找出模式串每一位的最长公共前缀和后缀。如果能预计算出来这个信息,暴力匹配中的逐位比较就可以变成跳着比较。

2. 模式串每一位最长公共前后缀的计算

定义数组 i n t [ n ]    n e x t int [n] \ \ next int[n]  next, n n n为模式串 T T T 的长度, n e x t [ i ] next[i] next[i] 表示 i i i位置作为最长公共后缀的末尾时,对应的最长公共前缀的末尾位置
例如对于上例中的模式串 a d a d e b c a d a d e y adadebcadadey adadebcadadey, 每一位对应的 n e x t [ i ] next[i] next[i]如下:
在这里插入图片描述

其中 − 1 -1 1是自定义的一个虚拟索引,表示该位置不存在最长公共前后缀。

接下来考虑 n e x t next next 数组如何简便的计算。

初始位置 n e x t [ 0 ] next[0] next[0]一定等于-1,因为就一个数字,不存在最长公共前后缀。

假设从 n e x t [ 0 ] next[0] next[0] n e x t [ i − 1 ] next[i-1] next[i1] 已经全都计算好了,那么对于 n e x t [ i ] next[i] next[i] 应该如何计算?

在这里插入图片描述

如上图,设 j = n e x t [ i − 1 ] j = next[i - 1] j=next[i1],那么根据定义,必定有 T [ 0 : j ] = T [ ( i − 1 − j ) : ( i − 1 ) ] T[0 : j] = T[(i - 1 - j) : (i-1)] T[0:j]=T[(i1j):(i1)] (分别表示最长公共前缀和后缀)。即上图中的两个阴影部分可以完全匹配。

此时到了 i i i 这个位置,只需要看 T [ i ] = = T [ j + 1 ] T[i] == T[j + 1] T[i]==T[j+1] 是否成立。

如果T[i] == T[j + 1]成立,那么上面相等的两个阴影部分就可以分别向后扩一位,即 T [ 0 : ( j + 1 ) ] = T [ ( i − j − 1 ) ) : i ] T[0 : (j + 1)] = T[(i - j - 1)) : i] T[0:(j+1)]=T[(ij1)):i] 成立,此时 n e x t [ i ] = j + 1 next[i] = j + 1 next[i]=j+1

如果T[i] == T[j + 1] 不成立,那么就在下面那个阴影部分( T [ 0 : j ] T[0 : j] T[0:j])中,找到** j j j位置作为最长公共后缀时,最长公共前缀的位置**,即 n e x t [ j ] next[j] next[j]。因为 j j j一定小于等于 i − 1 i - 1 i1, 所以 n e x t [ j ] next[j] next[j] 是已经计算好的。继续看 T [ i ] = = T [ n e x t [ j ] + 1 ] T[i] == T[next[j] + 1] T[i]==T[next[j]+1] 是否成立。

如此利用数学归纳法(递推思想)便可完成所有位置最长公共前后缀( n e x t next next 数组)的计算。

KMP算法代码实现

//string_match_kmp.cpp
#include <string>
#include <iostream>
#include <cstdio>

using namespace std;
void getNext(const char *pattern, int *next) {
    /*
    预先生成next数组
    pattern: 模式串
    next: kmp匹配中的next数组
    return: void
    */
	next[0] = -1; //j 表示上一个位置 next[i - 1] 值
	for (int i = 1, j = -1; pattern[i]; ++i) {
		while (j != -1 && pattern[j + 1] - pattern[i]) j = next[j];
		if (pattern[j + 1] == pattern[i]) j += 1;
		next[i] = j;
	}
	return;
}

void output_next(int *next, int n) {
	for (int i = 0; i < n; i++) {
		printf("%d ", next[i]);
	}
	printf("\n");
}

int kmp(const char *text, const char *pattern) {
	/*
	kmp匹配算法
	text: 母串(文本串)
	pattern: 模式串
	return: 第一个匹配成功的text数组的位置索引
	*/
	int n = strlen(pattern);
	int *next = (int *)malloc(sizeof(int) * n);
	getNext(pattern, next);  //预先计算好next数组
	//output_next(next, n);  //可以输出next数组,便于理解

	//j表示截止到上一个位置(text[i - 1])已经和 pattern的j位置及之前都匹配成功
	for (int i = 0, j = -1; text[i]; i++) {
		
		//text的 i 位置和pattern的j+1位置如果不相等,
		//那么继续缩小j到下一个最长公共前后缀的位置
		while (j != -1 && text[i] != pattern[j + 1]) j = next[j];  

		//相等则匹配成功的长度+1
		if (text[i] == pattern[j + 1]) j += 1;

		//j已经到了pattern的最后一个位置,此时i位置对应模式串的最后一个位置,
		//文本串对应的起始位置为i - j
		if (pattern[j + 1] == 0) return i - j;
	}
	return -1;
}

#define TEST(func, s, t) {\
	printf("%s(\"%s\",  \"%s\") = %d\n", #func, s, t, func(s, t));\
}

int main() {
	char s[200], t[200];
	while (cin >> s >> t) {
		TEST(kmp, s, t);
	}
	return 0;
}

代码总结与分析: 代码的整体思路和理论部分相对应。
在for循环中,while部分仅涉及pattern索引位置的转换,是近似O(1)的,所以整体算法的时间复杂度为O(n+m), n为文本串的长度,m为模式串的长度(加上了getNext部分的耗时)。

KMP算法的应用价值

  1. 相当于状态机:由上一节的代码部分可以看出,程序的主要部分为for循环内部的三行语句,而这三行起始可以总结为 j j j 的转换,相当于状态转换,所以KMP算法相当于一个状态机转换过程。
  2. 可以处理流式数据:由代码部分亦可看出,预先对模式串进行计算后,文本串 i i i 位置的求值,仅和 i i i之前的字符匹配到模式串的哪一位有关,和i后面的值是没有关系的。所以KMP算法可以处理流式文本串。即不需要存储文本串的全部信息,流式输入(文本串中的字符一个一个输入),便可流式地将结果输出。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值