8.8 & 8.10 | 113-120页:第四章 串next数组的公式推导+KMP算法的优化 |
---|
summary 第三章
在考试中,栈或队列都是作为一个工具来解决其他问题的,我们可以把或队列的声明和操作写得很简单,而不必分函数写出。以顺序栈的操作为例:
- 声明一个栈并初始化
ElemType stack[MaxSize]; int top = -1;
- 元素进栈
stack[top++] = x;
- 元素x出栈
x = stack[top--];
对于链栈,同样只需要定义一个结构体,然后从讲解中摘取必要的语句组合在自己的函数代码中即可。顺序栈考的比较多!!
第四章 串
本章主要掌握字符串模式匹配,重点掌握KMP匹配算法的原理及next数组的推理过程,手工求next数组可以先计算出部分匹配值表然后变形,或根据公式来求。了解nextval数组的求解方法。
4.1串的定义和实现 直接跳过,从4.2串的模式匹配开始
背完这章是不是就不想吃串串了
文字内容
问
- 什么是串的模式匹配?
- 暴力匹配效率低的原因?
- 对暴力匹配的改进是什么算法?从哪些地方开始着手的?
- 什么是前缀、后缀和部分匹配值(PM)?(–>服务于 怎么计算字符串的部分匹配值【应用】—>next数组的求法)
- 部分匹配值的作用?(—>核心理解公式的含义)
- next数组与PM数组的关系?
- next数组中的第一个元素,和最后一位元素是怎么处理的?原因是什么?
- 当next数组中的元素加一后,next[j]的含义是什么?
- 设主串为’s1s2…sn’,模式串为’p1p2…pm’,当主串中第i个字符与模式串中第j个字符失配时,子串应该向右滑动多远,然后与模式串中的哪个字符比较?
- 怎么求next数组?
- KMP算法的局限性和优点?
- 上述的next数组有什么缺陷?怎么优化?
答
- 子串的定位操作通常称为串的模式匹配,它求的是子串(常称模式串)在主串的位置。
- 暴力匹配效率低的主要原因在于它无法利用已匹配的信息来减少无效计算,并且在失配后需要重新从头开始匹配,导致了大量的重复比较和高时间复杂度。
更具体来说,可以分为以下三点:
a. 重复比较:暴力匹配在每次失配后都会将主串的指针右移一位,而模式串从头开始重新匹配,导致大量的重复字符比较,尤其是在主串和模式串中存在重复字符时,重复比较尤为严重。
b. 无法利用已匹配的信息:暴力匹配完全忽略了之前已经匹配成功的部分,无法利用这些信息来优化后续的匹配过程。在失配时,它并没有利用模式串的结构或之前的匹配结果来跳过无效的比较。
c. 最坏情况下时间复杂度高:暴力匹配的最坏情况时间复杂度为O(n * m)(其中n是主串长度,m是模式串长度),在最坏情况下,每个字符都需要多次比较,导致匹配效率低下。 - KMP算法。可以从分析模式串本身的结构着手,若已匹配相等的前缀序列(主串中の)中有某个后缀(这段被匹配相等中的后面几个)正好是模式串的前缀,则可以将模式串向后滑动到与这些相等字符对齐的位置,主串i指针无须回溯,并从该位置开始继续比较。而模式串向后滑动位数的计算仅与模式串本身的结构有关,而与主串无关。
- 前缀指除最后一个字符以外,字符串的所有头部子串;后缀指除第一个字符外,字符串的所有尾部子串;部分匹配值则为字符串的前缀和后缀的最长相等前后缀长度(PM表和主串无关)。
理解移动位数 = 已匹配字符数 - 对应PM值,含义是当前p[j]和s[i]失配,我需要移动子串让它的头部重新和主串的某一位置对齐,而移动位数就是寻找这个位置的关键,在已经匹配的序列中,发现p[0…j-1]是和主串匹配的,其中有k = PM[j-1] == next[j]位都是前后缀相等,也就是子串中的前k位和主串这段已匹配序列的后k位相等,所以只需要向后移动(j-1) - k个位数,就能让子串的前缀和主串子序列的后缀对齐,也就可以开始新的比较。
结合5问进行理解
- 部分匹配值在KMP算法中的作用是优化模式串的移动。它通过分析模式串内部的前后缀关系,在失配时指导模式串的移动,使得匹配过程更加高效。
【优化过程: 先写出PM表;在匹配过程中遇到不匹配情况时,子串中的下标 j j j的移动位数 = 已匹配字符数 - 对应的部分匹配值(j-1的PM值);然后再开始下一趟匹配。】
书上: 某趟发生失配时,若对应的部分匹配值为0,则表示已匹配相等序列中没有相等的前后缀,此时移动的位数最大,直接将子串首字符后移到主串当前位置进行下一趟比较;若已匹配相等序列中存在最大相等前后缀(可理解为首尾重合),则将子串向右滑动到和该相等前后缀对齐,然后从主串当前位置进行下一趟比较。 - 使用PM表时,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来不方便,所以将PM表右移一位,这样哪个元素匹配失败,直接看它自己的next数组值即可,这样也就是next数组。
此时move = (j -1) -next[j]。
此时j应回退到:j = j - move = j - j + 1 + next[j] = next[j] + 1
如果为了计算方便next数组中的数++
此时j = next[j] 表示: j回退到next[j]的位置 及维护我结合6问
- next[0] = -1的原因:当第一个字符失配时,没有任何前缀可以参考,模式串应直接右移一位。设置为-1可以简化处理逻辑,表明无前缀可用,直接移动模式串。
最后一位元素的处理:next数组的最后一个元素按照常规计算,但由于没有后续字符使用这个值,它在匹配过程中不会被用到,所以舍去。尽管如此,这个值还是有意义的,表明最后一个字符之前的匹配情况。 - 当子串的第j个字符与主串发生失配时,跳到子串的next[j]位置重新与主串当前位置进行比较。
当主串中的第i个字符与模式串中的第j个字符失配时,KMP算法使用next数组来决定模式串应该右移多少,以避免重复比较已经匹配过的部分。
结合9问
-
首先明确next[j]的值表示在模式串p[0…j-1]中,最长相同前后缀的长度。这意味着,p[0…next[j]-1]部分内容已经在之前的匹配过程中被确认无误,因此在失配后,不需要再次比较这部分内容。因此,当p[j]与s[i]失配时,模式串应该向右滑动j - next[j]位。这个滑动操作实质上就是跳过已经确认匹配的部分,从模式串的next[j]位置继续与主串的当前字符s[i]比较。
-
当p[j] == p[k]时:next[j] = k + 1。
当p[j] != p[k]时:继续递归查找next[k],直到找到一个使p[j] == p[next[k] + 1]的k,或者k为-1。
如果没有找到这样的k,那么next[j] = 0。 -
KMP算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快得多,其主要优点是主串不回溯。
-
上述的next数组有一个缺陷,即当失配时,如果模式串中存在重复的字符结构,可能会导致模式串多次进行无效的匹配操作。这个缺陷可以通过构建一个优化的next数组(通常称为nextval数组)来解决。在nextval数组中,当失配发生时,如果P[j] == P[next[j]],则直接跳过相同字符,从而减少无效的匹配,进一步提高匹配效率。
挖
- 简单/暴力匹配算法的时间复杂度为【】,其中n和m分别为【】。
- KMP算法的时间复杂度是【】。
空
- O(mn)、主串和模式串的长度
- O(m+n)
选择无
代码内容
简单的模式匹配算法algorithm
使用定长顺序结构,是一种不依赖于其他串操作的暴力匹配算法。
下标从1开始存储主串和模式串。
int Index(SString S , SString T){
int i = 1 , j = 1;
while(i <= S.length && j <= T.length){
if(S.ch[i] == T.ch[j]){
i++;j++;
}else{
i = i-j+2;j=1;//这一步是为了将 i 移动到下一个开始匹配的位置。
}
if(j>T.length) return i-T.length;//说明模式串被遍历了一遍,是子串,开始位置推的时候记得i执行了i++的语句!!!
else return 0;
}
}
如果不记得i更新的公式,自己推,反正i要退回到它本次匹配失败的 开始匹配位置(i-j+1) 的 下一个位置所以是i-j+2。
KMP算法
next数组
void get_next(SString T,int next[]){
int j = 1 , k = 0;
next[1] = 0;
while(j < T.length){
if(k == 0 || T.ch[j] == T.ch[k]){
++j;++k;
next[j] = k;//若pj == pk,则next[k+1] = next[k]+1
}else{
k = next[k];//否则令k = next[k]
}
}
}
- 初始状态(k == 0):
当k == 0时,表示目前还没有匹配到任何前缀(k为0也表示next[k]为0)。
在这种情况下,无论模式串当前位置P[j]是什么字符,模式串都应该从头开始比较。所以我们让k增加1,并更新next[j]。- 字符匹配(T.ch[j] == T.ch[k]):
如果当前字符T.ch[j]和前缀中的字符T.ch[k]匹配成功,那么我们可以扩展匹配长度,即k++,同时将next[j]更新为k的值。
最后一位元素的处理:next数组的最后一个元素按照常规计算,但由于没有后续字符使用这个值,它在匹配过程中不会被用到,所以舍去。因此此时while(j < T.length)而不是<=
nextval数组
void get_nextval(SString T, int nextval[]){
int j = 1 , k = 0 ;
nextval[1] = 0;
while(j < T.length){
if(k == 0 || T.ch[j] == T.ch[k]){
j++;k++;
if(T.ch[j] != T.ch[k]) nextval[j] = k;
else nextval[j] = nextval[k];
}else{
k = nextval[k];
}
}
}