KMP(快速子串查找)
问题:在字符串s中查找字符串p首次出现的位置。
正常情况下对s和p进行匹配的最坏时间复杂度是O(len(s)*len(p)),我们用i,j分别从s,p的头部进行匹配,每次匹配失败我们回退j到0,i+=1,进行下一轮匹配。
1 | int find(const string& s, const string& p) { |
KMP的思想就是预处理p得到next数组,保证i不回退,next就是预先算出i不回退的情况下j应该回退到哪,这样算法复杂度就降到了O(len(s)+len(p)) 也就是 O(len(s))。
有时候模式串是固定的,需要重复在不同的串中查找模式串,所以next数组也可以预先算好一直复用。
1 | // 计算next数组,即j不匹配时的回退位置 |
有了next数组后的匹配就想前面说的,只要根据next进行回退就可以了,没有过多技巧。
那么主要讲一下next数组的生成思路,根据next的定义,其实next[i]表示的是p[i]前面最长能有多少字符和p的开头匹配
例如:我们生成 "aabaaab"的next数组,考察next[4]和next[5],p[4]的前面最长有“a”和p的开头匹配,所以next[4]=1,
p[5]的前面最长有“aa”和p的开头匹配,所以next[5]=2。
1 | 0123456 |
总有next[0] = next[1] = 0,我们只要从下标2开始计算next。
对于next[i],我们可以采用数学归纳法的思维,我们找到i-1回退的位置,取j = next[i-1],如果p[i-1]==p[j],那么显然next[i] = next[i-1] + 1,如果p[i-1]!=p[j]呢,next[i]=0吗?并不是
我们还是以 aabaaab 为例,考察next[6],首先我们算出了next[0…5]=[0, 0, 1, 0, 1, 2],而p[5] !=p[2] (‘a’ != ‘b’)
1 | 0123456 |
这里就有个技巧了,对于p[i-1]和p[j]不匹配时,我们想知道让j回退多少,我们可以利用next数组的含义,尝试让j回退到next[j],再看看p[i-1]和p[j]是否相等,我们在生成next的时候就用到了规模更小的next,还是数学归纳法的思维,j=next[5]=2, 因为p[5]!=p[2] ,令j=next[j]=next[2]=1,而p[5]==p[1],所以next[6] = next[1] + 1 = 2,大致的理解思路就是这样,严格的证明见:前缀函数与 KMP 算法 - OI Wiki (oi-wiki.org)