【问题】给定两个字符串 S 和 T,在主串 S 中查找子串 T 的过程称为串匹配( stringmatching,也称模式匹配),T 称为模式。在文本处理系统、操作系统、编译系统、数据库系统以及 Internet 信息检索系统中,串匹配是使用最频繁的操作。
串匹配问题具有下面两个明显的特征:
(1)问题的输入规模很大,常常需要在大量信息中进行匹配,因此,算法的一次执行时间不容忽视;
(2)匹配操作经常被调用,执行频率高,因此,算法改进所取得的效益因积累往往比表面上看起来要大得多。
【想法 1】应用蛮力法解决串匹配问题的过程是:从主串 S 的第一个字符开始和模式 T 的第一个字符进行比较,若相等,则继续比较二者的后续字符;若不相等,则从主串 S 的第二个字符开始和模式 T 的第一个字符进行比较,重复上述过程,若 T 中的字符全部比较完毕,则说明本趟匹配成功:若 S 中的字符全部比较完毕,则匹配失败。这个算法称为朴素的模式匹配算法,简称BF算法,如下图所示。
设主串 S="abcabcacb",模式 T="abcac",BF 算法的匹配过程如下图所示。
【算法 1】设字符数组 S 存放主串,字符数组 T 存放模式,BF算法用伪代码描述如下。
算法:串匹配算法 BF
输入:主串 S,模式T
输出:T 在 S 中的位置
1.初始化主串比较的开始位置 index=0;
2.在串 S 和串 T 中设置比较的起始下标 i=0,j=0;
3.重复下述操作,直到 S 或 T 的所有字符均比较完毕:
3.1 如果 S[ i ] 等于T[ j ],则继续比较 S 和 T 的下一对字符;
3.2 否则,下一趟匹配的开始位置 index++,回溯下标 i=index,j=0;
4.如果 T 中所有字符均比较完,则返回匹配的开始位置 index;否则返回 0;
【算法分析 1】设主串 S 长度为 ,模式 T 长度为 ,在匹配成功的情况下,考虑最坏情况,即每趟不成功的匹配都发生在模式 T 的最后一个字符。
例如:S= "aaaaaaaaaaab"
T="aaab"
设匹配成功发生在 处,则在 趟不成功的匹配中共比较了 次,第 趟成功的匹配共比较了 次,所以总共比较了 次,因此平均比较次数是:
一般情况下,,因此最坏情况下的时间复杂性是 。
【算法实现 1】BF 算法用JAVA语言描述如下:
public class BruteForce {
static int BF(char S[], char T[])
{
int index = 0; //主串从下标0开始第一趟匹配
int i = 0, j = 0; //设置比较的起始下标
while ((S[i] != '\0') && (T[j] != '\0'))
{
if (S[i] == T[j]) {i++; j++;}
else {index++; i = index; j = 0; } //i和j分别回溯
}
if (T[j] == '\0') return index + 1; //返回本趟匹配的开始位置(不是下标)
else return 0;
}
public static void main(String args[]){
String S = "abcabcabcaccb\0";
char[] s=S.toCharArray();
String T = "abcacc\0";
char[] t=T.toCharArray();
int index=BF(s,t);
System.out.println("T在S中的位置是:"+index);
}
}
运行结果如下 1:
【想法 2】分析 BF 算法的执行过程,造成 BF 算法效率低的原因是回溯,即在某趟匹配失败后,对于主串 S 要回溯到本趟匹配开始字符的下一个字符,模式 T 要回溯到第一个字符,而这些回溯往往是不必要的。观察 BF 算法的执行过程,在第 1 趟匹配过程中,S[ 0 ] ~ S[ 3 ] 和 T[ 0 ] ~ T[ 3 ]匹配成功,S[ 4 ] ≠ T[ 4 ] 匹配失败,因此有了第 2 趟。因为在第 1 趟中有 S[ 1 ] = T[ 1 ],而 T[ 0 ] ≠ T[ 1 ],因此有 T[ 0 ] ≠ S[ 1 ],所以第 2 趟是不必要的,同理第 3 趟也是不必要的,可以直接到第 4 趟。进一步分析第 4 趟中的第一对字符 S[ 3 ] 和 T[ 0 ] 的比较是多余的,因为第 1 趟中已经比较了 S[ 3 ] 和 T[ 3 ],并且 S[ 3 ] = T[ 3 ],而 T[ 0 ] = T[ 3 ],因此必有 S[ 3 ] = T[ 0 ],因此第 4 趟比较可以从第二对字符 S[ 4 ] 和 T[ 1 ] 开始进行,这就是说,第 1 趟匹配失败后,下标 i 不回溯,而是将下标 j 回溯至第 2 个字符,从 T[ 1 ] 和 S[ 4 ] 开始进行比较。
综上所述,希望某趟在 S[ i ] 和 T[ j ] 匹配失败后,下标 i 不回溯,下标 j 回溯至某个位置 k,从 T[ k ] 和 S[ i ] 开始进行比较。显然,关键问题是如何确定位置 k?
观察部分匹配成功时的特征,某趟在 S[ i ] 和 T[ j ] 匹配失败后,下一趟比较从 S[ i ] 和 T[ k ]开始,则有 T[ 0 ] ~ T [ k - 1 ] = S[ i - k ] ~ S[ i - 1 ]成立,如下图 (a) 所示;在部分匹配成功时,有T [ j - k ] ~ T[ j - 1 ] = S [ i - k ] ~ S [ i - 1 ] 成立,如下图 (b) 所示。
由 T[ 0 ] ~ T[ k - 1 ] = S[ i - 1 ] 和 T[ j - k ] ~T[ j - 1 ] = S[ i - k ] ~S[ i - 1 ],可得:
T[ 0 ] ~ T[ k - 1 ] = T[ j - k ] ~ T[ j - 1 ]
上式说明,模式中的每一个字符 T[ j ] 都对应一个 k 值,这个 k 值仅依赖于模式本身,与主串无关,且 T[ 0 ] ~ T[ k - 1] 是 T[ 0 ] ~ T[ j - 1 ] 的真前缀,T [ j - k ] ~ T[ j - 1] 是 T[ 0 ] ~ T[ j - 1 ] 的真后缀,k 是 T[ 0 ] ~ T[ j - 1 ] 的真前缀和真后缀相等的最大子串的长度。用 next[ j ] 表示 T[ j ] 对应的 k 值 ( 0<= j < m ,其定义如下:
设模式 T="ababc",根据 next[ j ] 的定义,计算过程如下:
j = 0 时,next[ 0 ] = -1
j = 1 时,next[ 1 ] = 0
j = 2 时,T[ 0 ] ≠ T[ 1 ],则next[ 2 ] = 0
j = 3 时,T[ 0 ] T [1 ] ≠ T[ 1 ] T[ 2 ],T[ 0 ] = T[ 2 ],则next[ 3 ] = 1
j = 4 时,T[ 0 ] T[ 1 ] T[ 2 ] ≠ T[ 1 ] T[ 2 ] T[ 3 ],T[ 0 ] T[ 1 ] = T[ 2 ] T[ 3 ],则next[ 4 ] = 2
设主串S="ababaababcb",模式T="ababc" ,模式 T 的 next 值为 {-1,0,0,1,2},改进的串匹配算法(称为KMP算法)的匹配过程如下图所示。
【算法 2】在求得了模式 T 的 next 值后,KMP 算法用伪代码描述如下。
算法:串匹配算法KMP
输入:主串 S,模式 T
输出:T 在 S 中的位置
1.在串 S 和串 T 中分别设置比较的起始下标 i = o,j = 0;
2.重复下述操作,直到 S 或 T 的所有字符均比较完毕:
2.1 如果S[ i ] 等于 T[ j ],则继续比较 S 和 T 的下一对字符;
2.2 否则,将下标 j 回溯到 next[ j ] 位置,即 j = next[ j ];
2.3 如果 j 等于-1,则将下标 i 和 j 分别加 1,准备下一趟比较;
3.如果 T 中所有字符均比较完毕,则返回本趟匹配的开始位置;否则返回0;
【算法分析2】在求得模式 T 的 next 值后,KMP 算法只需将主串扫描一遍,设主串的长度为 n,则 KMP 算法的时间复杂性是 O(n) 。
【算法实现2】可以用蛮力法求得模式 T 的 next 值,KMP 算法用JAVA语言描述如下:
public class KMPSF {
int KMP(char S[],char T[])
{
int i = 0, j = 0;
int [] next=new int[80];
next[0]=-1;
GetNext(T,next);
while (S[i] != '\0' && T[j] != '\0')
{
if(S[i] == T[j])
{
i++; j++;
}
else
{
j = next[j];
if (j == -1) {i++; j++;}
}
}
if(T[j] == '\0') return i - T.length + 1;
else return 0;
}
void GetNext(char T[ ], int next[ ])
{
int j = 0, k = -1;
next[0] = -1;
while (T[j] != '\0') //直到字符串末尾
{
if (k == -1) { //无相同子串
next[++j] = 0; k = 0;
}else if (T[j] == T[k]) { //确定next[j+1]的值
k++;
next[++j] = k;
} else k = next[k]; //取T[0]...T[j]的下一个相等子串的长度
}
}
public static void main(String args[]){
char S[]={'a','b','a','b','c','a','b','c','a','c','b','a','b','\0'};
char T[]={'a','b','c','a','c','\0'};
KMPSF kmpsf=new KMPSF();
int index= kmpsf.KMP(S,T);
System.out.println("T在S中的位置是:"+index);
}
}
运行结果如下 2:
from:算法设计与分析(第2版)——王红梅 胡明 编著——清华大学出版社