KMP的个人向总结(next数组 || DFA实现--JAVA&&C++魔改版)--by wxj

之前学过KMP算法,但是当时学的时候就是比较模糊,对于它的认知也处在会用,会写的层次,但是对于它的内部的实现原理,仍是似懂非懂的状态,现在老师讲到字符串匹配算法的时候,我就重新学了一遍KMP,感觉之前有所疑惑的地方清晰了许多,趁现在对KMP仍有比较清晰的认知(个人觉得),赶紧记录下来,便于以后温习;

KMP,字符串的匹配算法,复杂度O(n+m),母串n+子串m,也就是说遍历一遍就可得出答案,相对传统的暴力匹配O(n*m)快了不是一星半点,当然也绕的不是一星半点;

KMP算法就是跳过重复的或着必定不会是答案的那段字符串,来节省不必要的时间;

例如:(存在返回首个下标,不存在返回-1)母串中查找子串;

母串:abdabcabda

子串:abcab;

这么简单的我们当然可以肉眼直接看了,但是计算机是不会直接看的,正常情况下,一个一个比较(暴力m*n)比较简单,也容易实现,就不写了,没什么意思,下面我们用KMP的方法查找母串:

首先我们要处理出子串的回溯数组回溯数组是KMP的灵魂,它的作用就是在子串失配时直接跳转到失配字符的上一个配对字符的位置;

子串的处理结果如下:(下标从0开始)

 a b c a b

-1 0 0 0 1

这些数字的含义就是该位置的字符可回溯的位置;

看不懂没关系,我们可以先这样写回溯数组:(下标从1开始)

 a b c a b

 0 0 0 1 2

这个意思很容易看懂吧,就是第i个字符是从头开始的第几位(和前面的那个位置的字符时相等的)

很明显,abc是第一组,ab可以在前面找到abc中的ab,因此对应的是1,2

将这个数组向右移动一位,前面补-1,即可得出回溯的数组;

得到了回溯数组,接下来就可以根据回溯数组进行快速匹配了;

首先,比较:(以后我默认以下标从0开始)

abdabcabda

abcab

-1 0 0 0 1

很显然,在第2处不一样,因为c的前面没有重复的元素(回溯到0),因此直接就可以跳到0的位置:

abdabcabda

    abcab

这样就相当于跳过了母串1位置元素的匹配了(因为这个位置是必定不可能匹配到的,可证明但我不证明);

然后再次比较:再往后移动

abdabcabda

      abcab

然后发现,匹配成功!

当然这个只是简单的例子,原理就是这个了,下面开始介绍如何求解回溯数组;

KMP的灵魂是回溯数组的应用(个人感觉)

代码镇楼:(下面解释)

//            p子串  lp子串长度  nxt回溯数组储存的数组
void get_nxt(char *p,int lp,int nxt[])
{//对于一个子串 ,nxt数组记录的是第i位置的字符他之前重复出现的字符的位置(如果在首位置,则全面的字符为-1) 
	nxt[0]=-1;//第0个元素没有前面相同的字符,初始化-1 
	int k=-1,j=0;//k(前面元素的位置), 
	while(j<lp)
	{
		if(k==-1||p[j]==p[k])//该字符在首位置||字符相等(可回溯到前面)
		{
			++k;
			++j;
			nxt[j]=k;//该位置能够回溯的位置
		}
		else
			k=nxt[k];//如果字符不等,继续向前回溯
	}
}

附带一个样例:

i:ababdababc

j:ababc

-1 0 0 1 2

第一次位移:

i:ababdababc

j:    ababc

j=2;p[j]='a';比较a和d,

i:ababdababc

j:        ababc

然后继续j=0比较a个d,

i:ababdababc

j:          ababc

之后j=-1,向后移动。

反正我看了这个样例之后很清楚

KMP比较的代码比较简单,就是按照上面的跑一边即可:

//          s母串  p子串  ls母串长度 lp子串长度  nxt数组储存回溯位置
void KMP(char *s,char *p,int ls,int lp,int nxt[])
{
	int ans=-1,i=0,j=0;
	while(i<ls)
	{
		//cout<<i<<" "<<j<<endl;
		if(j==-1||s[i]==p[j])//配对继续走
		{
			++i;
			++j;
		}
		else//失配回溯
			j=nxt[j];
		if(j==lp)//查看是否匹配完成
		{
			cout<<i-lp<<endl;//返回母串的下标
			return ;
		}
	}
	cout<<"NO FIND!"<<endl;//母串中没有子串
}

这样就比较清楚了,写完之后觉得比较清晰了。还是太菜啊!一个KMP拖到现在......

最后的是我的测试代码:

//#pragma comment(linker, "/STACK:1024000000,1024000000") 

#include<stdio.h>
#include<string.h>  
#include<math.h>  
  
//#include<map>   
//#include<set>
#include<deque>  
#include<queue>  
#include<stack>  
#include<bitset> 
#include<string>  
#include<fstream>
#include<iostream>  
#include<algorithm>  
using namespace std;  

#define ll long long  
//#define max(a,b) (a)>(b)?(a):(b)
//#define min(a,b) (a)<(b)?(a):(b) 
#define clean(a,b) memset(a,b,sizeof(a))// 水印 
//std::ios::sync_with_stdio(false);
const int MAXN=1e5+10;
const int INF=0x3f3f3f3f;
const ll mod=1e9+7;

void get_nxt(char *p,int lp,int nxt[])
{//对于一个子串 ,nxt数组记录的是第i位置的字符他之前重复出现的字符的位置(如果在首位置,则全面的字符为-1) 
	nxt[0]=-1;//第0个元素没有前面相同的字符,初始化-1 
	int k=-1,j=0;//k(前面元素的位置), 
	while(j<lp)
	{
		if(k==-1||p[j]==p[k])//如果第一次出现 
		{
			++k;
			++j;
			nxt[j]=k;
		}
		else
			k=nxt[k];
	}
}

void KMP(char *s,char *p,int ls,int lp,int nxt[])
{
	int ans=-1,i=0,j=0;
	while(i<ls)
	{
		//cout<<i<<" "<<j<<endl;
		if(j==-1||s[i]==p[j])
		{
			++i;
			++j;
		}
		else
			j=nxt[j];
		if(j==lp)
		{
			cout<<i-lp<<endl;
			return ;
		}
	}
	cout<<"NO FIND!"<<endl;
}
/*
3
abdbababcadd
abcab
abdbbcabcbab
abcab

*/
int main()
{
	int T;
	cin>>T;
	while(T--)
	{
		char s[MAXN],p[MAXN];
		//s母串,p子串 
		int nxt[MAXN];
		//子串中记录前驱字符的位置数组 
		clean(nxt,0);//初始化 
		cin>>s>>p;
		int lp=strlen(p),ls=strlen(s);
		get_nxt(p,lp,nxt);//获取nxt数组 
//		for(int i=0;i<=lp;++i)
//			cout<<nxt[i]<<" ";
//		cout<<endl; 
		KMP(s,p,ls,lp,nxt);
	}
}

以下为第一次补充:

DFA实现KMP:

//-------------------用DFA理解KMP;
对于一个字串,创建一个DFA二维表,然后加入一个DFA系统的重启状态:
模式匹配失败的时候,从DFA中的某一个状态重启 

关于DFA数组的构建:

首先看一下DFA数组的手动构建法:
以常见的三个字符组成的为例:
匹配的字串为:
ABABAC
拆开的状态:
	A	 B	  A	   B	A	 C
  0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 
这个是唯一的匹配成功的状态
因此写出对应的DFA数组
int DFA[3][6]={//因为只3种字符,和六种状态,因此我们可以构建出一个3*6的二维数组 
	0 1 2 3 4 5//下标 
	A,B,A,B,A,C,
A	1,1,3,1,5,1, 
B	0,2,0,4,0,4,
C	0,0,0,0,0,6
}; 
按照数组开始跑一边,然后就知道具体的运作了:
首先是状态0进入
然后如果读入的字符是A,则j=DFA[0(A)][0(j)A]=状态1 ;反之若读入为其他字符,则返回 状态0 ;
然后对于此时为 状态1 ;
读入的字符为B,则j=DFA[1(B)][1(j)B]=状态2 ;反之若读入其他字符,发现A的话j=DFA[0(A)][1(j)B]=状态1,C的话是j=DFA[2(C)][1(j)B]=状态0;
然后同理,对于后面的那些字符,读入A,返回j=状态3
读入 B j=状态4
读入 A j=状态5
读入 C j=状态6
状态6即为终态,查找结束。
好了,现在我们知道DFA数组的构成了,但是因为字串有很多个,一个一个的人找很麻烦,于是就有了生成DFA数组的算法:
我们将DFA数组的行设置为字符的种类,列设置为子串的长度

charAt(int index)//JAVA中查找字符串中的字符 的函数 
dfa[pat.charAt(0)][0]=1;
for(int X=0,j=1;j<M;++j)
{
	//compute dfa[][]
	for(int c=0;c<R;++c)
		dfa[c][j]=dfa[c][X];
	dfa[pat.charAt(j)][j]=j+1;
	
	X=dfa[pat.charAt(j)][X];
}
代码解释:
对于每个j,
匹配失败的时候,dfa[][X]复制到dfa[][j];
匹配成功的时候,dfa[pat.charAt(j)][j]设置为j+1;
更新X 。
状态先后面转移,必须是接受了与模式匹配的正确的字符,
每次这个字符有且只有一种情况,这个字符是pat.charAt(j)。
其他的字符只能使状态保持或状态回退 
状态回退也就是让DFA重置到适当状态 
dfa的自动建立见连接中的建立过程图:
https://blog.csdn.net/congduan/article/details/45459963
其实按照这个程序跑一边上面提到的dfa数组就可以清楚的知道它的运行了
//下面是网上看到获取dfa数组代码 
//CSDN:核心伪代码 https://blog.csdn.net/lsrnature/article/details/50901583
string pat;
int dfa[pat.charAt(0)][0]=1;
int iReStartPoint=0;
int len=pat.lenth();
for(int j=1;j<len;++j)
{
	for(int c=0;c<R;++c)
		dfa[c][j]=dfa[c][iReStartPoint];
	dfa[pat.charAt(j)][j]=j+1;
	iRStartPoint=dfa[pat.charAt(j)][iReStartPoint];
}

//----------------------已知DFA数组的KMP 
首先是运用DFA查找:
public int search(String txt)
{
	int i,j,N,txt.length(),M=pat.length();
	for(int i=0,j=0;i<N&&j<M;i++)
		j=dfa[(txt.charAt(i))][j];
	if(j==M)
		return i-m;//找到匹配 
	else
		return N;//未找到匹配 
}
也不难理解,照着跑一边就行了,给出测试样例:
B C B A A B A C A A B A B A C A A
A B A B A C 
照着跑一边就知道为什么只用一个dfa就能完成KMP查找了。 
//--------------------
来看一下《算法》第四版的代码:
public static int search(string pat,string txt)
{
	int j,M=pat.lenth();
	int i,N=txt.lenth();
	for(i=0,j=0;i<N&&j<M;++i)
	{
		if(txt.charAt(i)==pat.charAt(j))
			++j;
		else
		{
			//--朴素查找 
			i=i-j;//go back
			j=0;
			//---KMP用DFA查找
			j=dfa[txt.charAt(i)][j];//use na array dfa[][] 
		}
	}
	if(j==M)
		return i-M;//match
	else
		return N;//not match
}

下面给出完整的代码,首先因为我是看着JAVA学的,因此先帖上官方的以示尊敬:

public class KMP{
	private string pat;
	private int dfa[][];
	public KMP(string pat)//获取dfa数组 
	{
		this.pat=pat;
		int M=pat.lenth();
		int R=256;
		dfa=new int [R][M];
		
		dfa[pat.charAt(0)][0]=1;//初始化 
		for(int X=0,j=1;j<M;++j)//将整个长度记录都遍历出来 
		{
			for(int c=0;c<R;c++)//遍历所有字符 
				dfa[c][j]=dfa[c][X];//复制匹配失败时的回溯值 
			dfa[pat.charAt(j)][j]=j+1;//记录匹配成功时的值 
			X=dfa[pat.charAt(j)][X];//更新重启状态 
		}
	}
	public int search(string txt)//开始匹配字符 
	{//在txt上模拟DFA运行 
		int i,j,N=txt.length(),M=pat.length();
		for(i=0,j=0;i<N&&j<M;++i)
			j=dfa[txt.charAt(i)][j];
		if(j==M)
			return i-M;//找到匹配 (字串的结尾)先判断 
		else
			return N;// 没有找到匹配 (母船的结尾) 
	}
	public static void main(string args[])
	{//KMP字符串查找的测试用例 
		string pat=args[0];
		string txt=args[1];
		KMP kmp=new KMP(pat);
		stdout.println("text: "+txt);
		stdout.print("pattern: ");
		for(int i=0;i<offset;++i)
			stdout.print(" ");
		stdout.println(pat);
	}
} 

接下来是我自己魔改版的C++的DFA_KMP。
也不知道对不对。。。还没有经过一些OJ的测试。。

int **get_head(int **head,const int lp,const int i,const int j)
{
	//cout<<1<<endl;
	//cout<<*head<<" "<<head<<endl;
	int len=i*lp+j;
	for(int c=0;c<len;++c)
		head++;//*((int*)head++);
	//cout<<*head<<" "<<head<<endl;
	return head;
}

void get_dfa(char *p,int **DFA,int lp)
{
	//cout<<"get_dfa"<<endl;
	int **now=get_head(DFA,lp,p[0]-'a',0);
	//cout<<**now<<endl;
	**now=1;
	//cout<<**now<<endl;
	//DFA[p[0]-'a'][0]=1;
	for(int X=0,j=1;j<lp;++j)
	{
		for(int c=0;c<26;++c)
		{
			int **now1=get_head(DFA,lp,c,j);
			int **now2=get_head(DFA,lp,c,X);
			**now1=**now2;
		} 
		//	DFA[c][j]=DFA[c][X];
		**now=**get_head(DFA,lp,p[j]-'a',j);
		**now=j+1;
		X=**get_head(DFA,lp,p[j]-'a',X);
		//DFA[p[j]-'a'][j]=j+1;
		//X=DFA[p[j]-'a'][X];
	}
}

int KMP(char *s,char *p,int ls,int lp,int **DFA)
{
	int i,j;
	for(i=0,j=0;i<ls&&j<lp;++i)
	{
		int **now=get_head(DFA,lp,s[i]-'a',j);
		j=**now;
	}
	//	j=DFA[s[i]-'a'][j];
	if(j==lp)
		return i-lp;//返回匹配位置 
	else
		return ls;//返回到结尾 
}
/*
bcbaabacaababacaa
ababac
*/
int main()
{
	char s[MAXN],p[MAXN];
	cin>>s>>p;
	int ls=strlen(s);
	int lp=strlen(p);
	int DFA[26][lp];
	clean(DFA,0);
	get_dfa(p,(int **)DFA,lp);
	int location=KMP(s,p,ls,lp,(int **)DFA);
	if(location!=ls)
		cout<<"Find! The location is "<<location<<endl;
	else
		cout<<"No find!"<<endl;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值