1.KMP算法在模式串和主串匹配过程中,利用了每次匹配中已经匹配的字符数和模式串本身的特点。模式串本身的特点最为关键的是模式串字符的部分匹配值,后面的next数组的求法其根基在这里。next[j]数组反应的是,当模式串第j个字符发生失配时,下一次继续匹配的字符从模式串哪个字符开始——next[j]。
2.next[j]的求法成为KMP算法的关键。next[j]的求法与模式串字符的部分匹配值密切相关,最终求next[j]的过程又被转化成求k的过程( ′ p 1 p 2 . . . p k − 1 ′ = ′ p j − ( k − 1 ) p j − ( k − 2 ) p j − 1 p j − 1 ′ 'p_1p_2...p_{k-1}'='p_{j-(k-1)}p_{j-(k-2)}p_{j-1}p_{j-1}' ′p1p2...pk−1′=′pj−(k−1)pj−(k−2)pj−1pj−1′)。
3.从模式串的第二个字符开始计算k(第一个不用计算),然后第三个第四个,依次递增即对应于数学归纳法由next[j]到next[j+1]的扩展,到next[j+1]有两种情况:pk=pj;pk ≠ \neq = pj(下面有详细的讨论)。总之就是扩展到next[j+1],如果新增的字符和第k个字符相等,那么从最大公共前后缀的角度,肯定是加1的;如果不相等,那么就要缩小k的值,让第k个字符再与第j个字符比较如果相等那么就可以确定新的最大公共前后缀,而这个过程又是个字符串匹配问题,第k个字符及之前的所有字符next数组都已经建立好了,所以匹配又是高效的并且跳转到next数组指定的位置接着比较也能保证前面的都是已经匹配成功的。直到建立所有字符的next数组。
KMP算法原理
KMP算法是一种字符串匹配算法,它是对朴素模式算法(暴力匹配)的改进。 我们先看朴素模式下是怎么进行字符串匹配的。假设我们有两个字符串,被匹配的大串称为 主串,要匹配的小串称为模式串。
模式串要和主串匹配,我们很容易想到一个双重循环下的暴力匹配方法。先从主串的第一个字符开始匹配,依次遍历模式串的字符,如果不匹配就从主串的第二个字符匹配。如果主串的长度为n,模式串的长度为m,显然这个算法的最坏时间复杂度为 O ( n m ) O(nm) O(nm)。
这个方法一个最大的缺点是没能从每次失败匹配中得到教训。 如果模式串中的前两个元素匹配成功,即前两个元素与主串中对应的元素相等,而这个两个元素又不相等,在上图中显示就是模式串中的a没必要再与主串的b进行比较了,显然不相等。
我们再来看另一种情况,如果在匹配成功的模式串中存在相等的字符又是什么情况呢?在下图中模式串中第二个a已经匹配了,但是第一个a也必然能够和主串匹配,我们不能像上面所说跳过,而是移动对齐然后开始用模式串中相等字符后面的元素与主串匹配失败的元素开始比较。
这体现了匹配过程中两个主要的特点:①存在匹配成功的模式串子串;②匹配成功的子串中存在相等的字符。这也是KMP算法抓住的字符匹配过程的特点。
如果进一步观察会发现匹配成功的子串中存在相等的字符 其实是匹配成功的子串中相等前后缀。为什么是前后呢,因为我们想尽可能移动得远点啊!(哈哈)如果相等的字符不是对应的前缀和后缀,即使已匹配模式串中存在相等的字符,也是不需要移动对齐相等字符再与主串比较的,可以自己举几个例子看下。 (注意:我们这个前后缀有个前提,就是在匹配成功的子串中。) 在匹配成功的子串中,我们关注的是模式串中有哪些字符是相等的,因为只有这种情况,我们不能肯定不需要比较了。如果不存在匹配成功的子串,自然不用前后缀的概念,模式串往下移一位接着比较就好了。所以判断模式串中是否存在相等的前后缀,将会是解决问题的关键之一。所以有必要引出字符串的前缀、后缀和部分匹配值的概念。前缀是指除最后一个字符以外的,字符串的所有头部子串(这里有一个细节,结合刚才的匹配过程我们发现,我们在发现不匹配的字符时关注的是不匹配字符之前的串)这个细节和前缀为什么这么取关联不大;后缀是指除第一个字符以外,字符串的所有尾部子串; 部分匹配值则为字符串的前缀和后缀的最长相等的前后缀长度。
通过字符串’abcac’进行说明:
- 'a’的前缀和后缀是空,最长相等的前后缀长度为0;
- 'ab’的前缀为{a},后缀为{b},最长相等的前后缀长度为0;
- 'abc’的前缀为{a,ab},后缀为{c,bc},最长相等的前后缀长度为0;
- 'abca’的前缀为{a,ab,abc},后缀为{a,ca,bca},最长相等的前后缀长度为1;
- 'abcac’的前缀为{a,ab,abc,abca},后缀{c,ac,cac,bcac},最长相等的前后缀长度为0;
注意:前缀取的时候是从前往后的顺序,后缀虽然从最后一个字符开始取,但也是从前往后的顺序。
将部分匹配值写成数组的形式,得到部分匹配值(partial Match, PM)的表。
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
s | a | b | c | a | c |
PM | 0 | 0 | 0 | 1 | 0 |
这里先给出结论, 我们上面所讨论移动的位数就是已匹配的字符数减去对应的部分匹配值。
移 动 位 数 = 已 匹 配 字 符 数 − 对 应 的 部 分 匹 配 值 移动位数=已匹配字符数-对应的部分匹配值 移动位数=已匹配字符数−对应的部分匹配值
以上图字符串"abcac"匹配为例,已匹配的字符数是4个,不匹配位置前面的a对应的部分匹配值为1,所以移动位数为4-1=3,而我们在下一轮比较前确实把模式串向右移动了三位。那么其中的原理是什么?如果没有这个部分匹配值,我们移动的是一个已匹配的长度,这种情况只存在于模式串没有重复出现字符的情况。所以部分匹配值就是要解决模式串中出现重复字符的情况,而且要知道这个重复字符出现在哪里。 部分匹配值就是要解决这个问题。
下面我们对这个过程进行算法描述:我们用一个变量j记录当前匹配到模式串的第几位,所以每次进行匹配时当j会暂停在匹配不相等即失配位置,而已匹配的字符数即为j-1,而我们要找的也是匹配不相等字符的前一位字符的部分匹配值。所以下一轮比较前移动的位数就是:
n u m S t e p = ( j − 1 ) − P M [ j − 1 ] numStep=(j-1)-PM[j-1] numStep=(j−1)−PM[j−1]
要找匹配不相等字符的前一位字符的部分匹配值怎么感觉也不方便,所以我们把PM表整体向右移动一位,这样就得到了一个新的数组,称之为next数组。
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
s | a | b | c | a | c |
next | -1 | 0 | 0 | 0 | 1 |
为什么第一位字符 n e x t [ 1 ] next[1] next[1]值空缺值用-1填充呢?因为如果模式串第一个字符匹配失败,则需要将子串向右移动一位(已匹配字符数为零,0-(-1)=1)。最后一位字符的 P M [ 5 ] PM[5] PM[5]值溢出又怎么办呢?这个不必担心因为移动之前如果最后一位字符不匹配,我们看的也是它前面字符的部分匹配值。所以有:
n u m S t e p = ( j − 1 ) − n e x t [ j ] numStep=(j-1)-next[j] numStep=(j−1)−next[j]
我们一直都在描述模式串移动的位数,但我们知道在内存中模式串是不会移动的,那么模式串向后移动,其实就意味着我们的j向前移动即回退,所以得到下一轮比较前j的值就是
j = j − n u m S t e p = j − ( ( j − 1 ) − n e x t [ j ] ) = n e x t [ j ] + 1 j=j-numStep=j-((j-1)-next[j])=next[j]+1 j=j−numStep=j−(