串的模式匹配
概念
就是找到模式串在主串中从第pos个字符后首次👈出现的位置
串的模式匹配算法👇
BF算法🎈
思路
Brute-Force算法又称“蛮力匹配”算法,从主串S的第pos个字符开始,和模式串T的第一个字符比较,若相等,则继续逐个比较后续字符;遇到不相等的情况就回溯到主串第pos + 1 个字符,重新开始和模式串T的第一个字符及后续字符进行逐个比较。以此类推,直到模式串T中的每一个字符依次和主串S中的一个连续字符串全部相等,则模式匹配成功,此时返回模式串T的第一个字符在主串S的位置;否则主串中没有和模式串相等的字符串,称模式匹配不成功,返回0
实现
int Index(SString S , int pos , SString T)
{
int i = pos , j = 1; //主串从第pos位置开始,模式串从第1个位置开始
while (i <= S.len && j <= T.len)
{
if (S.ch[i] == T.ch[j]) //当对应字符相等时,比较后续字符
{
i++;
j++;
}
else //当对应字符不相等时
{
i = i - j + 2; //主串回溯到 i - j + 2 的位置重新比较
j = 1; //模式串从头开始重新比较
}
}
if (j > T.len) //匹配成功时,返回匹配的起始位置
return i - T.len;
else
return 0; //匹配失败时,返回 0
}
- 例1:
S : Hello World!
T : llo
返回 :3
- 例2:
S : oooool
T : ol
返回 :5
注意:为了方便理解,串的位序都是从 1 开始的
效率
在最坏的情况下,即每个子串的前m - 1 个字符都和模式串匹配,只有第 m 个字符不匹配时,BF算法的时间复杂度为 O(n×m),其中n和m分别是主串和模式串的长度。这个算法的缺点是:不匹配时,主串的扫描指针经常回溯,会造成重复的比较而导致时间开销增加👀。
KMP算法🚩
思路
每当一次匹配过程中出现字符不匹配的情况时,主串的指示指针 i 不用回溯,而是利用已经得到的“部分匹配”结果将模式串向右“滑动”尽可能远的距离后继续进行比较
实现
KMP算法是在已知模式串的next 数组的值的基础上执行的,所以需要先实现 next[] 数组,这是实现KMP算法的一个关键点也是难点。
引入两个前置概念:
-
串的前缀:包含第一个字符且不包含最后一个字符的子串
-
串的后缀:包含最后一个字符且不包含第一个字符的子串
next数组的手算方法:当第 j 个字符匹配失败,由前 1 ~ j-1 个字符组成的串记为 Q ,则 next [ j ] = Q 的最长公共前后缀(即前后缀的相等)的长度 + 1
例:给一模式串:ababaa,则它的next数组的每个元素值如下:
序号 j | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
模式串 | a | b | a | b | a | a |
next [ j ] | 0 | 1 | 1 | 2 | 3 | 4 |
当j = 1 时,默认都为 0 ,则 next[ 1 ] = 0;
当 j = 2 时,那就看 1 ~ j - 1 那段字符的公共前后缀,观察可知此时没有公共前后缀,则next[ 2 ] = 0 + 1 = 1 ;
当 j = 3时,此时没有公共前后缀,则next[ 3 ] = 0 + 1 = 1 ;
当 j = 4时,此时的公共前后缀为 a ,长度为1,则next[ 4 ] = 1 + 1 = 2 ;
当 j = 5时,此时的公共前后缀为 ab ,长度为2,则next[ 5 ] = 2 + 1 = 3 ;
当 j = 6时,此时的公共前后缀为 aba ,长度为3,则next[ 6 ] = 3 + 1 = 4 。
//求模式串T的next数组
void get_next(SString T , int next[])
{
int i = 1 , j = 0;
next[1] = 0;
while(i < T.len)
{
if(j == 0 || T.ch[i] == T.ch[j])
{
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
//KMP 算法
int Index_KMP(SString S , int pos , SString T)
{
int i = pos , j = 1; //主串从pos 开始,模式串从头开始
int next[T.len + 1];
get_next(T,next); //求模式串的next数组
while(i <= S.len && j <= T.len)
{
if(j == 0 || S.ch[i] == T.ch[j]) // 继续比较后续字符
{
i++;
j++;
}
else
j = next[j]; //模式串的指示指针 j 移动,即模式串向后移动
}
if(j > T.len)
return i - T.len; //匹配成功,返回匹配的起始位置
else
return 0; //匹配失败,返回 0
}
效率
KMP算法的不同点是,当主串与模式串不匹配时,主串指针 i 不回溯,只移动模式串的指针 j 。求模式串的next数组时间复杂度为O(m),再加上KPM算法里的循环,整个算法的时间复杂度为O(m + n)
优化
上述定义的next函数在某些情况下尚有缺陷。假设主串为 ‘goolggoogle’,模式串为‘google’
当发现第四个字符不匹配的时候,按照next数组,j 会回退到 1 ,即主串中的第四个字符 ‘l’ 会和模式串的第一个字符 ‘g’ 匹配,但是我们发现,第一个字符和前一个字符是一样的都是 ‘g’ ,所以当前主串字符 ‘l’ 和第四个字符不匹配,那也不会和第一个字符匹配,所以这次比较是毫无意义的💔。
处理的方式就是优化数组,使用nextval数组去替代next数组
再看一个例子📄:
这个模式串的next数组如下:
在写nextval数组时,从左往右开始:
nextval [ 1 ] = 0 是固定的
第二个字符和第一个字符相等,所以nextval [ 2 ] = nextval [ 1 ] = 0
第三个字符和第二个字符相等,所以nextval [ 3 ] = nextval [ 2 ] = 0
第四个字符和第三个字符相等,所以nextval [ 4 ] = nextval [ 3 ] = 0
第五个字符和第四个字符不相等,所以nextval [ 5 ] = next [ 5 ] = 4 不变
所以nextval数组如下:
nextval数组的求法📌:
先算出next数组且令nextval[1] = 0
for (int j = 2 ; j <= T.len ; j++)
{
if (T.ch[next[j]] == T.ch[j])
nextval[j] = nextval[next[j]];
else
nextval[j] = next[j];
}
所以KMP算法的优化就是next数组的优化:当子串和模式串不匹配时 j = nextval [ j ];