数据结构Java实现——③串--->串的模式匹配:Brute-Force算法和 KMP算法


写在前面

       

其实关于串的模式匹配问题,我最初的了解并不深,只知道一个最简单的 Brute-Force模式匹配算法,所以感觉在串这一章应该很简单的就复习完了,现在不由的感慨,井底之蛙不知天大。。。。。


 Brute-Force模式匹配算法,当然是轻车熟路,半个小时重新学习后,能够独立的写出完整的代码,简单随意。


但是KMP模式匹配算法,愣是让我好好的琢磨了整整两天,才算是明白了七七八八,之前一直感觉眼前有层纱,看不清真相。。。。


文字描述


在描述之前,我们先做个约定,因为串的匹配算法是找目标串在已存在的串中出现的位置(下标),我们把要找的下标所在的串称之为主串S,目标串称之为子串t


那此时的串的匹配就可称为:寻找子串在主串中出现的位置


有两个变量(整型变量或者指针变量)记录当前比对的主串(i)和子串(j)的字符对应的下标


匹配的过程,其实类似于游标卡尺的读数的过程(当然不完全一致,只是再次举例做类比),每次移动子串,和主串进行比较,直到找到某一满足条件的位置,或者找不到,也就是匹配失败。

数据结构Java实现——③串--->串的模式匹配:Brute-Force算法和 KMP算法_第1张图片


1、Brute-Force模式匹配算法

关于这个算法我不想做过多的描述,因为比较简单,比如从abababc(主串)中寻找abc(子串)的步骤如图


数据结构Java实现——③串--->串的模式匹配:Brute-Force算法和 KMP算法_第2张图片

从主串的某一个字符开始,与子串的第一个字符进行比较,

如果相等,就比较二者对应的下一个字符,也就是Si=tj,i++;j++;

如果不相等,那么就让主串的下一个字符与子串的第一个字符进行比较

也就是一旦出现Si≠tj,匹配失败,那么此时要向右移动子串,并修改记录主串和子串的变量i=i-j+1,j=0;

直到匹配成功,返回子串在主串中出现的位置(i - j),或者主串中剩余未比较的字符串的长度(S.length)小于子串的长度(t.length)(肯定是不可能有匹配的情况了)返回-1。


优点是:简单,容易理解,容易实现
缺点是:需要匹配的次数太多,每次 i 和 j 的移动距离过大,存在大量不必要的比较

2、KMP模式匹配算法


     (1)总体介绍


在上面的Brute-Force算法的基础上进行了改进,改进后的算法,可以在匹配失败后,无需修改记录主串当前比较的字符的下标  i;而且,记录子串当前比较的字符的下标 j也不用清零。而是在已经比较过的字符的基础上,针对子串本身的特点,移动子串,以达到减少比较次数的目的


(2)详细描述


当某次匹配不成功时,主串s的当前比较位置i不用回退,此时主串Si 可以直接和子串中的某个tk进行比较,此处的k的确定与主串无关,只有子串本身的构有关,即从子串本身就可以计算出k的值


现在讨论一般情况。

假设主串S=“s0s1s2s3...s(n-1)”, 子串t=“t0t1t2...t(m-1)”,从主串S中某一个位置开始比较,当匹配不成功(si≠tj)时,此时一定存在

“s0s1s2s3...s(i-1)”=“t0t1t2...t(j-1)” ----->(等式1)


①如果子串中不存在满足下列式子的  k值


“t0t1t2...t(k-1)”=“t(j-k)t(j-k+1)...t(j-1)” (其中0(等式2)


说明在子串“t0t1t2...t(j-1)”中不存在前缀t0t1t2...t(k-1)与主串s(i-j)s(i-j+1)...s(i-1)中的s(i-j)s(i-j+1)...s(i-1)子串相匹配,


下一次可以直接比较Si 和t0


②若子串中存在满足等式2,则说明在子串“t0t1t2...t(j-1)”中存在前缀t0t1t2...t(k-1)已经与主串s(i-j)s(i-j+1)...s(i-1)中的s(i-j)s(i-j+1)...s(i-1)子串相匹配


下一次可以直接比较Si和tk



比如从abcabdabcabc(主串)中寻找abcabc(子串)的步骤如图


数据结构Java实现——③串--->串的模式匹配:Brute-Force算法和 KMP算法_第3张图片



因此,当主串中的 Si 和子串中的 tj不匹配时,只需要将 Si 和 tk 比较即可,


也就是说此时问题的难点就在于如何求子串的每一个字符所对应的 k值


而此时选取k的原则是:子串中的前k个字符前缀子串等于 tj 之前的前k个字符子串,并且是具有此性质的最大子串的串长。


因为每一个字符 tj 都对应一个k值,而且这个k值仅与子串有关,与主串无关,因此我们可以在匹配 之前求出每一个字符所对应的 k值,记作next[ j ]


(3)next[ j ]的求法


在讲具体的求法之前,我想再重新强调一下next[ j ]函数的意义:


next[ j ]是:当子串 t j和主串s i  比较失败后,下一次和主串比较的子串的下标



next的求法:

数据结构Java实现——③串--->串的模式匹配:Brute-Force算法和 KMP算法_第4张图片

很显然next[ j ]函数中下一个值与当前值之间有关系,用递推法容易求解


下面讨论求next[ j ]函数的问题


由定义可知   初始时  next[ 0 ]= - 1;     next[ 1 ]=0


若存在next[ j ]=k,  则表明在子串中存在


“t0t1t2...t(k-1)”=“t(j-k)t(j-k+1)...t(j-1)” (其中0

 

其中,k为满足等式的最大值,那么对于next[ j+1 ]的值存在以下两种情况


(1)若 tk=tj,则表明在子串中存在


“t0t1t2...t(k-1)t(k)”=“t(j-k)t(j-k+1)...t(j-1)tj” (其中0


则next[ j+1 ]=next[ j ]+1=k+1


(2)若 tk≠tj,说明此时子串中:


“t0t1t2...t(k-1)t(k)”≠“t(j-k)t(j-k+1)...t(j-1)tj”


那么此时可以将求next[ j ]函数的问题看做是一个模式匹配问题子串既是主串也是子串


当前已经匹配的有“t0t1t2...t(k-1)”=“t(j-k)t(j-k+1)...t(j-1)” (其中0


则当t k≠tj 时,应该将子串t向右滑动到   k'=next[ k ],


(PS:为什么向右滑动到next[ k ]?? 

首先k不仅代表的是next[  j ]的值(PPS:已经求出的,子串 t j和主串s i  比较失败后,下一次和主串比较的子串的下标)

而且还代表了本次求next[ j ]匹配过程中,当前比较的"子串"的下标。

那么next[ k ]代表的含义是:当前比较失败后的下一次应该比较的子串的下标,正好符合


 并将k' 位置上的字符与“主串”中第j个位置上的字符进行比较


如图,求ababaaa的next[ j ]的过程



数据结构Java实现——③串--->串的模式匹配:Brute-Force算法和 KMP算法_第5张图片


代码实现


1、Brute-Force模式匹配算法

 


 


/**  
	 * @Title: indexOf_BF 
	 * @Description: TODO() 
	 * @param t
	 * @param begin
	 * @return
	 */
	public int indexOf_BF(SeqString t, int begin) {
		if (this != null && t != null && t.length() >= 0
				&& this.length() > t.length()) {
			int slen = this.length();
			int tlen = t.length();
			int i = begin;
			int j = 0;
			while (begin <= slen - tlen && j < tlen) {
				if (this.charAt(i) == t.charAt(j)) {
					i++;
					j++;
				} else {
					begin++;
					i = begin;
					j = 0;
				}
			}
			if (j >= tlen) {
				return begin;
			} else {
				return -1;
			}
		} else {
			return -1;
		}
	}



 


2、KMP模式匹配算法


2.1  next[ j ]函数实现

/**
	 * @Description: TODO(Next(j)函数,表示下标为j的子串的k值 ) 也就是子串下标j之前 满足
	 *               t0t1t2..t(k-1)=t(j-k+1)t(j-k+1)...t(j-1);的k的最大值
	 * @param T
	 * @return
	 */
	private int[] getNext(MString T) {
		int[] next = new int[T.length()];
		int j = 1;
		int k = 0;
		next[0] = -1;
		next[1] = 0;
		while (j < T.length() - 1) {
			if (T.charAt(j) == T.charAt(k)) {
				next[j + 1] = k + 1;
				j++;
				k++;
			} else if (k == 0) {
				next[j + 1] = 0;
				j++;
			} else {
				k = next[k];
			}
		}
		return next;
	}


2.2  KMP算法的实现

/**
	 * @Description: TODO(KMP算法 ) 指向主串的下标不往回移动, 而是根据子串本身的性质来移动子串,大大的减少了匹配的次数
	 * @param T
	 * @param begin
	 * @return
	 */
	public int indexOf_KMP(MString T, int begin) {
		int[] next = getNext(T);
		int i = begin;
		int j = 0;
		while (i < this.length() && j < T.length()) {
			if (j == -1 || this.charAt(i) == T.charAt(j)) {
				i++;
				j++;
			} else {
				j = next[j];
			}
		}
		if (j < T.length()) {
			return -1;
		} else {
			return (i - T.length());
		}
	}


后记


串这章的知识点终于算是总结完了,学完了串,不得不说,最初我的想法是幼稚的,一些看起来简单的东西一定有他不简单的地方,下面开始复习数组,希望继续像串这一章这样有那么大的收获



你可能感兴趣的:(JAVA,数据结构)