Knuth-Morris-Pratt Algorithm KMP算法

Knuth-Morris-Pratt Algorithm

所谓 Knuth-Morris-Pratt Algorithm 由三位大佬:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。

简称 KMP 算法,是一种经过改进的字符串匹配算法,其核心是由模式串推出Next数组,在匹配

中只需 O(m+n) 的时间复杂度。

一、Next 数组

定义:next[j] 表示模式串前j-1的子串的最大公共前后缀,"即next[j]表示 p[0,j-1] (前闭后闭) 的公共前后缀"

​ next[0] = -1

/*
* Next 数组是KMP算法的核心所在,当然也是让人比较难理解的地方
* 我们定义 next[j] 表示模式串前j-1的字串的最大公共前后缀
* 先上求next数组的代码
*/

vector<int> NextArray(string& pattern) {
	int len = pattern.size();
	vector<int> next(len);  
	next[0] = -1;
	int k=0, j=1;
	while (j < len - 1) {
		if (pattern[k] == pattern[j]) { // p[j]==p[k] 的情况
			next[j + 1] = k + 1;
			++k; ++j;
		}
		else {
			if (k == 0) { // 不能 k = next[k],因为next[0] == -1
				next[j+1] = 0; // 就是说在前j的子串没有公共前后缀
				++j;
			}
			else { k = next[k]; } // 核心且难以理解
		}
	}
	return next;
}

索引 0 1 2 3 4 5 6 7 8 9
string(p) A B A C D A B A B C
next -1 0 0 1 0 0 1 ?
标记 0 k j-1 j

对于 p[k]==p[j] 非常容易理解,由上图可知,只需 next[j+1]=k+1即可。next[7]=1+1=2

然后 ++k,++j 进入新一轮循环,易推出 next[8]=3;

索引 0 1 2 3 4 5 6 7 8 9
string(p) A B A C D A B A B C
next -1 0 0 1 0 0 1 2 3 ?
标记 0 k-2 k-1 k j-k j-k+1 j-1 j

但是当我们继续推就会出现 p[k]!=p[j] 的情况,由代码可知有 k=next[k],然后再次进入循环,令人不解,

如果你一眼就知道怎么回事,那么比我强的多!对此可能有不同的理解,这里我用数学归纳法来解答。

NOTE: 把 next[j] 和 公共前后缀 搞清 在看!!!!!!!!!!!!!!!!!!!!!!

A B ... D C A ... A B ... D C B T
0 1 ... k-2 k-1 k ... j-k j-k+1 ... j-2 j-1 j j+1 ...

​ 首先判断 p[k]和p[j] 我们是为了确定 next[j+1] 的值,即 p[0,j] (前闭后闭) 的最长公共前后缀,

如果 p[0,k] 和 p[j-k,j] 依次相等那么 next[j+1]一定等于k+1,但是现在不相等了,所以一定有

next[j+1]<=k 即我们需要判断 p[0,k-1]的前缀 和 p[j-k+1,j]的后缀 的最长公共部分

​ 将 p[0,k-1] 分割成 : p[0,k-2] 和 p[k-1]

​ 将p[j-k+1,j] 分割成 : p[j-k+1,j-1] 和 p[j]

​ 易得如果 p[0,k-2]的前缀 和 p[j-k+1,j-1]后缀 尽可能多的相等,再有 p[k-1] 和 p[j] 相等 next[j+1]一定最大。

当我们在推导过程中求next[j+1]只需要确定 p[k] 是否等于 p[j],是因为我们已经知道了p[0,k-1] 和 p[j-k,j-1]

是依次相等的了,由此可以推出 p[1,k-1] 和 p[j-k+1,j-1] 是相等的,所以上面的 p[0,k-2] 和 p[j-k+1,j-1] 的比较

可以转化成 p[0,k-2] 和 p[1,k-1] 的比较 (可以看一下下图的抽象),我们知道 next[k] 就是表示 [0,k-1] 的最长

公共前后缀,假设 next[k]=Y,可知 p[0,Y-1] 和 p[k-Y,k-1] 是相等的,因为我们已经有 p[1,k-1] 和 p[j-k+1,j-1]是

相等的了,所以 p[k-Y,k-1] 和 p[j-Y+1,j-1] 相等,所以 p[0,Y-1] 和 p[j-Y+1,j-1] 相等,我们只需要再次判断

p[Y] 是否等于 p[j],既可以确定 next[j+1]的值 ,当然这是下层循环干的事,

这一层只需:若 p[k] != p[j] 则 k=next[k] 。

<0--------------Y-1--Y-------------------------k-2>  

	  <1-----------------------------k-Y--------------------k-1>

二、字符串匹配

字符串匹配就非常简单了,这里只提供代码和简单注释

int March(string& pattern, string& text, vector<int>& next) {
    if (pattern.size() < text.size()) { return -1; }
	int i = 0; // pattern 的位置
	int j = 0; // text 的位置
	while (i < pattern.size() && j < text.size()) {
		if (j == -1 || pattern[i] == text[j]) {
			++i; ++j;
		}
		else { // i 无需回溯
			j = next[j];
		}
	}
	return j == text.size() ? i - j : -1;
}

posted @ 2022-09-01 15:43  捏鼠  阅读(586)  评论(0)    收藏  举报