KMP
俗称看毛片算法:
kmp算法是一种字符串匹配算法,用于模式串和主串之间的关系
暴力算法(幼稚匹配算法
-
每次匹配模式串的第0到第i个字符串,一直到匹配成功或者出现了不一样的字符
-
不一样字符串的时候,窗口向前移动一位,然后右指针回溯到初始位置重新匹配
KMP算法
暴力解每次的右指针回溯,让我们中间匹配的过程全 部 木 大,难道我们一直以来的努力都全部白费吗?于是便有了kmp算法
我们可以想想,当右指针出现不匹配的时候,窗口内的值是不是已经全部匹配了,那这部分的值真的毫无作用吗,如果能提前判断好模式串的结构,能不能不回溯右指针,而是反过来移动窗口。于是便有了kmp(虽然并不是这样出来的)
核心:kmp算法省去了右指针的回溯过程(所以时间复杂度是o(m+n)),通过提前对模式串的结构进行分析,在匹配的时候直接移动窗口而不是移动右指针。
理论基础:前缀,后缀,以及前后缀匹配值。
-
前缀:从第0个字符到第i个字符的字符子串 (不包含最后一个字符)
-
后缀:从第i个字符到第n个字符的字符字串 (n为字符长度)(不包含第一个字符)
-
前后缀匹配值:即前缀和后缀的相同部分
我们提前对模式串的每个位置的前后缀结构进行分析。
比如字符串:"abcaba"
-
对于位置1(从1开始,0用于放其他东西),只有'a',所以其前后缀均为空集
-
对与位置2,”ab“,很容易看出其前后缀匹配值也是空。
-
对于位置3:”abc“,同理
-
位置4,”abca“,其中前缀”a“,和后缀”a“出现重合,那么位置四对应前后缀匹配值就是”a“(TIPS:我们通常仅仅记录前后缀匹配的最长长度,因为我们移动的时候只要知道距离就行)
-
位置5:”abcab“,容易看出前缀“ab”,后缀“ab”相同,那么最长长度就是2
-
位置6:最后一位,我们不做记录(原因之后会有)
好了,你现在已经掌握了必要理论(大概
那么这个理论又有什么用呢
图来自上面的视频
对于上图这样的情况,我们已经提前知道了指针处左侧的字符串的结构,他的后缀和前缀有两位匹配,那么我们直接将窗口前缀的“AB”移动到指针左边的那个“AB”处,然后继续移动指针。
这样就不会出现右指针的回溯了(跳过的元素中并不会出现漏掉的答案,因为如果出现漏掉的答案那么其会有和模式串相同的结构,这部分的信息已经被我们提前用前后缀匹配过了,并不会出现这样的情况,这里有点难懂,想想应该能明白,没图画给你了)
那么对于模式串的结构,我们要怎么求出来呢。(我们用next[]存储每个对应位置的最长前后缀匹配长度)
如果你根据上面所说的方法求解,你会发现最后又变成了暴力求解。
因为对于每一个位置,求解前后缀匹配长度,都需要O(n)的时间,那么n个位置,就是O(n2)那么这个算法就失去了原本的意义(即使是这样,也比暴力快)
对于next[]数组
以下图片也来自于此视频
//将对于s的next数组生成放到next里面 int GetNext(string& s, int next[], int size) { next[1] = 0; int i = 1, j = 0; while(i < length){ if(j == 0 || ch[i] == ch[j]) { next[++i] = ++j; //i++; j++; next[i] = j }else{ j = next[i]; } } }
假设我们要求解,next[17],那么我们一定已经知道了next[16],那么由next[16] = 8,我们可以知道所框部分一定是相同的。那么此时只要看看ch[8]和ch[16]是否相等即可。
如果相等就是最好的情况,直接让next[17] = next[16] + 1即可
那么如果ch[16] != ch[8]呢,我们可以再分,把1-7分成两部分
这里我们再分,就有1-3起码是一样的所以只要判断ch[4] ?= ch[16]
这里数字取的都比较巧妙,建议自己用其他数字再演算一边加深印象
我们可以仔细思考下kmp算法的极端情况,如果next数组一个前后匹配的都没有呢,那么就是每次窗口移动一格,此时我们也能发现暴力其实也是一次移动一格(因为最开始就出现了不匹配),这个时候速度相当,(暴力还快些,因为不需要提前处理)
所以其实如果模式串结构不明显,或者主串中很少出现部分匹配的话,暴力算法和kmp算法的在时间上相差并没有那么多。