KMP算法
引子
假设我们解决这样一个问题:
有字符串S与模式串P,想知道模式串P在字符串S的什么位置,应如何解决?
解决子串匹配问题
字符串S:“abcabcabd”
模式串P:“abcabd”
采用暴力(Brute Force)算法,即朴素算法的做法:
依次比较每个字符,直到串的每个字符都一一吻合。
算法执行流程:
-
每个字符比较后,直到发现c与d不符合,将模式串向后移动。
-
发现b与a不符合,模式串再向后移动。
-
发现c与a不符合,模式串再向后移动。
-
每个字符比较,发现一一吻合。
朴素算法中,一旦匹配不成功,要到目标串下一位,再和模式串从头匹配,流程相对繁琐。很明显,例子中的2
与3
步骤是可以省略的,关键在于,能否保存两个串中,相同部分的字符,每次移动模式串时,直接忽略其中字符不相同的部分,来优化整个匹配流程。这就是KMP算法的核心。
什么是KMP算法
KMP算法,即Knuth-Morris-Pratt 字符串查找算法。KMP算法由Knuth、Morris和Pratt提出,是字符串匹配算法的一种改进算法。
KMP算法通常需要一个部分匹配表(Partial Match Table)。用next数组保存部分匹配值,也就是前 i 个字符所组成的子串的真前缀与真后缀相同的最大长度。当发生不匹配时,直接从next数组保存的索引开始匹配,忽略不必要的匹配操作。
需要知道的知识
真前缀
真前缀,指字符串首部开始的除了最后一个字符的所有子串。
例如"abcde"的真前缀有[a,ab,abc,abcd]
真后缀
真后缀,指字符串尾部开始的除了第一个字符的所有子串。
例如"abcde"的真后缀有[bcde,cde,de,e]
真前缀与真后缀相同的最大长度
例如"ababa"的真前缀为[a,ab,aba,abab],真后缀为[baba,aba,ba,a],其中相同的最长子串为"aba" 即最大长度为3。
next数组
根据模式串中每个字符的,前 i 个字符所组成的子串的真前缀与真后缀相等的最大长度,可求的一个next数组。
以"abcabd"为例:
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
模式串 | a | b | c | a | b | d |
next数组 | 0 | 0 | 0 | 1 | 2 | 0 |
当目标串与模式串不匹配时,模式串向右移动位数 = 已经匹配的字符数 - 前一位已匹配字符的最大长度值。
实际操作中,会用指针的移动来代替模式串的移动,也就是将比较指针,移动到前一位已匹配的next数组值的索引。
字符串S:“abcabcabd”
模式串P:“abcabd”
例子中,在模式串索引为5的字符d出现问题,会将比较指针移动到next数组索引为4的值,也就是从模式串中索引为2的字符开始下轮比较。
注意:不同的写法会有不同的next数组,尽管写法不同,但整体的算法思想是相同的,区别只在于如何利用next数组。
例如同样的模式串的不同next数组:
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
模式串 | a | b | c | a | b | d |
next数组 | -1 | 0 | 0 | 0 | 1 | 2 |
其中-1通常会在算法中作为边界值使用。
当目标串与模式串不匹配时,模式串向右移动位数 = 不匹配的字符索引 - 不匹配字符的所对应的next数组值。
两种方式中,“已经匹配的字符数” 在 “不匹配的字符索引” 的前一位。“前一位已匹配字符的最大长度值” 同样在 “不匹配字符的所对应的next数组值” 的前一位。也就是说两个不同next数组在比较中,使得模式串移动的位数是一样的,所达到的实际效果也是一样的。
KMP算法为什么是正确的
KMP算法是如何正确的
前面已经做了很多铺垫。当发生字符不匹配时,实际上是将模式串中已经匹配部分的后缀与模式串的前缀进行比较,获取两者相同的部分,进而忽略不匹配的部分。由于发生部分不匹配时,在不匹配之前的字符一定是匹配的,所以这些步骤在模式串本身就可以实现。得到next数组的过程,实际上就是模式串自己匹配自己的过程,也就是获取模式串本身的真前缀和真后缀相同的最大长度,将长度保存下来,即得到要获取的next数组。匹配的时候,模式串再根据next数组的值进行移动。
next数组的实现
next数组的实现通常分为:
初始化,前后缀不相同时操作,前后缀相同时操作,next数组赋值。
- 初始化包括指针 i 和指针 j 的值,还有next数组的索引为 0 的值。
- i 为标准指针, j 为比较指针。在以 i 为标准在for循环中,遍历每个字符,在内嵌的while循环中比较字符是否相同,如果不相同便将已经匹配的字符数传给比较指针 j 。
- 在之前以 i 为标准的for循环中,如果比较字符相同,使比较指针 j 自增, j 不仅代表比较指针,同时也记录已匹配的字符数。
- 最后将已