• 基于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;
    }
    
  • 相关阅读:
    C#演练—Windows应用程序—在windows窗体上动态创建上下文菜单
    C#演练—Windows应用程序—可视化继承
    C#演练—Windows应用程序—创建主从windows窗体
    小胖IT大讲堂之三 Hook实战(二) SQL Monitor山寨版
    小胖的2011总结之回忆篇
    Oracle安装示例数据库
    《领域驱动设计》读书笔记(一) 分离领域
    小胖IT大讲堂之一 .NET Reflector工具介绍
    10年前我不是小胖,也是个“诗人”
    忘记
  • 原文地址:https://www.cnblogs.com/luoyang0515/p/10662221.html
Copyright © 2020-2023  润新知