NO.5数据结构串和KMP算法|字符串匹配|主串与模式串|KMP|失配分析|next表
串
字符串匹配与暴力算法
主串与模式串
字符串匹配是计算机科学中最古老、 研究最广泛的问题之一。
一个字符串是一个定义在有限字母表∑上的字符序列。 例如, ATCTAGAGA 是字母表∑ = {A,C,G,T}上的一个字符串。 字符串匹配问题就是在一个大的字符串 S 中搜索某个字符串 P 的所有出现位置。 在这个问题中, S 又被称为主串, P 又被成为模式串。
暴力算法
一种平凡的思路是模式串和主串从主串起始位置开始匹配, 若发生匹配失败, 则模式串向后移动 1 位再进行比较, 直到匹配成功或主串的每个起始位置都已经被尝试过(匹配失败) 。
如上图中, 对于模式 P = aab 和主串 T = acaabc, 将模式 P 沿着 T 从左到右滑动, 逐个比较字符以判断模式 P 在主串 T 中是否存在。
显然, 假设主串的长度为 n, 模式串的长度 m, 那么显然, 这个算法的时间复杂度为O(nm), 当 m 与 n 同阶时, 复杂度可能达到 O(n²)。
Kmp 算法
求解 next 数组的口诀
- 求出模式串的部分匹配值;
- 将部分匹配值右移一位;
- 在首位字符处补-1;
注: 若是选择题, 需要观察选项的 next 数组是从-1 开始的还是 0 开始的, 若是从 0 开始的需要将刚才求出的 next 数组中所有的值+1 即可得到答案;
下面着重讲解部分匹配值的求法;
先给出相关定义:
字符串前缀: 字符串最后一个字符之外的所有头部子串;
字符串后缀: 字符串第一个字符之外的所有尾部子串;
部分匹配值: 字符串的前缀和后缀的最长相等前后缀长度;
比如字符串”aabaa”, ’a’的前后缀都是空, ’aa’的前后缀分别是’a’和’a’, 那么此处的部分匹配值就是 1, …, 以此类推, 最后整个字符串的前缀有{a,aa,aab,aaba}后缀有{abaa,baa,aa,a},最长的相等前后缀长度就是 2
然后将该部分匹配值表的内容统一右移一位(舍去最右边那一位):
在首位补-1, 即可得到 next 数组:
最终就求得了字符串 aabaa 的 next 表-1,0,1,0,1, 若需要从 0 开始就整体+1, 即 0,1,2,1,2
练习题
主串是 ababbababa,模式串‘ababa’ 的 next 数组是? 在使用 kmp 算法匹配时, 一共发生了几次失配?
失配时模式串指针回退到P[next[j]]
,P[next[4]] = P[2]
P[next[2]] = P[0]
P[next[0]] = P[-1]
主串和模式串指针都右移一位
kmp算法
失配分析
匹配成功必要条件: T.suffix == T.pre
若T.suffix == T.pre : 直接从S[j]
和P[j]
处开始比较即可
j’的含义:
当模式串P在P[j]
处与S[i]
发生失配时,将P[j]
移动到与s[j]
对齐的位置继续匹配,P右移j-j'
长度
在kmp算法中,用next[j]
来替换这里的j’
next[j]的含义
显然:
在多个可选的T.pre == T.suffix中,只有选择较长的pre和suffix,才能确保滑动距离最短,满足所有情况。但是,pre和suffix不能取T,否则不能移动
构建next表
next[0]=-1
:
当P[0]
处失配时,需要将P移过当前位置,等效为将P[-1]
与S[j]
对齐。
next[2]=1
:
当P[2]
处失配时,匹配成功部分最长,可重叠前后缀长度为1。
next[3]=0
:
匹配成功部分最长,当P[3]
处失配时,可重叠前后缀长度为0。
next[4]=1
:
当P[4]
处失配时,匹配成功部分最长,可重叠前后缀长度为1。
next[5]=2
:
当P[5]
处失配时,匹配成功部分最长,可重叠前后缀长度为2。
next[6]=2
:
当P[6]
处失配时,匹配成功部分最长,可重叠前后缀长度为2。
next[7]=3
:
匹配成功部分最长,当匹配成功时,可重叠前后缀长度为3。
next[] = [-1,0,1,0,1,2,2,3]
注意:上面的next表数组是从0开始计数的,当数组从1开始计数,需要整体加1:
next[] = [0,1,2,1,2,3,3,4]
考试时根据选项next表第一个元素是-1还是0判断
主算法
失配时:
j = 5
, next[5] = 2
,因此j = next[5] = 2
失配时:
j = 5
, next[5] = 2
, 因此j = next[5] = 2
继续依次比较,可以匹配成功。
int KMP(char * S,char * T){int next[10];Next(T,next);//根据模式串T,初始化next数组 int i = 0; //i表示主串指针int j = 0; //j表示模式串指针while(i < strlen(S) && j < strlen(T)){if(j == -1 || s[i] == T[j]){i++; j++;}else{j = next[j];//如果测试的两个字符不相等,i不动,j变为当前模式串的next值}}if(j >= strlen(T)){//如果条件为真,说明匹配成功return i -(int)strlen(T);} return -1;
}
改进版kmp算法
j=6处发生失配,原因:
P[j] = ‘a’ ≠ S[i]
按朴素kmp算法: j = next[j] = 3
此时发现,P[j]
依然等于’a’
。 此次匹配注定也是失败的。
因此,若P[next[j]] == P[j]
,这种替换也是无效的,所以在前面构建next[]
表的基础上,还必须加入限制条件:
P[next[j]] ≠ P[j]
nextval[0] = -1
:这一点和构建next表方式是一样的
nextval[1] = -1
:next[0] = 0
,但是由于P[0] == P[1]
,所以不能选用0。
nextval[2] = 1
:匹配成功部分为”aa”,因此最长可匹配的前后缀为”a”和”a”,长度为1,且P[1]!=P[2]
.
nextval[3] = -1
:匹配成功部分为”aab”,因此最长可匹配的前后缀为长度为0,且P[0] == P[3]
,所以不能选用该方案,退而选择nextval[3] = -1
。
nextval[4] = -1
:匹配成功部分为”aaba”,因此最长可匹配的前后缀为“a”和”a”,长度为1,且P[1] == P[4]
,所以不能选用该方案,又因为P[0] == P[4]
,所以nextval[4]!=0
。
nextval[5] = 1
:匹配成功部分为”aabaa”,因此最长可匹配的前后缀为“aa”和”aa”,长度为2,又因为P[2] == P[5]
,所以不能选用该方案,继续寻找备选,次长可匹配前后缀为’a’和’a’,长度为1,而且P[1]!=P[5]
,所以nextval[5] = 1
nextval[6] = -1
:匹配成功部分为”aabaab”,因此最长可匹配的前后缀为“aab”和”aab”,长度为3,又因为P[3] == P[6]
, 所以不能选用该方案,继续寻找备选,次长可匹配前后长度为0,而且P[0] == P[6]
,所以nextval[5]!=0
,因此nextval[6] = -1
。
显然,nextval[7] = next[7] = 1
快速构建next表
本质就是找到一个合适的移动距离,使得P的前缀和P的后缀可以匹配
若P[j-1] == P[next[j-1]]
则next[j] = next[j-1] + 1
若P[j-1] != P[next[j-1]]
则继续比较P[j-1]
与P[next[next[j-1]]
void Next(int* next,char P[])
{int i = 0, j = -1; next[0] = -1;while(i < strlen(P)){if(j == -1 || P[i] == P[j]){next[i + 1] = j+1;i++; j++;}else{j = next[j];}}
}