KMP算法及其改进
字符串匹配算法也就是从一个很长的字符串里面找出与我们手中的字符串相匹配的字符串(是这个大字符串的第几个字符开始),对于这个问题我们有很简单的解法,叫BF算法,Brute Force也就是蛮力的意思,充分依靠计算能力来解决问题的方法,对于这种解法可以用下面的图片来表述:
上面的算法就是BF算法,不好之处是效率太低了,因为就像第三趟比较中那样,我们只有最后一个元素没有匹配上就要从头再来,主串的对应元素竟然要回头从第四个元素开始比较,我们明明比较到了主串的第七个元素,前面的工作全部白费了。
KMP算法的想法减少我们比较时主串又回溯回去的这种现象,在KMP算法里,比较过的主串的元素(匹配成功)不会再进行比较,我们只是在失败的时候选中字串的某一个元素来和主串所对应的元素进行比较。最核心的就是next数组了,next数组对于比较失败时的情况给出了明确指引下一步我们应该拿子串的那一个元素再进行比较,这种指引是建立在对字串充分解析的基础之上的。
上面就是一个字串,下面的一行就是我们给出的next数组,用法也很简单,譬如说我们子串的第七个元素a与主串对应的元素进相比较失败了,由这一点可以说明,子串前面的六个元素都与主串匹配成功了,我们下一步就依照next数组的指示,用子串的第四个元素来与主串当中刚才没有匹配成功的那个元素比较,这样选择的依据是,我们把第四个元素与主串中刚才失败的那个元素对齐后,子串的第四个元素前面的元素都是匹配的。
next数组的产生,对于第一个元素a来说,next[1]永远都是0,意味着我们要用第0号元素与主串元素对其,但没有0号元素,也就是说我们要用1号元素与主串的下一个元素对齐,对于第二个元素b来说,我们假想他与主串匹配失败,用子串的第一个元素来对齐,这种情况直到第五个元素发生了变化,我们用第二个元素来对齐,可以看见子串的第一个元素a与主串的失败元素的前一个元素是完全匹配的都是a,这样相比较BF算法我们就少比较了一次,第六个元素C如果失败了,我们就用第三个元素C来与主串中的元素对齐,发现前面的ab两个元素不用比较了,再说子串第八个元素失败的情况,我们用子串的第五个元素与主串元素对齐,可以发现前面的abca四个元素是完全匹配的。
通过充分解析字串的内容,我们可以给出next数组来指引我们加快匹配过程,具体的怎么填写next数组有具体的程序,但这里还是要说一下我的思路,假设第一个元素a比较失败,没办法,我们只能将子串后移一下与主串在进行比较,填写0意味着我们放弃与主串的这个元素比较转而与主串的下一个元素进行比较,如果第二个元素b比较失败,我们发现它的前面只有一个a,我们就填1了,这里说的太牵强了,那第六个元素来说,当它失败的时候,我们发现子串中他前面的元素中 后缀ab(4,5)与前缀ab(1,2)是一样的,我们如果把第三个元素移到此处对齐时就会发现前两个元素已经完全匹配了,这就是基于我们对子串中失败元素前面(所有元素成为一个整体)的前缀和后缀相不相同分析而来的,因为我们如果依照next数组移动了,肯定就是原来子串中失败元素的前缀和后缀对齐在一起。前缀后缀都是来年各个元素,这里我们+1,因为对齐的话主串中失败元素正好对的是子串的第三个元素。
分析失败元素的前缀和后缀得到了next数组,前缀为从前开始的前几个元素的组合,后缀是左侧紧贴失败元素的几个元素的组合,例如 next[8]==5,是因为1234和4567都是abca,4+1==5;
当然这种方法还有值得改进的地方,如果我们按照next数组的指示换子串的另一个元素来比较,但这个元素与原来的子串中的那个元素是相等的,这样和主串比较不还是失败么!!!如子串中第六个元素是C,它比较失败了,我们按照next数组指示选择第三个元素来接他的班,但第三个元素与第六个元素相等都是C,不用想,依旧会比较失败,这里我们怎么办???我们要尽享以下判断,如果第六个元素与第三个元素不相等我们就用第三个元素,如果相等了,我们就用第三个元素对应的next成员来接替(也就是第一个元素a)
假如上面的不好理解的话,我们可以假设我们傻啦吧唧的用了第三个元素C来对齐比较,结果不出所料,失败了,我们又会用第三个元素对应的next元素来与之对其进行比较——————我们上面做的只不过是提前了一步,投石问路,少走了一步弯路而已。
因为next数组是从前往后依次建立的,上面的做法最终使得第i个元素与它对应的next[i]总不会是相同的,这样避免了可预料的重复失败,进一步加快了匹配速度,按照这种思想建立如下:
注意上面的next数组,其实少了一个元素,我们没有看见next[0]但是在实际的next数组中这个元素肯定是存在的,这样的话我们的next数组要比子串长度大一,因为next[0]永远都不会被用到,所以我们也就不对他进行赋值了,但他还是存在的。
下面贴一下改进后的kmp_next数组产生程序:
void get_next(HString S, int next[]) { int i = 1, j = 0; HString subs1, subs2; next[1] = 0;//这是肯定的,next[0]我们没有处理 while (i < S.StrLength())//如果后缀i还没有到达末尾 { S.SubString(subs1, i, 1);//这就是取S的第i个字符的意思 S.SubString(subs2, j, 1); //因为j等于0,所以就不会返回什么东西了 /* 下面的处理函数当进入if的时候也就是说前缀和后缀字符相等,通常情况下我们进行的操作就是 i++,j++,next[i]=j;意味着当我们在后缀i这个地方发生不匹配的时候,我们可以使用第j个元素进行匹配,但是呢我们没有考虑到的一个问题就是,如果当前的(增加后的)后缀i(潜在发生不匹配的元素)与要接替他的元素(增加后的J)是相等的该怎么办呢?如果相等,也就是说还是会不匹配,假设我们就真的写了 next[i]=j;因为第i个元素与要接替他的第j个元素是相等的,我们很快就会用第j个元素所对应的next元素来接替第j个元素,所以呢,我们提前考虑一步,如果当前的i与要接替他的元素j是相等的,那我们就跨过一步,直接让要接替j的元素来接替i,如果不想等,那么我们可以放心的接替也就是直接让 next[i]=j; */ //看看上面取到的字符相不相等,至于j==0这是实际真的会发生的,此时进入if的原因就不//是后面的相等了,而是0==j这一条件。 if (0 == j || subs1.StrCompare(subs2) == 0) { ++i; ++j; S.SubString(subs1, i, 1); S.SubString(subs2, j, 1); if (subs1.StrCompare(subs2) != 0) next[i] = j; else next[i] = next[j]; } else //发生不匹配,那我们就用j所对应的next元素来接替他接着进行比较 { j = next[j]; } } }
我们按照上面给出的S再推一遍,首先i指向a,j==0指向第0个元素(S没有第0号元素),接下来就是next[1]==0;这是永远都成立的,也就是说要放弃与主串中的当前比较失败元素的继续比较转而与主串的下一个元素进行比较,然后我们看到了一个if条件问相不相等,怎么会相等?S都没有第0号元素我们怎么取它?所以subs2什么也没有取回来,好在if在j==0的时候也可以进入,我们将i和j都加加,i现在是2,j现在是1,再取出对应的第X个字符,两者不相等,我们应该next【i】=j;这也就是普通的KMP算法的做法,甚至都不比较相不相等,如果两者相等的话我们就执行next【i】=next【j】这里有点递归的意思,因为上面已经说明了为什么这样所以就不再解释了,这样next【2】=1了,然后再次看看能不能进入if,结果进不去了,这个时候就执行else的 j=next[j]了,j也就变成了next【1】也就是0而i却没有发生变化i还是2,j变成了0,在试试if果断进去了因为j==0,然后加加,i是3,j是1,取字符,结果不相等,next【i】=next【j】,next[3]==1,然后出去发现依然不相等,j=next[j],j变成了0,进入大if,i为4,j是1,二者相等,相等的话next【i】=next【j】也就是0,然后出去,在进入if,i是5,j是2,也相等next【i】=next【j】这样next【5】=1,又进入if,i为6,j是3,相等,next【6】=1,又进入,i为7,j是4,又相等,next【7】=0,再一次进入,i为8,j是5,不相等,next【8】=j也就是5,然后进不去了,j=next【j】也就是1,i为8,j是1,不相等,j变成了next[J]==0,成功进入,i为9,j是1,相等,next【9】=next【1】==0然后判断 i < S.StrLength() 此时是相等的,所以跳出while,next数组产生完毕。
然后下面再贴一下怎么使用next数组:
//S是模式串,T是主串,pos是说在T中第几个元素之后开始搜索,next就是我们要传入的next数组 //其中S非空,POS大于等于1,小于主串的长度 int Index_KMP(HString T, HString S, int pos, int next[]) { int i = pos,j = 1;// HString subs1, subs2; while (i <= T.StrLength() && j <= S.StrLength())//如果主串和子串都没有比较完毕,我们还需要继续比较。 { T.SubString(subs1, i, 1);//取主串的第i个字符 S.SubString(subs2, j, 1);//取子串的第j个字符 if (j == 0 || subs1.StrCompare(subs2) == 0) { ++i; ++j;//相等的话就都加1,在比较下一对字符,j==0的进入条件是为了下面else产生的j=next[j]==0,这样i增加了j却没有增加 } else j = next[j];//再从子串的第next[j]个元素开始比较,也就是当j是0的时候,我们放弃与主串的当前元素比较,转而与主串下一个元素进行比较,所以 //上面有i++,j++,i加加意味着主串比较元素后移一位,而j++却变成了1,我们要用子串的第一个元素和主串元素比较,这不就是开始新一轮的比较么? } if (j > S.StrLength()) { return i - S.StrLength();//就是说从主串的这个元素开始我们发现了匹配的子串 } else return 0; }
我们使用的时候,要制定从主串的第几个元素开始进行寻找,也就是设置的pos值,T和S分别是主串和子串,next是我们根据子串T已经产生好的next数组,实际上穿的是数组的名字,也就是地址。void get_next(HString S, int next[])是得到next数组的函数,我们传进去子串S以及我们自己动态分配好了的next数组,例如可以这样写 char*p=new char[S.StrLength()+1],我们传进去的实际上就是p,至于为什么p数组的长度比S的长度大1,上面已经解释过了就不再说了。
顺便说一句KMP算法的时间复杂度是:O(m+n)