1)串的模式 匹配的概念
设有两个串s和t,要在串s中找到与t相等的子串。通常将s称为目标串,t称为模式串,这种串的定位查找也称为模式匹配。
2)串的模式匹配算法有几种?具体说明。
常见的串的模式匹配算法有4种。
1.BF算法:
BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法。
2.KMP算法:
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。KMP算法是各大教科书上的看家绝学,曾被投票选为当今世界最伟大的十大算法之一;但是晦涩难懂,并且十分难以实现。
3.BM算法:
在计算机科学里,Boyer-Moore字符串搜索算法是一种非常高效的字符串搜索算法。它由Bob Boyer和J Strother Moore设计于1977年。此算法仅对搜索目标字符串(关键字)进行预处理,而非被搜索的字符串。虽然Boyer-Moore算法的执行时间同样线性依赖于被搜索字符串的大小,但是通常仅为其它算法的一小部分:它不需要对被搜索的字符串中的字符进行逐一比较,而会跳过其中某些部分。通常搜索关键字越长,算法速度越快。它的效率来自于这样的事实:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。
4.Sunday算法:
Sunday算法是Daniel M.Sunday于1990年提出的字符串模式匹配。其核心思想是:在匹配过程中,模式串发现不匹配时,算法能跳过尽可能多的字符以进行下一步的匹配,从而提高了匹配效率。目前发现的最高效且容易理解的算法。
3) 串的BF模式匹配算法思路是?如何实现?效率如何?
算法思路: 简单暴力的一个算法,如果遇到字符不匹配,主串i指针回溯到本次匹配位置的下一个位置,而模式串则重新回到0(开始的位置),开始下一轮的匹配。 成功匹配的条件是模式串指针j走到头,也就是j=length-1,length为模式串长度。跳出循环表示为j=length
代码实现:
int mate(char* B, char* A) {
int i = 0, j = 0;
while (i < strlen(B) && j < strlen(A)) {
if (B[i] == A[j]) {
i++;
j++;
}
else {
//匹配失败时,i 向后移动一位,j 重置
i = i - j + 1;
j = 0;
}
}
//跳出循环有两种可能,i=strlen(B)说明已经遍历完主串,匹配失败;j=strlen(A),说明模式串遍历完成,在主串中成功匹配
if (j == strlen(A)) {
return i - strlen(A) + 1;
}
//运行到此,为 i==strlen(B) 的情况,模式匹配失败
return -1;
}
时间复杂度:
例:S='0000000001',T= '0001',pos=1
若n为主串长度,m为子串长度,最坏情况是
主串前面n-m个位置都部分匹配到子串的最后一位,即这n-m位各比较了m次
最后m位也各比较了1次
总次数为:(n-m)*m+m=(n-m+1)*m
若m<<n,则算法复杂度为O(n*m)
4)串的KMP模式匹配算法思路是?如何实现?效率如何?
算法思路:KMP算法中,每当一趟匹配过程中出现失配时,主串S中的i指针不需要回溯,而是利用已经得到的“部分匹配结果”,将模式串向右“滑动”尽可能远的一段距离后,继续进行比较,从而快速达到匹配结果。
代码实现:
int Index_KMP(char S[],int pos,char T[],int next[])
{
int i,j;
while(i<=strlen(S)&&j<=strlen(T))
{
if(j==0||S[i]==T[j])
{
i++;
j++;
}
else
{
j=next[j];
}
if(j>strlen(T))
return i-strlen(T);
else
return 0;
}
}
时间复杂度:
假设m为模式串strM的长度,n为待匹配的字符串strN的长度。
O(m+n)=O( [m,2m]+ [n,2n] ) = 计算next数组的时间复杂度+遍历比较的复杂度。
也就是:
计算next数组时的比较次数介于[m,2m]。
遍历比较的比较次数介于[n,2n],最坏情形形如T=“aaaabaaaab”,P=“aaaaa”。
所以算法时间复杂度是O(m+n).
NEXT数组求解:
int GetNext(char T[],int next[])
{
next[1]=0;
int i=1,j=0;
while(i<=strlen(T))
{
if(j==0||T[i]==T[j])
{
++i;
++j;
next[i]=j;
}
else j=next[j];
}
}
例:T= a b a a b c a b a
1 2 3 4 5 6 7 8 9
位置 1 上的元素 a
前面没有子串,因此这里我们令 next[1] = 0
;
位置 2 上的元素 b
,它前面的字符串为 a,字符串 a 没有最大相等前缀和后缀(注意:最大相等前缀后缀不包括自身),因此 next[2] = 1
;
位置 3 上元素的 a
, 它前面的字符串为 ab
,字符串 ab
没有最大相等前缀和后缀,因此next[3] = 1
;
位置4上元素的a
, 它前面的字符串为aba
,字符串aba
的最大相等前缀和后缀为a
,因此next[4] = 2
;
位置5上元素的b
, 它前面的字符串为abaa
,字符串abaa
的最大相等前缀和后缀为a
,因此next[5] = 2
;
位置 6 上的元素 c
, 它前面的字符串为 abaab
,字符串 abaab
的最大相等前缀和后缀为 ab
,因此 next[6] = 3
;
位置 7 上的 元素a
, 它前面的字符串为 abaabc
,字符串 abaabc
的没有最大相等前缀和后缀,因此 next[7] = 1
;
位置 8 上的 元素b
, 它前面的字符串为 abaabca
,字符串 abaabca
的最大相等前缀和后缀为 a
,因此 next[8] = 2
;
位置 9 上的 元素a
, 它前面的字符串为 abaabcab
,字符串 abaabcab
的最大相等前缀和后缀为 ab
,因此 next[9] = 3
。
通过以上步骤,我们便可得到整个next
数组的值,其余T的对应关系如下:
T= a b a a b c a b a
next = 0 1 1 2 2 3 1 2 3
如何通过代码求解:
对于模式串的位置j
,有next[j] = k
,例如取j = 4
,则有next[4] = 1
;则对于模式串的位置j + 1
,有以下两种情况:
若 p[k] == p[j]
,即p[1] == p[4]
,则有next[j + 1] = next[j] + 1
;
若 p[k] != p[j]
,则令k = next[k]
,若 p[k] == p[j]
,next[j + 1] = k + 1
,否则重复此过程程。
5)KMP模式匹配算法如何改进?nextval值如何求解?举例说明。
改进原因:KMP的next数组只分析了当前字符之前的字符串的相似度,而没有把当前字符考虑进去, 从而导致上述没有意义的比较操作。
那么如何才能把当前字符也考虑进去呢?
基本原理就是,在需要子串指针回溯时,进行当前位置元素与回溯之后位置元素比较,如果相等,那么就没有必要再进行比较了,子串的指针继续回溯。如此往复
因此,改进的KMP算法又添加了一个数组nextval, 它是在next基础之上计算出来的。
Nextval数组求解:
int GetNextval(char T[], int nextval[]){
i=1;
j=0;
nextval[1]=0;
while(i < strlen(T){
if(j==0 || T[i] == T[j])
{
++i;
++j;
if(T != T[j])
nextval[i]=j; //当前的j值为nextval在i位置的值
else
nextval[i]=nextval[j]; //前缀字符==后缀字符,将前缀字符的nextval值赋值给 nextval在i位置的值
}
else
j=nextval[j]; //字符不同,则左侧下标回溯
}
}
例:
T= a b a a b c a b a
next = 0 1 1 2 2 3 1 2 3
nextval = 0 1 0 2 1 3 0 1 1
nextval数组第一个数直接为0。
nextval第二数:模式串第二个字符为b,对应的下标数组第二个数是1,那就是将模式串的第1个字符和b相比较,a!=b,所以直接将下标数组第二个数1作为next-val数组第二个数的值
第三个数:模式串第三个字符为a,对应下标数组第三个数为1,取其作为下标,找到模式串第1个字符为a,a=a,那取nextval的第一个数做为nextval第三个数的值,也就是0 。
第四个数:模式串第四个字符为a,对应下标数组第四个数为2,取其作为下标,找到模式串第2个字符为b,a!=b,所以直接将下标数组第四个数2作为nextval数组第四个数的值
以此类推可求得其余nextval数组的值如上。