【数据结构与算法】字符串匹配算法

一、暴力匹配

	public static int worstMatch(String s, String p)  
	{  
	    int sLen = s.length();  
	    int pLen = p.length();  
	  
	    int i = 0;  
	    int j = 0;  
	    while (i < sLen && j < pLen)  
	    {  
	        if (s.charAt(i) == p.charAt(j)) 
	        {  
	            //如果当前字符匹配成功(即S[i] == P[j]),则i++,j++      
	            i++;  
	            j++;  
	        }  
	        else  
	        {  
	            //如果失配(即S[i]! = P[j]),s回退j步到匹配开始的位置,
	            //然后从下一位置重新开始,即i = i - j+1      
	            i = i - j + 1;  
	            j = 0;  
	        }  
	    }  
	    //匹配成功,返回模式串p在文本串s中的位置,否则返回-1  
	    if (j == pLen)  return i - j;  
	    return -1;  
	}

二、KMP算法

对于KMP算法,这篇博客讲解得非常透彻。KMP算法详解

1、KMP思想

总的思想:记录模式串中最长重复子串,只移动模式串,当发现不匹配字符时,模式串的指针尽可能少的回退到下一个可能匹配的位置。
例如匹配串s是ABCDEABCDEABCDFABCDE,模式串p是ABCDEABCDF,那么模式串p中F之前实际上有最长重复子串ABCD,所以当指针i移到s[9],指针j移到p[9]时,发现字符E和字符F不相等,则i不变j回退到4,下次匹配时直接比较s[9]和p[4]即可,而无须i回到1,j回到0。即当s[i]和s[j]不匹配时,j=next[9]=4。
那么如何得到next数组呢,假设已知next[i]=k,则p[0]、p[1]、p[2]、…、P[k-1]这前k个字符与p[i-k]、p[i-k+1]、p[i-k+2]、~、p[i-1]这后k个字符相同,那么计算next[i+1]时,我们只需要判断p[i]和p[k]两个字符就有可能确定next[i+1]的值。如果p[i]=p[k],依照next的定义即可知道next[i+1]=k+1。
以ABEABCDABEABCD为例,我们知道next[12]=5,那么在求next[13]时,p[5]=C、p[12]=C、它们相等那么就可以直接确定next[13]=6。
再看p[i]和p[k]不相等的情况,我们假设next[k]=j,那么第一段p[0]、…、p[j-1],第二段p[k-j]、…、p[k-1],第三段p[i-k]、…、p[i-k+j-1]以及第四段p[i-j]、…、p[i-1]这四段长度为j的字符串都是相同的。也就是说如果p[j]==p[i],那么p[0]、…、p[j]这前j+1个字符串与p[i-j]~p[i]这后j+1个字符串一定是相同的,也就是说next[i+1]=next[k]+1。
以ABEABCDABEABED为例,我们知道next[12]=5,但是p[5]是C、p[12]是E,它们不相等所以不能得出next[13]=6,再看next[5]的值,它前面是ABEAB,所以next[5]=2,我们比较p[2]和p[12]的值发现它们相等,这个时候我们就可以得出next[13]=3。而若以ABEABCDABEABDC为例,因为next[12]=5,next[5]=2,但是此时p[2]!=p[12]且next[2]=0,则可以直接得出next[13]=0。

2、KMP代码

从上面的分析可以看出,在已知next[i]=k时,若p[i]=p[k],则next[i+1]=k+1。若p[i]!=p[k],则需要判断next[next[i]]的值,即若next[next[i]]=j,则判断一下p[j]和p[i]的值,若相等则next[i+1]=next[next[i]]+1。若不相等则需要继续判断直至next值为0。从编程的角度来看,我们判断p[i]的值是否能追加到之前的最长相同前后缀时,求的其实是next[i+1],也就是最外层循环只有n-1次,同时有可能存在一个内部循环。于是可以写出以下代码:

	private static int[] getNext(String pattern){
		if(pattern==null) return null;
		int len=pattern.length();
		if(len==0) return null;
		int[] next=new int[len];
		next[0]=-1;		
		int k;
		for(int i=0;i0){
					if(pattern.charAt(i)==pattern.charAt(k)){
						next[i+1]=k+1;
						break;
					}else{
						k=next[k];
					}
					if(k<=0) next[i+1]=0;
				}
			}
		}
		return next;
	}

不过这段代码虽然逻辑上比较清晰但是不够简练,它可以优化成下面这段代码:

	private static int[] getNext(String pattern){
		if(pattern==null) return null;
		int len=pattern.length();
		if(len==0) return null;
		int[] next=new int[len];
		next[0]=-1;
		int k=-1;
		int i=0;
		while(i

那么完整的KMP代码就是:

	public static int kmpSearch(String s, String p)  
	{  
		if(s==null) return -1;
		if(p==null) return -2;
		int sLen=s.length(),pLen=p.length();
		if(sLen==0) return -1;
		if(pLen==0) return -2;
		int[] next=getNext(p);
		int i=0,j=0;
	    while (i < sLen && j < pLen)  
	    {  
	        //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
	        if (j == -1 || s.charAt(i) == p.charAt(j))  
	        {  
	            i++;  
	            j++;  
	        }  
	        else  
	        {  
	            //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
	            //next[j]即为j所对应的next值        
	            j = next[j];  
	        }  
	    }  
	    if (j == pLen)  
	        return i - j;  
	    else  
	        return -1;  
	}

3、next数组优化

同时大神的博客也讲到了next数组的一个优化,假设匹配串ababbcabadabc、模式串ababbcabab。next数组不优化时为[-1,0,0,1,2,0,0,0,2,3],第一趟匹配失败发生在i=9,j=9。此时next[9]=3,所以第二趟匹配开始时i=9,j=3,而这必然是匹配失败的,因为s[9]=d,p[3]=b之前已经比较了d和b不相等,所以s[9]和p[3]的匹配完全是没必要的。同样next[3]=1,s[1]=b,也无需匹配。这种优化是很有必要的,因为next数组优化是一次性的,而模式串中有可能多次出现类似情况,如果在匹配时再去尝试这种必定失败的操作,会造成很大的不必要的性能开销。
所以当匹配失败且p[next[j]]=p[j]时,需要循环回退到不与p[j]相同的next点。那么代码中需要再增加一个循环判断吗?实际上也并不需要,因为在求next数组时,是从前向后计算的,我们可以保证之前的每个p[next[k]]不等于p[k],那么如果某点i的前继为k且p[i]=p[k],则next[i]应为k的前继next[k],那么p[next[i]]=p[next[k]]不等于p[k]即不等于p[i]。
因此代码中实际上只须回退一次就够了,没必要循环回退,改进的代码如下

	private static int[] getNext(String pattern){
		if(pattern==null) return null;
		int len=pattern.length();
		if(len==0) return null;
		int[] next=new int[len];
		next[0]=-1;
		int k=-1;
		int i=0;
		while(i

三、BM算法

BM算法的核心思想是后向匹配,坏字符和好后缀规则。
三步引入BM算法
BM算法详解

1、后向匹配

KMP算法中,都是从匹配串S和模式串P的起始位置开始匹配,模式串P的指针是从左向右移动。而BM算法是后向匹配,模式串P的指针是从右向左移动。当然匹配串S的指针肯定仍然是从左向右移动。
实际上对于长度为n的匹配串S和长度为m的模式串P,即使是用暴力匹配法也最多只需要匹配n-m+1次,每次需要比较两个长度为m的字符串是否匹配,发现当前子串不匹配时将匹配串的起始指针整体右移一位,即进行另一次匹配。而BM算法的目标就是尽量增大这个步长,每次匹配失败之后尽可能移动多位。

	public static int reverseMatch(String s, String p)  
	{  
	    int sLen = s.length();  
	    int pLen = p.length();  
	    int iStart = pLen-1;
	    int i=iStart;  
	    int j = pLen-1;  
	    while (j>=0&&iStart<=sLen-1)  
	    {  
	        if (s.charAt(i) == p.charAt(j))  {  
	            //如果当前字符匹配成功,则指针从后往左移动一位    
	            i--;  
	            j--;  
	        }  else  {  
	            //如果存在不同字符,则开始下一次匹配,iStart只移动了一位
	            iStart = iStart+1;
	            i=iStart;  
	            j = pLen-1;  
	        }  
	    }  
	    //匹配成功,返回模式串p在文本串s中的位置,否则返回-1  
	    if (j == -1)  return iStart - pLen+1;  
	    return -1;  
	}

2、坏字符

【待完成】

3、好后缀

【待完成】

四、Sunday算法

【待完成】Sunday算法介绍

五、RK算法

【待完成】基于Hash思想的RK算法

你可能感兴趣的:(数据结构与算法)