一、问题的提出
假设有一个文本串S、和一个模式串P、若想要查找P在S中第一次出现的位置,有什么方法?
二、暴力匹配算法
暴力匹配算法可能是解决此类问题最直观的思路,容易想到,但是其效率不高,时间复杂度为O(m*n),因此会想去寻找更优化的方法。
思路
如果当前字符匹配成功,那么指向S的下标i和指向P的下标j都进行加一操作,匹配下一个字符
如果当前字符匹配不成功,那么指向S的下标i回溯(i = i - ( j - 1 ))指向P的下标j被置为0
三、KMP算法
1.暴力匹配算法不好的地方在于,假如匹配到模式串P的某个字符发现不匹配了,按逻辑P[0]要和本来P[1]所匹配的文本串的那个字符相匹配,然而,由于我们能很清楚知道P[0]与P[1]是否相等,所以如果不相等的话,回溯过去必然导致下一步失配。为此,我们可以跳过这些一定会失配的字符,利用之前已经部分匹配这个有效信息,保持i 不回溯,通过修改j 的位置,让模式串尽量地移动到有效的位置。这就是KMP算法的大致思路。
2.寻找最长公共元素长度
-
对于P = p0 p1 …pj-1 pj,寻找模式串P中长度最大且相等的前缀和后缀。如果存在p0 p1 …pk-1 pk = pj- k pj-k+1…pj-1 pj,那么在包含pj的模式串中有最大长度为k+1的相同前缀后缀。
-
举个栗子,排除最左边的元素从左到右看,与排除最右边的元素从右向左看,找出最长相同元素的字符数量
-
为什么要找出这个值呢?因为前面说到如果失配要跳过模式串P中的一些字符以提高效率,假如匹配到某个字符失配了,那么模式串往右移动的数量就是根据这个最长公共元素长度表来的,它表明了当前失配字符的前一个字符在模式串P前面部分存在某个字符以前都是相等的,(我自己理解为对称性),因此可以直接跳转。具体网上大神表述为:失配时,模式模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
3.求next数组
- 为什么有了最长公共元素长度表还要有个next数组?当匹配到一个字符失配时,其实没必要考虑当前失配的字符,我们每次失配时都是看的失配字符的上一位字符对应的最大长度值。如此便引出了next 数组。最大长度表向右移动一位并在next数组下标为0的位置置为-1便可得到完整的next数组(未优化前)
- 有了next数组后,失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next值
- next数组的求解
l 如果对于值k,已有p0 p1, …, pk-1 = pj-k pj-k+1, …, pj-1,相当于next[j] = k。代表p[j] 之前的模式串子串中,有长度为k 的相同前缀和后缀。在KMP匹配中,当模式串中j 处的字符失配时,下一步用next[j]处的字符继续跟文本串匹配,相当于模式串向右移动j - next[j] 位。
l 已知next [0, …, j],求出next [j + 1],若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;
l 到这一步之前都比较好理解,但是当p[k] != p[j]的时候,就需要寻找长度更短的相同前缀后缀。这时候可以递归去找(或者说根据对称性来寻找),引用下面的图和文字:
Pj和Pk不匹配,那么往前找更小长度的相同相同前缀后缀,由于Pk之前的蓝色块和P0所在的蓝色块是匹配的,所以可以直接递归至Pnext[k]处之前找就可以
4.根据next数组进行匹配
如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。
四、感悟
- 利用一些杂碎的时间去看KMP算法,比如上班坐公车的时候,但是需要理解的东西感觉最好还是得有一段完整的时间专注的领悟反而更快一些;
- 平时要多做总结,多复盘。之前在学校的时候学过KMP算法,蜻蜓点水看过几遍,结果还是似懂非懂。
参考:https://blog.csdn.net/qq_34374664/article/details/52735320
- END -
下方扫码关注 高性能服务架构,一起学习成长、共同进步,做一个码场最贵Coder!