KMP算法详讲(问题导向,通俗易懂)

        KMP算法是一种高效的字符串匹配算法,相比于BF算法的时间复杂度为O(n*m),它的时间复杂度降低到了O(n+m)。这种算法的高效性在于它利用了主串的指针不回溯,而只移动模式串的指针位置。然而,对于初学者来说,KMP算法并不容易理解。因此,本文将采用问题导向的方法,以通俗易懂的方式解释KMP算法。

        (注:本文主串与模式串字符的默认起始位置为1!)

      为什么BF算法的效率低下?

        我们先感性地匹配一下如下两个字符串(x表示未知字符):

主串:       xxxxaaabaaabxxxx

模式串:   aaabaaad

        此时主串的指针i 指向b,模式串的指针j指向d,两者此时“失配”了。根据BF算法的匹配方式,此时指针i将回溯到绿色的a,指针j回溯到第一个a.此时细心的同学可以发现,其实并不需要如此“大打出手”,因为回溯依旧无法匹配。其实,指针i可以保持不动,指针j只移动b位置上,让模式串头三个字符aaa直接匹配主串字符b前的三个字符aaa,此时才作后续的判断,匹配效率必定会更高一点.

        那么问题来了,此种匹配方式真的可信吗?如果真的可行,那么每次“失配”,指针j移动到什么位置呢?这是下文所需解决的问题.

        如何定位指针j的回溯位置?

        我们需要确定指针j前的子串的最长相同前后缀的长度len,那么j的回溯位置就是len+1.

        在开始如下内容前,务必搞清楚如下几个概念:

1.前缀:包含首位字符但不包含末位字符的子串.

2.后缀:包含末位字符但不包含首位字符的子串.

3.字符串的最长相同前后缀:

    若字符串为:abacab,那么它的前缀为:{"a","ab","aba","abac","abaca"},后缀为:{"b","ab","cab","acab","bacab"},显然最长相同前后缀为:"ab".

        为了方便解释原理,命名模式串指针j前的子串为sonString,sonString的最长相同前缀为:preString,最长相同后缀为:sufString.

       KMP算法详讲(问题导向,通俗易懂)_第1张图片

               如上图所示,长条代表主串,短条代表模式串,此时h无法与g匹配,在红色长条中也存在最长相同的前后缀"ba"。此时为了提高匹配的效率,指针i保持不动,指针j指向a,如下图所示:

        KMP算法详讲(问题导向,通俗易懂)_第2张图片

        很显然,当指针j所指向的字符出现“失配”的情况下,通过让最长前缀preString覆盖住最长后缀fudString,能够更高效地匹配模式串。同时,若preString的长度为len,那么指针j应该回溯到len+1.

        剩下的问题就是,如何求指针j的回溯位置了.

        如何求指针j的回溯位置?

        定义next数组,next[j]表示:当模式串的指针j所指向的字符与主串“失配”时,指针j应该回溯的位置,即next[j]代表j前面的子串的最长相同前后缀长度+1.

        若next[i] = k,那么它有如下意义:

字符串:a_{1}a_{2} \cdots a_{k-1}a_{k} \cdots a_{i+1-k}a_{i+2-k} \cdots a_{i-1}a_{i}

其中子串:a_{1}a_{2} \cdots a_{k-1}a_{i+1-k}a_{i+2-k} \cdots a_{i-1},前者为最长相同前缀,后者为最长相同后缀.

        给定模式串便能求算next数组,代码实现如下:

#include
using namespace std;

const int N = 100;
int Next[N];
char mStr[N];
char tStr[N];

void GetNext(char ch[], int length) {
	Next[1] = 0;
	int i = 1, j = 0;
	while (i <= length) {
		if (j == 0 || ch[j] == ch[i]) {
			Next[++i] = ++j;
		}
		else {
			j = Next[j];	//	模式串的指针回溯位置
		}
	}
}

        求算next数组的代码很简短,它的原理十分奥秘,以下是它的推导过程:

        根据代码所示,我们规定Next[1] = 0,指针i指向sonString的最长相同后缀后面的第一个字符,指针j指向最长相同前缀后面的第一个字符,很显然,当ch[i]==ch[j]时,指针i+1后面的子串的最长相同前后缀的长度就是j+1.

        当ch[i]!=ch[j]时,假设k1 = Next[j],k2 = Next[k1],那么公式合理性推导如下:

        原sonString为:a_{1} \cdots a_{j-1} a_{j} \cdots a_{i-j+1} \cdots a_{i-1} a_{i},此时a_{1} \cdots a_{j-1} ==a_{i-j+1} \cdots a_{i-1},但是a_{j} !=a_{i}.

        因为k1 = Next[j],根据对称性,a1....a(k1-1) == a(j+1-k1)....a(j-1)==a(i+1-k1)....a(i-1),因在下一轮循环中,如果a[j]==a[i](此时j==k1)],那么a1....a(k1)==a(i+1-k1).....a(i),显然成立.

        若a[k1]!=a[i],那么重复j = Next[j],推导方式同上.

KMP算法的代码实现

#include
using namespace std;

const int N = 100;
int Next[N];
char mStr[N];
char tStr[N];

void GetNext(char ch[], int length) {
	Next[1] = 0;
	int i = 1, j = 0;
	while (i <= length) {
		if (j == 0 || ch[j] == ch[i]) {
			Next[++i] = ++j;
		}
		else {
			j = Next[j];	//	模式串的指针回溯
		}
	}
}

int stringMatch() {
	int mLen = strlen(mStr + 1), tLen = strlen(tStr + 1);
	int i = 1, j = 1;
	while (i <= mLen && j <= tLen && i - j <= mLen - tLen) {
		if (mStr[i] == tStr[j]) {
			i++, j++;
		}
		else {
			j = Next[j];
		}

		if (j == 0) {
			j++, i++;	//	重新开始匹配
		}
	}
	if (j == tLen + 1) {
		return i - tLen;
	}
	else {
		return 0;
	}
}


int main() {
	while (1) {
		memset(Next, 0, sizeof(Next));
		cout << "输入主串与模式串:" << endl;
		cin >> mStr + 1;
		cin >> tStr + 1;
		GetNext(tStr, sizeof(tStr + 1));
		int pos = stringMatch();
		if (pos) {
			cout << "匹配成功,主串的匹配起始位置为:" << pos << endl;
		}
		else {
			cout << "匹配失败..." << endl;
		}
	}
	return 0;
}

        

        

        

你可能感兴趣的:(数据结构与算法,数据结构,KMP算法,模式匹配)