字符串匹配算法(母串和子串)
如以下两个字符串
str1
a | a | b | a | b | b | a |
str2
a | b | a | b |
在字符串str1中查找str2的字符串,并返回首次查找到的下标
在还没学kmp算法之前,我一直使用的是朴素的模式匹配法 |
void fun(char str1[],char str2[])
{
int i = 0, j = 0;
while (i < strlen(str1) && j < strlen(str2))
{
// 若相等,都前进一步
if (str1[i] == str2[j])
{
i++;
j++;
}
else
{
i = i - j + 1;
j = 0;
}
}
// 匹配成功返回下标位置
if (j == strlen(str2))
{
printf("%d\n",i - j);
}
}
使用暴力算法固然简单易懂,但是时间复杂度不容易满足大多数的字符串匹配问题。
对于KMP算法:
KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。
首先,我们来了解什么是最长公共前后缀:
前缀和后缀集合中相同部分,同时取最长的那个。
例如: a b a b 前缀是 [ a , ab , aba ] 后缀是 [ b , ab , bab ] 所以最长公共前后缀就是 ab |
我们来讨论一下next ()是怎么来的:
如
j | i | |||
a | b | a | b | c |
j 指向模式字符串的前缀,i 指向模式字符串的后缀
str2子串
i(下标从1开始)
a | b | a | b | c |
j
a | b | a | b | c |
(1)当i=1时,next[1]=0;
(2)当i=2时,i由1到i-1就只有字符“a”,next[2]=1;
(3)当i=3时,此时由 1 到 i-1 的串为“aba”,前缀“a”与后缀"a"相等,因此可以推算出next[i]的值为1;
(4)当i=4时,此时由1到i-1的串为“abab”,前缀“ab”和后缀“ab”相等,所以next[4]=2;
(5)当i=5时,此时由1到i-1的串为“ababc”,显然后缀c没有与之配对的前缀,所以next[5]=1;
即
next [ j ]=
0 j==1 nax(k|1<k<j且p1...p(k-1)=p(j-k-1)...p(j-1)} 前缀等于后缀(最长公共前后缀) 1 其他情况
求KMP算法中的 next [ ] 值:
void fun(char s[],int next[]) //s为子串,next为回溯数组
{
int i=1,j=0;
next[1]=0;
while(i<strlen(s))
{
if(j==0||s[i]==s[j])
{
i++;
j++;
next[i]=j; //下标为i的字符前的字符串最长相等的前后缀长度为j
}
else j=next[j-1];//表示该处字符不匹配时应该回溯到的字符下标
}
}
next [ ]数组的数值只与子串本身有关
next [ j ]:其值等于 第 j 位字符前面的 j - 1位字符组成的子串的前后缀重合字符数 + 1
KMP模式匹配算法的改进
面对s=aaaab这样的子串时,可以省略许多判断,由于s串的2,3,4位置的字符都和首位‘a’相等,那么可以用首位next[1]的值去取代与它相等的字符后续next[j]的值。
void fun(char s[],int next[]) //s为子串,next为回溯数组
{
int i=1,j=0;
next[1]=0;
while(i<strlen(s))
{
if(j==0||s[i]==s[j])
{
i++;
j++;
if(s[i]!=s[j]) //若当前字符与前缀字符不同,则当前j为next在i位置上的值
next[i]=j;
else
next[i]=next[j]; //若与前缀字符相同,则将前缀字符的next值赋值给next在i位置的值
}
else j=next[j-1];//表示该处字符不匹配时应该回溯到的字符下标
}
}
例
例题:
【模板】KMP字符串匹配
题目描述
给出两个字符串 s_1 和 s_2,若 s_1的区间 [l, r] 子串与 s_2 完全相同,则称 s_2 在 s_1 中出现了,其出现位置为 l 。
现在请你求出 s_2 在 s_1 中所有出现的位置。
定义一个字符串 s 的 border 为 s 的一个非 s 本身的子串 t,满足 t 既是 s 的前缀,又是 s 的后缀。
对于 s_2,你还需要求出对于其每个前缀 s' 的最长 border t' 的长度。
输入格式
第一行为一个字符串,即为 s_1。
第二行为一个字符串,即为 s_2。
输出格式
首先输出若干行,每行一个整数,按从小到大的顺序输出 s_2 在 s_1 中出现的位置。
最后一行输出 |s_2| 个整数,第 i 个整数表示 s_2 的长度为 i 的前缀的最长 border 长度。
样例 #1
样例输入 #1
ABABABC ABA
样例输出 #1
1 3 0 0 1
提示
样例 1 解释
。
对于 s_2 长度为 3 的前缀 ABA,字符串 A 既是其后缀也是其前缀,且是最长的,因此最长 border 长度为 1。
数据规模与约定
本题采用多测试点捆绑测试,共有 3 个子任务。
- Subtask 1(30 points):|s_1|<=15,|s_2|<=5
- Subtask 2(40 points):|s_1|<=10^4,|s_2|<=10^2
- Subtask 3(30 points):无特殊约定。
对于全部的测试点,保证 1<=|s_1|,|s_2|<=10^6,s_1, s_2 中均只含大写英文字母
#include<stdio.h> #include<string.h> char s1[10000000],s2[10000000]; int next[10000000]={0}; void fun() { int i=1,j=0; int m=strlen(s2); next[0]=0; while(i<m) { while(j>0&&s2[j]!=s2[i]) j=next[j-1]; if(s2[j]==s2[i]) j++; next[i]=j; i++; } } int main() { int i=0,j=0; scanf("%s",s1); scanf("%s",s2); int n=strlen(s1); int m=strlen(s2); fun(); while(i<n) { while(j>0&&s2[j]!=s1[i]) //两个字母不相等,则指针后退,重新匹配,j退回合适的位置 j=next[j-1]; if(s2[j]==s1[i]) j++; if(j==m) printf("%d\n",i+1-m+1); i++; } for(int i=0;i<m;i++) printf("%d ",next[i]); return 0; }