串的模式匹配:就是子串的定位操作。
一、BF算法
(一)算法原理
从主串S的第pos个字符起和子串的第一个字符比较之,若相等,则逐个比较后续字符;若不相等,则从主串的下一个字符起再重新和子串的字符比较之。依次类推,直至子串T中的每一个字符依次和主串S中的一个连续的字符序列相等,则称匹配成功,函数值为子串T中的第一个字符在主串S中的序号,否则称匹配不成功,函数值为零。


(二)C语言实现
cpp
int BF_Search(const char* MainStr, const char* SubStr)
{
//1. 申请两个指针i和j, 分别指向主串和子串的起始位置
int i = 0;
int j = 0;
//2. 进入while循环, 循环条件是i和j合法(主串指针i没有遍历完主串 并且 子串指针j没有遍历完子串)
while (i < strlen(MainStr) && j < strlen(SubStr))
{
//3. 比较i和j指向的字符, 如果相同, 则让i和j同步向后移动一个位置
if (MainStr[i] == SubStr[j])
{
i++;
j++;
}
//4. 如果不相同, 匹配失败,需要回退指针i和j
else
{
i = i - j + 1; //主串指针i回退到本次匹配起始位置的下一位置
j = 0; //字串指针j回退到开始位置(0下标)
}
}
//5. 判断子串是否全部匹配完成
if (j < strlen(SubStr)) //子串没有遍历完,说明主串没有找到子串,返回-1
{
return -1;
}
else //子串全部匹配成功
{
return i - j;
}
}
(三)复杂度分析
普遍情况 :时间复杂度为O(n+m),其中n为主串长度,m为要匹配的字串长度;
最好情况 :就是一开始就匹配成功,比如"googlegood"中去找"google",时间复杂度为O(1);
最坏情况 :就是每次不成功的匹配都发生在子串的最后一个字符,比如"000000001"中去找"00001",时间复杂度为O((n-m+1)*m)
二、KMP算法
(一)算法原理
1.为什么需要KMP算法
BF算法,每次字符匹配失败时,主串指针i需要回退,字串指针j重置为0,从头开始匹配;这种算法会导致大量重复的字符比较,在长字符串匹配中效率很低。
2.KMP的核心思想
主串指针i只向前移动,永远不回退;匹配失败时,只调整子串指针j到合适的位置,避免重复比较。它的实现完全依赖于子串自身的结构信息,具体来说就是利用子串的最长相等前后缀。
相较于BF算法,KMP算法的改进在于:每当一趟匹配过程中出现字符比较不等时,不需要回溯 i 指针,而是利用已经得到的"部分匹配"的结果将模式向右"滑动"尽可能远的一段距离后,继续进行比较。
(二)next数组值推导
1.推导过程
(1)j=0时,next0=-1
(2)j=1时,next1=0
(3)j>1时,观察下标由0到j-1的字符串,如果前后缀没有相等字符,next值为0;如果前后缀一个字符相等,next值为1;两个字符相等,next值为2;k个字符相等,next值就是k
2.举例
(前缀用下划线表示,后缀用斜体表示)
(1)以"abcdex"为例
|------|----|---|---|---|---|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 |
| T | a | b | c | d | e | x |
| next | -1 | 0 | 0 | 0 | 0 | 0 |
a)默认next0 = -1,next1 = 0
b)j=2时,由下标0到j-1的字符串是"ab",显然"a"与"b"不相等,前后缀没有相等字符,next2=0
c)j=3时,由下标0到j-1的字符串是"abc",前后缀没有相等字符,next3=0
d)此后同理,最终此串的nextj为-100000
(2)以"abcabx"为例
|------|----|---|---|---|---|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 |
| T | a | b | c | a | b | x |
| next | -1 | 0 | 0 | 0 | 1 | 2 |
a)默认next0 = -1,next1 = 0
b)j=2时,由下标0到j-1的字符串是"ab",显然"a"与"b"不相等,前后缀没有相等字符,next2=0
c)j=3时,由下标0到j-1的字符串是"abc",前后缀没有相等字符,next3=0
d)j=4时,由下标0到j-1的字符串是"++a++ bca ",前缀字符"++a++ "与后缀字符"a"相等,next4=1
e)j=5时,由下标0到j-1的字符串是"++ab++ cab ",前缀字符"++ab++ "与后缀字符"ab"相等,next5=2
最终此串的nextj为-100012
(3)以"ababaaaba"为例
|------|----|---|---|---|---|---|---|---|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| T | a | b | a | b | a | a | a | b | a |
| next | -1 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 |
a)默认next0 = -1,next1 = 0
b)j=2时,由下标0到j-1的字符串是"ab",显然"a"与"b"不相等,前后缀没有相等字符,next2=0
c)j=3时,由下标0到j-1的字符串是"++a++ ba ",前缀字符"++a++ "与后缀字符"a"相等,next3=1
d)j=4时,由下标0到j-1的字符串是"++ab++ ab ",前缀字符"++ab++ "与后缀字符"ab"相等,next4=2
e)j=5时,由下标0到j-1的字符串是"++aba++ ba ",前缀字符"++aba++ "与后缀字符"aba"相等,next5=3
f)j=6时,由下标0到j-1的字符串是"++a++ babaa ",前缀字符"++a++ "与后缀字符"a"相等,next6=1
g)j=7时,由下标0到j-1的字符串是"++a++ babaaa ",前缀字符"++a++ "与后缀字符"a"相等,next7=1
h)j=8时,由下标0到j-1的字符串是"++ab++ abaaab ",前缀字符"++ab++ "与后缀字符"ab"相等,next8=2
最终此串的nextj为-100123112
(4)以"aaaaaaaab"为例
|------|----|---|---|---|---|---|---|---|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| T | a | a | a | a | a | a | a | a | b |
| next | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
a)默认next0 = -1,next1 = 0
b)j=2时,由下标0到j-1的字符串是"++aa++ ",前缀字符"++a++ "与后缀字符"a"相等,next2=1
c)j=3时,由下标0到j-1的字符串是"++aa++ a ",前缀字符"++aa++ "与后缀字符"aa"相等,next3=2
d)j=4时,由下标0到j-1的字符串是"++aa++ ++a++a ",前缀字符"++aaa++ "与后缀字符"aaa"相等,next4=3
e)j=5时,由下标0到j-1的字符串是"++aaa++ ++a++a ",前缀字符"++aaaa++ "与后缀字符"aaaa"相等,next5=4
f)j=6时,由下标0到j-1的字符串是"++aaaa++ ++a++a ",前缀字符"++aaaaa++ "与后缀字符"aaaaa"相等,next6=5
g)j=7时,由下标0到j-1的字符串是"++aaaaa++ ++a++a ",前缀字符"++aaaaaa++ "与后缀字符"aaaaaa"相等,next7=6
h)j=8时,由下标0到j-1的字符串是"++aaaaaa++ ++a++a ",前缀字符"++aaaaaaa++ "与后缀字符"aaaaaaa"相等,next8=7
最终此串的nextj为-101234567
(三)C语言实现
cpp
int* Get_Next(const char* SubStr)
{
//1.申请一个next数组,长度和子串长度一致
int* next = (int*)malloc(sizeof(int) * strlen(SubStr));
if (next == NULL) exit(EXIT_FAILURE);
//2.将next数组前两个格子直接赋值-1和0
next[0] = -1;
next[1] = 0;
//3.申请一个变量j指向当前最新的已知其next值的字符(初始化为1号下标)
int j = 1;
//4.再申请一个变量NEX用来保存当前字符失配时,向前可以看到的最长公共前后缀的共有长度(换句话说,就是可以有多少个字符可以不用比较)
int NEX = 0;
//5.进入while循环,循环条件是j合法(j最多指向倒数第二个字符)
while (j + 1 < strlen(SubStr))
{
//6.用当前j指向的字符和其NEX保存的回退位置的字符进行比较
//7.如果相同,则让NEX+1,然后赋值给下一个字符的next数组位置
if (SubStr[j] == SubStr[NEX] || NEX == -1) //9.如果一直比较失败,也就是NEX==-1,则此时将NEX+1变成0赋值给下一个字符的next数组的位置
{
next[++j] = ++NEX;
}
else //8.如果不相同,则让NEX去保存其回退位置字符的回退位置,接着比较,直到比较成功,或者NEX触底(触底就是NEX==-1)
{
NEX = next[NEX];
}
}
//10.当while循环退出,则next数组全部赋值完毕
return next;
}
int KMP_Search(const char* MainStr, const char* SubStr)
{
//1.申请两个指针i和j,分别指向主串和子串的开始位置
int i = 0;
int j = 0;
//2.提供子串对应的next数组
int* next = Get_Next(SubStr);
//3.进入while循环,循环的条件是i和j合法(换句话说就是还没有越界)
while (i < (int)strlen(MainStr) && j < (int)strlen(SubStr))
{
//4.比较i和j指向的字符,如果相同,则让i和j同步向后移动一个位置
if (j == -1 || MainStr[i] == SubStr[j])
{
i++;
j++;
}
//5.如果不相同,则让指向子串的j变量,回退到开始位置(0下标)
else
{
//i永远不回退
j = next[j];//j回退到一个合适的位置
}
}
//6.如果while循环进不去,则说明查找结束了,只需要对j来做判断,就可以知道是否查找成功(j走出自身范围则找到,相反j未走出自身范围则没找到)
if (j < strlen(SubStr))
{
return -1;
}
return i - j;
}
(四)复杂度分析
时间复杂度为O(n+m),其中n为主串长度,m为字串长度。
注意: KMP算法仅当子串与主串之间存在许多"部分匹配"的情况下才体现出它的优势,否则两者差异并不明显。
三、KMP算法改进
(一)改进原理

以主串"aaaabcde",子串"aaaaax"为例,可以发现,我们使用KMP算法时,过程中的②③④⑤步骤,其实是多余的判断。由于子串的第二、三、四、五位置的字符都与首位的"a"相等,那么可以用首位next值去取代它相等字符的next值。
(二)nextval数组值推导
1.推导过程
(1)先算出next数组
(2)j=0时,nextval0=-1
(3)j>0时,若j指向的字符与其next值所对应的回退位置字符相同,则说明next值保存的回退位置无效,直接将其回退位置字符的nextval值誊抄过来(nextvalj = nextvalnext\[j]);若j指向的字符与其next值所对应的回退位置字符不相同,则说明next值保存的回退位置有效,直接将next值誊抄过来(nextvalj=nextj)
2.举例
(1)以"ababaaaba"为例
|---------|----|---|----|---|----|---|---|---|----|
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| T | a | b | a | b | a | a | a | b | a |
| next | -1 | 0 | 0 | 1 | 2 | 3 | 1 | 1 | 2 |
| nextval | -1 | 0 | -1 | 0 | -1 | 3 | 1 | 0 | -1 |
a)默认nextval0 = -1
b)j=1时,j指向的字符是"b",next值是0,0下标的字符是"a",它们不相同,所以nextval1=next1=0
c)j=2时,j指向的字符是"a",next值是0,0下标的字符是"a",它们相同,所以nextval2=nextval0=-1
d)j=3时,j指向的字符是"b",next值是1,1下标的字符是"b",它们相同,所以nextval3=nextval1=0
e)j=4时,j指向的字符是"a",next值是2,2下标的字符是"a",它们相同,所以nextval4=nextval2=-1
f)j=5时,j指向的字符是"a",next值是3,3下标的字符是"b",它们不相同,所以nextval5=next5=3
g)j=6时,j指向的字符是"a",next值是1,1下标的字符是"b",它们不相同,所以nextval6=next6=1
h)j=7时,j指向的字符是"b",next值是1,1下标的字符是"b",它们相同,所以nextval7=nextval1=0
i)j=8时,j指向的字符是"a",next值是2,2下标的字符是"a",它们相同,所以nextval8=nextval2=-1
最终此串的nextj为-1 0 -1 0 -1 3 1 0 -1
(2)以"aaaaaaaab"为例
|---------|----|----|----|----|----|----|----|----|---|
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| T | a | a | a | a | a | a | a | a | b |
| next | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| nextval | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | 7 |
a)默认nextval0 = -1
b)j=1时,j指向的字符是"a",next值是0,0下标的字符是"a",它们相同,所以nextval1=nextval0=-1
c)j=2时,j指向的字符是"a",next值是1,1下标的字符是"a",它们相同,所以nextval2=nextval1=-1
d)j=3时,j指向的字符是"a",next值是2,2下标的字符是"a",它们相同,所以nextval3=nextval2=-1
e)j=4时,j指向的字符是"a",next值是3,3下标的字符是"a",它们相同,所以nextval4=nextval3=-1
f)j=5时,j指向的字符是"a",next值是4,4下标的字符是"a",它们相同,所以nextval5=nextval4=-1
g)j=6时,j指向的字符是"a",next值是5,5下标的字符是"a",它们相同,所以nextval6=nextval5=-1
h)j=7时,j指向的字符是"a",next值是6,6下标的字符是"a",它们相同,所以nextval7=nextval6=-1
i)j=8时,j指向的字符是"b",next值是7,7下标的字符是"a",它们不相同,所以nextval8=next8=7
最终此串的nextj为-1 -1 -1 -1 -1 -1 -1 -1 7
(三)C语言实现
cpp
int* Get_Nextval(const char* SubStr)
{
//1.先完成next数组
int* next = Get_Next(SubStr);
//2.再申请一个nextval数组,并将其第一个格子赋值为-1
int* nextval = (int*)malloc(sizeof(int) * strlen(SubStr));
if (nextval == NULL) exit(EXIT_FAILURE);
nextval[0] = -1;
//3.申请变量j,让其从第二个字符开始向后遍历
for (int j = 1; j < strlen(SubStr); j++)
{
//4.如果j指向的字符和其next数组保存的回退位置字符相同,则说明next数组保存的回退位置无效,直接将其回退位置字符的nextval对应值(最优解)照抄过来
if (SubStr[j] == SubStr[next[j]])
{
nextval[j] = nextval[next[j]];
}
//5.如果j指向的字符和其next数组保存的回退位置字符不相同,则说明next数组保存的回退位置有效,直接照抄过来即可
else
{
nextval[j] = next[j];
}
}
free(next);
return nextval;
}
int KMP_Search2(const char* MainStr, const char* SubStr)
{
//1.申请两个指针i和j,分别指向主串和子串的开始位置
int i = 0;
int j = 0;
//2.提供子串对应的next数组
int* nextval = Get_Nextval(SubStr);
//3.进入while循环,循环的条件是i和j合法(换句话说就是还没有越界)
while (i < (int)strlen(MainStr) && j < (int)strlen(SubStr))
{
//4.比较i和j指向的字符,如果相同,则让i和j同步向后移动一个位置
if (j == -1 || MainStr[i] == SubStr[j])
{
i++;
j++;
}
//5.如果不相同,则让指向子串的j变量,回退到开始位置(0下标)
else
{
//i永远不回退
j = nextval[j];//j回退到一个合适的位置
}
}
//5.如果while循环进不去,则说明查找结束了,只需要对j来做判断,就可以知道是否查找成功(j走出自身范围则找到,相反j未走出自身范围则没找到)
if (j < strlen(SubStr))
{
return -1;
}
return i - j;
}