写在前面
其实关于串的模式匹配问题,我最初的了解并不深,只知道一个最简单的 Brute-Force模式匹配算法,所以感觉在串这一章应该很简单的就复习完了,现在不由的感慨,
Brute-Force模式匹配算法,当然是轻车熟路,半个小时重新学习后,能够独立的写出完整的代码,简单随意。
但是KMP模式匹配算法,愣是让我好好的琢磨了整整两天,才算是明白了七七八八,之前一直感觉眼前有层纱,看不清真相。。。。
文字描述
在描述之前,我们先做个约定,因为串的匹配算法是找目标串在已存在的串中出现的位置(下标),我们把要找的下标所在的串称之为主串S,目标串称之为子串t。
那此时的串的匹配就可称为:寻找子串在主串中出现的位置。
有两个变量(整型变量或者指针变量)记录当前比对的主串(i)和子串(j)的字符对应的下标。
匹配的过程,其实类似于游标卡尺的读数的过程(当然不完全一致,只是再次举例做类比),每次移动子串,和主串进行比较,直到找到某一满足条件的位置,或者找不到,也就是匹配失败。
1、Brute-Force模式匹配算法
关于这个算法我不想做过多的描述,因为比较简单,比如从abababc(主串)中寻找abc(子串)的步骤如图
从主串的某一个字符开始,与子串的第一个字符进行比较,
如果相等,就比较二者对应的下一个字符,也就是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<k<j) —->(等式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(子串)的步骤如图
因此,当主串中的 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的求法:
很显然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<j)
其中,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<k<j)
则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<k<j),
则当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 ]的过程
代码实现
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()); } }
后记
串这章的知识点终于算是总结完了,学完了串,不得不说,最初我的想法是幼稚的,一些看起来简单的东西一定有他不简单的地方,下面开始复习数组,希望继续像串这一章这样有那么大的收获