基于KMP算法的字符串模式匹配问题

基于KMP算法的字符匹配问题

反正整个清明都在纠结这玩意...差点我以为下个清明要给自己过了。
至于大体的理解,我就不再多说了(还要画图多麻烦鸭),我参考了以下两个博客,写的真的不错,我放了超链接,点击就可以传送过去了。
(原创)详解KMP算法(点击跳转) :图画的很棒,很好理解,一步步带你深入
KMP算法最浅显理解——一看就明白(点击跳转):对主要的疑问有很细致地回答
需要注意的是,两篇博客都是以字符数组下标为0处开始存储
我对next数组不是很理解,说是next[j]表示的是j下一个指向的模式串的位置,但还是很抽象。经过一番思索,我对其有了个人理解
next数组的存在是KMP算法的核心,它的意义,就是如上所说,模式串中j下一个指向的地方,但这样说不够确切,应该是这样:
当主串s与模式串t分别在s[i],t[j]处,他们不相同的时候,保持s[i]的位置不变,将t[j]移动到t[next[j]]处
至于next数组应该怎么得来,只要明白了上面所说的,next数组的作用后,得到next数组的方式就显而易见了
遍历模式串t,得到每个位置t[j]的next[j]的值,这很像我们做策划时候的紧急预案,“如果我比较的时候在这个位置不同了,那我应该把j指向哪呢?”
本着这种思想,我们就可以得到next数组,而这个过程实际上是模式串t自我比较的过程,很多博客中都有,不再赘述。
而整体的逻辑就很明朗了——先做好预案才能放心地工作,对吧——先得到next数组,再把模式串和主串进行比较
由于KMP基于BF暴力算法,所以建议先打一边BF算法,再在其基础上改成KMP,而且需要修改的地方不是很多,可以加深理解。

K的值

实际上,模式串的每个位置t[j]都会自己对应的k值,正式我们所求的next[j],比如k1=next[1],k2=next[2].......
不过有些人对k的值不理解:什么叫“相同的最大前缀和最大后缀长”?
其实我们可以用更通俗的方法理解前缀后缀:(以ABCABB为例)

  1. 把模式串的最后一个字符遮住,剩下的串是不是能分成很多子串?值得注意的是,这里的字串要包括未遮住部分的第一个字符,如遮住最后一个B,剩下的是ABCAB,从左到右看依次是A,AB,ABC,ABCA,ABCAB,他们就是后前缀
  2. 把模式串的第一个字符遮住,剩下的串是不是能分成很多子串?同样,这里的字串要包括未遮住部分的第一个字符,如遮住第一个A,剩下的是BCABB,从左到右看依次是B,BC,BCA,BCAB,BCABB,他们就是后缀

当我们比较到ABCABB的最后一个B的时候,前面已经比较过的,和主串相同的部分自然是ABCAB。对于这部分来说前缀里的AB(ABC的AB)和后缀里的AB(CAB的AB)是相同的,并且也是最长的相同串。他们的长度2就是最后一个B的K值:如果比较到B不相等的时候,就把模式串右移到下标为2,也就是第三个字符C的位置再进行比较。注意区分此处从下标0开始存储和下标1开始存储的区别。

我的问题

编译器bug

最后讲讲我遇到的一些问题,真的痛苦,bug还没改完编译器先出问题。

“...”(Win32): 已加载......无法查找或打开 PDB 文件
“...”(Win32): 已加载“...ntdll.dll”。无法查找或打开 PDB 文件。
“...”(Win32): 已加载“...kernel32.dll”。无法查找或打开 PDB 文件
“...”(Win32): 已加载“...KernelBase.dll”。无法查找或打开 PDB 文件。
“...”(Win32): 已加载“...msvcr120d.dll”。无法查找或打开 PDB 文件
......

参考博客:无法查找或打开 PDB 文件(点击跳转)
反正是VS搞事情,见怪不怪了,习惯就好。

过大的数组长度

第二个问题是自然是,一百万个数据的处理。一开始在main函数里定义个长一百万的数组肯定不行,而且不管主串、模式串,甚至next数组的长度也要和模式串相同。
参考博客:数组元素过多怎么处理(点击跳转)
因为我用的定长顺序存储结构,所以最后将主串、模式串和next数组定义为全局变量。
还没完。
粗心的我最后发现程序还是在最大长度出错,怎么回事呢?
因为我把数组长度正正好好定义为一百万,忘了我是从下标为1开始存储的。最后给长度加了2就行了。
所以建议一开始定义数组的时候把长度都加长一些

next数组不明确

一开始看到这个问题是蒙蔽的。我把next数组定为全据变量后,报错“next 不明确”
我:???第一次看到这么神奇的bug
不过经过一番研究,发现next是C++的保留字,存在std::next的表达。所以将next作为main中的变量名是可以的,但是如果将其作为全局变量,在main函数中调用的时候就会出现不明确的提示。
参考:std::next(点击跳转)
所以我把next改成Next了。

代码

别问源码,再问BF。
不过还是给出比较难理解的部分代码梳理一下

void getNext(SString T, int next[]) {
	int i = 1, j = 0;	//j表示最长串的最后一个,i进行遍历
	next[1] = 0;
	while (i < T.length) {
		if (j == 0 || T.ch[i] == T.ch[j]) {		//j=0表示没有相同串,ch[i] = ch[j]表示如果相同那么没有必要右移模式串
			i++;
			j++;
			if (T.ch[i] != T.ch[j]) {	//i和j都右移后如果不同,是正常情况,直接把最长串最后一个的位置给next
				next[i] = j;
			}
			else {						//但是如果相同,就让next变为next[j],因为next[j]是之前已经得到的最长串最后一个的位置
				next[i] = next[j];
			}
		}
		else {					//如果j不为0且ch[i] 与 ch[j]不等,表示之前有相同串,但是现在右移了导致之前的相同串不相同了,说明之前得出的相同串已经废了,于是重置j
			j = next[j];
		}
	}
	return;
}

这里想讲的是注释最长的但是代码很短的这个

j = next[j];

之所以能这样进行赋值,实质上是为了利用我们之前已经得到的数据(k值),避免重复计算。
KMP算法的实质其实就是基于模式串的自我重复,之所以能将模式串右移,就是比较后面重复的部分的到时候失败了,所以回到前面重复的部分。
还是这个例子,ABCABB。其中最长的重复部分是AB,那么当最后一个B不同的时候,我就移动整个模式串,让前面的AB站到后面AB的位置继续比较,从而缩减少比较次数。
那么,我已经知道AB是重复的了。那么当我比较后面的AB的B的时候发现不匹配,我就可以直接用前面的AB的B的k值,这样后面就不用在算了。
于是便有了上面这行代码。
为了大家能更好理解,我po上源码。注释也尽量写的详细了,上面的内容也是对代码的一个扩充,希望大家能理解这段代码。

#include <iostream>
#include <string.h>

using namespace std;

//定义从1开始存放的串结构,由Read实现
typedef struct {
	char ch[1000002] ;     //原为char temp[1000002] = { " " };感谢不愿透露姓名的小仙女的指正,详见评论
	int length;
}SString;

void Read(SString &S, char temp[]);
void getNext(SString T, int next[]);
int KMP(SString S, SString T, int next[]);

SString S, T;        
char temp[1000002] = { ‘ ’ };        //原为char temp[1000002] = { " " }; 同上
int Next[1000002] = { 0 };

int main() {
	int result = 0;		//result为结果,即T在S中第一个出现的位置,没有出现则为0
	cin >> temp;
	Read(S, temp);
	cin >> temp;
	Read(T, temp);
	getNext(T, Next);
	result = KMP(S, T, Next);
	cout << result;
	return 0;
}

//先让模式串自我比较得出next数组,因为数组实际上是首地址,所以可以用void类函数且不引用
void getNext(SString T, int next[]) {
	int i = 1, j = 0;	//j表示最长串的最后一个,i进行遍历
	next[1] = 0;
	while (i < T.length) {
		if (j == 0 || T.ch[i] == T.ch[j]) {		//j=0表示没有相同串,ch[i] = ch[j]表示如果相同那么没有必要右移模式串
			i++;
			j++;
			if (T.ch[i] != T.ch[j]) {	//i和j都右移后如果不同,是正常情况,直接把最长串最后一个的位置给next
				next[i] = j;
			}
			else {						//但是如果相同,就让next变为next[j],因为next[j]是之前已经得到的最长串最后一个的位置
				next[i] = next[j];
			}
		}
		else {					//如果j不为0且ch[i] 与 ch[j]不等,表示之前有相同串,但是现在右移了导致之前的相同串不相同了,说明之前得出的相同串已经废了,于是重置j
			j = next[j];
		}
	}
	return;
}

int KMP(SString S, SString T, int next[]) {	//S主串,T模式串
	int i = 1, j = 1;		//i指主串,j指模式串
	int result = 0;
	while (i <= S.length&&j <= T.length) {
		if (j == 0 || S.ch[i] == T.ch[j]) {		//如果匹配接着往下比较,不匹配就右移T
			i++;
			j++;
		}
		else {
			j = next[j];
		}
	}
	if (j > T.length) {		//当且仅当j到最后仍与主串相等+1时匹配成功
		result = i - T.length;
	}
	return result;
}

void Read(SString &S, char temp[]) {	//将输入的字符串接在S后面,实现从下标为1开始存储
	S.ch[0] = ' ';
	strcat(S.ch, temp);
	S.length = strlen(S.ch) - 1;
	return;
}
posted @ 2019-04-08 18:48  BlackDn  阅读(720)  评论(2编辑  收藏  举报
https://blog-static.cnblogs.com/files/luoyang0515/live2d.js https://files.cnblogs.com/files/kousak/waifu.css
Live2D