字符串匹配KMP算法

给定文本串以及模式串,判断文本串中是否含有模式串,如果含有则返回模式串在文本串的首地址,如果不包含则返回-1即可。

例如:

文本串String txt = "abcababcabababccdabsadasas";   模式串String pat = "ababcabababc";  返回3

文本串String txt = "aaabaaabaaabaaabaaab";   模式串String pat = "aaaab";  返回-1。

主要内容

暴力求解

KMP算法核心思想

next数组求解

next数组的求解过程一

利用递推式来求解next数组

next数组求解优化

KMP算法


暴力求解

利用双指针,i从文本串起始位置开始遍历,对于文本串的每一个位置,模式串从该位置依次往后匹配,如果模式串能够全部匹配,则返回i位置即可;如果模式串不匹配,则移动i,如果i到达尾部还不匹配,则返回-1;

        public static int search1(String txt, String pat){
		int N = txt.length();
		int M = pat.length();
		//文本串进行每一位进行循环
		for(int i=0; i<=N-M; i++){
			int j;
			for(j=0; j<M; j++){
				if(txt.charAt(i+j) != pat.charAt(j)) break;
			}
			if(j == M) 
				return i;
		}
		return -1;
	}

暴力求解的另一种策略:

i指向文本串S  j指向模式串P   从起始位置i=0 j=0开始匹配,如果S[i]==S[j],则i++,j++;如果S[i]!=S[j],则说明从文本串的i位置开始不会匹配模式串,应该将i更新为本次匹配i初始位置的下一个位置  j指向模式串的0位置。

                                    

        public static int search2(String txt, String pat){
		int N= txt.length();
		int M = pat.length();
		int j = 0;
		int i = 0;
		//文本串以及模式都往后移动
		while(i<N && j<M){
			//如果匹配,则文本以及模式串都往后移动一个位置
			if(txt.charAt(i)==pat.charAt(j)){
				j++;
				i++;
			}else{
			//如果不匹配,则文本回退j个位置  模式串清零
			//回退j个位置回到本次不匹配的起始位置,还应该加一,使得文本串往后移动一次
				i = i-j+1;
				j = 0;
			}
		}
		if(j == M) return i-M;
		return -1;
	}

由于出现不匹配情况时,i,j指针都需要回退,该算法时间复杂度为O(MN)

KMP算法核心思想

如下图所示,S为文本串  P为模式串   初始位置i=0  j=0;

当i=7  j=7时,出现不匹配,暴力求解的思路是,i退回到初始位置下一个位置1 j=0,此时显然不匹配,继续i=2,3,..直到i=5时,此时j=0,有S[i]=S[j],则i++,j++继续开始匹配,直到i=7,j=2时,即从i=1,2,3,4过程中,j=0,该过程是没有必要的,因为必定不相等;i=5,j=0  i=6  j=1也是没有必要比较的,因为必定相等,即当i=7,j=7时出现不匹配,此时可以保持i不变,将j直接回退到2位置继续比较即可。

参考下图,暴力求解i,j回退之后再次比较,实质是模式串的前缀与模式串不匹配字符之前的后缀来进行比较。

例如 i=1  j=0   其实是比较S[1,6]与P[0,5] 又因为S[1,6]=P[1,6]   即该过程是比较P[0,5]与P[1,6];同理i=2, j=0是比较P[0,4]与P[2,6]。并且该过程只与模式串有关,如果事先对于模式串进行处理,求得模式串的不同位置,最大前缀与后缀,则可以实现i保持不变,j回退到最大前缀后缀位置处。

初始情况:

             

如果S[i]==S[j],则i++  j++ 继续匹配下一个字符,直到S[i]!=P[j]

         

此时i保持不变,j回退到某个位置,继续进行比较,j应该回退到模式串未匹配字符之前的字符串的最大前缀后缀;

假定对于模式串存在某一个最大的K,有

P[0,k-1]=P[j-k,j-1]   即此时模式串未匹配字符之前有最大前缀后缀P[0,k-1]。

     

因为S[i]!=P[j]   说明S[i-j, i-1]=P[0, j-1];

因为P[0,k-1]=P[j-k,j-1]      即S[j-k,j-1]=P[j-k,j-1]=P[0,k-1]

即如果模式串P中存在最大前缀后缀P[0,k-1],则P[0,k-1]与S[j-k,j-1]必定相等,因为之前已经比较过,则将j回退到P[k]位置处即可

KMP算法的核心思想:

当S[i]==P[j]时,i++  j++;

当S[i]!=P[j] 时,此时i保持不变   j回退到P[0,j-1]中的最大前缀后缀位置处,继续进行S[i]与P[j]比较;

假定next数组用来保存每个j位置不匹配时,j应该回退的位置j=next[j],则KMP算法可以如下:

        public static int kmp(String txt, String pat){
		int N = txt.length();
		int M = pat.length();
		int[] next = getNext1(pat);
		int i = 0;
		int j = 0;
		while(i<N && j<M){
			if(j==-1 || txt.charAt(i)==pat.charAt(j)){
				i++;
				j++;
			}else{
				j = next[j];
			}
		}
		if(j == M) return i-M;
		return -1;
	}

next数组求解

对于模式串的每个位置,都需要求解最大前缀后缀,即是否存在某个K,使得模式串满足:

P[0,k-1]=P[j-k,j-1]

假定模式串长度为M,则定义next数组来保存最大前缀后缀k的值.

next[j]=k,说明当S[i]!=P[j]时,此时j=next[j],即P[0,k-1]=P[j-k,j-1]

定义next[0]=-1,此时说明j的下一个位置无论是多少都无法满足i位置处匹配模式串,即i位置处所有情况已经处理都不匹配,此时可以将i后移一位;j指向0位置处,即若j==-1时,i++,j++即可;

      

next[j](0<=j<=M-1)数组定义如下:

next[0]=-1,表示文本串需要后移一位;
如果存在P[0,k-1]=P[j-k,j-1] 0<k<j,则 next[j]=max{k|P[0,k-1]=P[j-k,j-1] 0<k<j},表示存在最大前缀后缀;

其他情况next[j]=0,表示从模式串初始位置开始匹配;


next数组的求解过程一

当出现不匹配时,求出不匹配字符的前面字符串的最大前缀后缀;

假定模式串为ababcabababc   由以上定义可知 next[0]=-1  next[1]=0

当j==2时,求解过程如下:固定模式串P作为文本串,此时文本串指针指向2,将模式串指针指向1,此时比较P[0],P[1],由于不相等,即子串“ab”没有符合条件的k,next[j]=0

     

当j==3时,固定模式串P,将模式串从1位置处依次后移,判断是否含有最大前缀后缀

即此时需要判断“aba”是否含有最大前缀后缀,比较“ab”ba"   再比较“a“”a”  该比较过程分别对应下图模式串的后移不同位置

      

即要求某一个字符串的最大前缀后缀,等价于模式串作为文本串固定,模式串从1位置处依次后移一位,判断是否有最大前缀后缀。

j=4,5,6,7,8,9,10,11情况依次如下:

                          

                           

                           

将上述探索过程,使用代码来求解:

以下是利用暴力方法来探索模式串每一个子串是否为最大前缀后缀,时间复杂度过大

        public static int[] getNext1(String pat){
		int M = pat.length();
		int[] next = new int[M];
		next[0] = -1;
		next[1] = 0;
		int j = 2;
		//next[0] [1]由定义可以直接确定
		//对于j>1 next[j]使用暴力匹配求解,模式串与模式串右移一位开始匹配,直到发现最大相同前后缀
		while(j<M){
			System.out.println(Arrays.toString(next));
			for(int i=1; i<j; i++){
				if(pat.substring(i, j).equals(pat.substring(0, j-i))){
					next[j] = j - i;
					break;
				}
			}
			j++;
		}
		System.out.println(Arrays.toString(next));
		return next;
	}

利用递推式来求解next数组

分析next数组暴力求解过程,可以发现求解next过程也是一个字符串匹配问题,此时文本串与模式串均为模式串;

next数组定义如下:

next[0]=-1;
如果存在这样的k满足P[0,k-1]=P[j-k,j-1] 0<k<j, 则next[j]=max{k|P[0,k-1]=P[j-k,j-1] 0<k<j};

其他情况next[j]=0;

next数组的递推式

next[j]=k的含义是P[0,k-1]=P[j-k,j-1]

如果有P[k]=P[j],则有P[0,k]=P[j-k,j]

由next定义得next[j+1]=k+1

             

即求解模式串next数组过程中,将模式串与模式串进行匹配,初始条件是next[0]=-1,

next[j]=k

如果P[j]==P[k],则说明next[j+1]=k+1

如果P[j]!=P[k],则j应该回退到next[j]位置。

以模式串“aaab”为例分析:

                       

                       

如上所示,将模式串作为文本串,同时也作为模式串,i指向文本串  j指向模式串,初始状态next[0]=-1    i=0  j=-1,为了便于观察,任何时刻i与j指针都要对齐。与KMP算法思路相同

如果j==-1,说明此时i位置需要移动,j回到0位置处   即i=1  j=0  此时next[j]=j=0;

下一步比较P[1]与P[0],相等,则i++, j++有next[j] =j;

i不断移动直到i=3  j=2时,此时P[3]!=P[2],即此时P[1,3]!=P[0,2]。因此此时最大前缀后缀长度不会增加,j需要回退到next[j]位置1处,继续进行匹配,直到i=4,结束,此时next数组求解为{-1,0,1,2}

求解代码如下:

        //获取next数组
	public static int[] getNext2(String pat){
		int M = pat.length();
		int[] next = new int[M];
		next[0] = -1;
		//文本串--模式串从0位置开始
		int i = 0;
		//模式串从-1位置开始  j==-1 说明文本串以及模式串均需要后移
		int j = -1;
		while(i<M-1){	
			//System.out.println(i+" "+j+" "+Arrays.toString(next));
			//j==-1 表示当前文本串i位置不可能出现匹配,应该i++ j++,使得文本串后移一位,模式串指向初始位置0,开始匹配
			//S[i]==S[j] 说明出现最大相同前后缀 将前后缀长度更新到next[i]中
			if(j==-1 || pat.charAt(i)==pat.charAt(j)){					
				i++;
				j++;
				next[i] = j;
			}
			//如果出现不匹配,则j指针回退
			else{
				j = next[j];
			}
		}
		System.out.println(Arrays.toString(next));
		return next;
	}

next数组求解优化

假定文本串为aabaabaabaabaab  模式串为aaab,利用上述求解过程有next={-1,0,1,2}

具体匹配过程如下:

如上所示,当i=2,j=2时出现不匹配,此时j回退到j=1,该位置继续不匹配j继续回退j=0,该位置不匹配,继续回退到-1,由于j=-1,此时i后移一位,j=0。观察整个过程  j=1  j=0回退过程是没有必要的,

当S[i]!=P[j]时,将j=next[j]=k,即下一步比较S[i]与P[k],如果有P[j]==P[k],则S[i]!=P[k]必定成立,即此时不需要进行比较,即在求解next过程中,如果某个位置有P[j]==P[k],则说明next[j]=next[k]即可,即如果出现不匹配时,P[j]!=S[i],此时j回退j=next[j],j应该回退到某个位置k此时有P[k]!=P[j],如果P[k]==P[j],则应该继续回退以前位置,此处比较时没有意义的。

        //获取next数组
	public static int[] getNext3(String pat){
		int M = pat.length();
		int[] next = new int[M];
		next[0] = -1;
		//文本串--模式串从0位置开始
		int i = 0;
		//模式串从-1位置开始  j==-1 说明文本串以及模式串均需要后移
		int j = -1;
		while(i<M-1){	
			//System.out.println(i+" "+j+" "+Arrays.toString(next));
			//j==-1 表示当前文本串i位置不可能出现匹配,应该i++ j++,使得文本串后移一位,模式串指向初始位置0,开始匹配
			//S[i]==S[j] 说明出现最大相同前后缀 将前后缀长度更新到next[i]中
			if(j==-1 || pat.charAt(i)==pat.charAt(j)){					
				i++;
				j++;
				//判断下一个位置是否相同,如果相同,则直接next[i]=next[j]
				if(pat.charAt(i)!=pat.charAt(j))
					next[i] = j;
				else
					next[i] = next[j];
			}
			//如果出现不匹配,则j指针回退
			else{
				j = next[j];
			}
		}
		System.out.println(Arrays.toString(next));
		return next;
	}

 

KMP算法

KMP最终代码实现如下所示:  时间复杂度为O(M+N)

        public static int kmp(String txt, String pat){
		int N = txt.length();
		int M = pat.length();
		int[] next = getNext3(pat);
		int i = 0;
		int j = 0;
		while(i<N && j<M){
			if(j==-1 || txt.charAt(i)==pat.charAt(j)){
				i++;
				j++;
			}else{
				j = next[j];
			}
		}
		if(j == M) return i-M;
		return -1;
	}
	 
	
	//获取next数组
	public static int[] getNext3(String pat){
		int M = pat.length();
		int[] next = new int[M];
		next[0] = -1;
		//文本串--模式串从0位置开始
		int i = 0;
		//模式串从-1位置开始  j==-1 说明文本串以及模式串均需要后移
		int j = -1;
		while(i<M-1){	
			//System.out.println(i+" "+j+" "+Arrays.toString(next));
			//j==-1 表示当前文本串i位置不可能出现匹配,应该i++ j++,使得文本串后移一位,模式串指向初始位置0,开始匹配
			//S[i]==S[j] 说明出现最大相同前后缀 将前后缀长度更新到next[i]中
			if(j==-1 || pat.charAt(i)==pat.charAt(j)){					
				i++;
				j++;
				//判断下一个位置是否相同,如果相同,则直接next[i]=next[j]
				if(pat.charAt(i)!=pat.charAt(j))
					next[i] = j;
				else
					next[i] = next[j];
			}
			//如果出现不匹配,则j指针回退
			else{
				j = next[j];
			}
		}
		System.out.println(Arrays.toString(next));
		return next;
	}

next数组

next[j]=k  说明对于模式串P 满足P[0,k-1]=P[j-k, j-1]。此时如果S[i]!=P[j],则j=next[j]=k位置处继续匹配;

参考链接:

https://blog.csdn.net/xiaohuanglv/article/details/85178138

https://pan.baidu.com/s/1c0k8DNU

http://www.aichengxu.com/suanfa/801445.htm

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值