模式匹配算法——KMP算法
如图,L为字符串,L1为已经匹配了的字符子串,红色部分表示L1的下一位出现不匹配。假如在已经匹配了的子串中从某个位置开始,出现了新的匹配,即这里的L2。那么下一次对字符串的匹配只需要从L2开始进行匹配就可以了(如果子串中出现新的匹配,它必然在L2中,L2代表这最大的新匹配),而不需要从L1首位置的下一位开始。
由观察可以发现,L2为L1的后缀,而L2又是新的匹配,说明它必然和L1的前一部分相同,即它也是L1的前缀。因此要定位下次重新匹配的位置,我们只需从找到L1前缀和后缀相同最长的部分的长度。
这里我们引进一个数组去记录字符子串的每一位作为终点时,前面的子串最长公共前后缀长度,即部分匹配值(Partial Match,PM)(如这里L1的最后一个元素,它的最长公共前后缀长度为L2的长度)。当在某个位置出现不匹配时,查找它前一位的PM值,通过将子串向后移动若干位,即可找到新的匹配起始位置。移动公式为:
即
如图中的例子,将子串向后移动(L1 - L2)位,即可与L2重叠。
对于子串‘abcac’,它的PM值如下表:
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | 0 | 0 | 0 | 1 | 0 |
KPM算法的改进
因为每次不匹配时都要去找前一个元素的PM值,很不方便,因此把每一个PM值向后移动一位。
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | d | c |
next | -1 | 0 | 0 | 0 | 1 |
当第一个元素不匹配时,只需向后移动一位,因此不需要计算子串移动的位数。
KPM算法再改进
在每次匹配时都要去计算移动的位数,而实际上在每一个位置需要移动的位数是已知的,我们可以提前算好。已知 j 是最后匹配的字符,它是子串的比较指针。每次不匹配时,只需要将 j 回退若干位,在该位置重新进行匹配。这里新的比较指针的位置为:
为了公式更简洁,将next整体+1。
如下表:
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | d | c |
next | 0 | 1 | 1 | 1 | 2 |
KMP算法的进一步优化
模式‘aaaab’在和主串‘aaabaaaab’进行匹配时(这里的next [ j ] 指的是PM值)
主串 | a | a | a | b | a | a | a | a | b |
---|---|---|---|---|---|---|---|---|---|
模式 | a | a | a | a | b | ||||
j | 1 | 2 | 3 | 4 | 5 | ||||
next [ j ] | 0 | 1 | 2 | 3 | 4 | ||||
nextval [ j ] | 0 | 0 | 0 | 0 | 4 |
在j=4时出现不匹配,子串将向后移动 j - 1 - next [ j - 1 ] 位,即1位,此时第 j - 1位到达了 j 的位置与 b 进行匹配,但因为子串在该位的值与之前的相同,都是a,因此必然导致失配。
聪明的我已经看出了问题所在,也就是当子串后移 j - 1 - next [ j - 1 ] 位时,原先的第next [ j - 1 ] 位到达 j 位,如果原先的第next [ j - 1 ] 位的值与失配时的值一样,必然导致重复失配。重复失配后,我们又要将向后移动 j - 1 - next [ j - 1 ] 位,第二次的第next [ j - 1 ] 位到达 j 位。如此反复重复,直到最终next [ j - 1 ]位的值不等于 j 位的主串的值。
因此这里我们引入了一个新的数组nextval,提前记录重复匹配的终点,即最终的next。
按这个例子,在 j = 4 时发生失配,需要移动的个数不再是1,而是 j - 1 -nextval [ j - 1],即3位。