KMP算法是字符串算法中经典的算法之一,本文仅作记录理解之用,其中有参考《严书》和他人博客,建议先看《严书》。
1. KMP算法的由来
KMP算法是解决字符串匹配问题。
问题:字符串匹配,一个主串,一个模式串,模式串是否在主串中,如果在,求出第一个字符的下标。
正常解法: 一个字符一个字符比
1 | //s是主串,p是模式串,返回模式串在主串中的起始下标,如果不存在,返回-1 |
严书的代码:
1 | //返回模式串P在主串S中第pos个字符之后的位置,若不存在,返回-1 |
这种方法太低效,时间复杂度接近O(n * m),所以,是否可以利用已经部分匹配的信息,减少回溯的次数,让模式串尽可能快速移动到合适的位置。
那么,问题来了, 当主串的指针i和模式串的指针j不匹配时,模式串的指针j应该移动到哪个位置?
于是,KMP算法就有了。
2. KMP算法
先介绍Next数组的含义。
2.1 next数组的含义
next[j] = k,当主串的指针i和模式串的指针j不匹配时,模式串的指针j应该移动到指针k,主串指针i不需要回溯。
先举个例子,明白next[j] = k等式的含义。
next[j] = k,k的含义指模式串的前k个字符和其最后的k个字符相同(下标从0开始)。
2.2 理解next数组的正确
主串用T[i]表示,模式串用P[j]表示,下标从0开始。
从严谨角度来看,请参考《严书》的数学归纳法证明。
本文以理解为主。
1 | 当 T[i] != P[j]时, |
2.3 如何求出next数组
求出过程,是数学归纳法,也是递推过程。
当j=0时,即模式串中第一个不匹配时,有next[0] = 0; 我们先不管初始条件,否则容易乱。
假设当next[j] = k,next数组含义知有,P[0 ~ k-1] == P[j-k ~ j-1]。
那么next[j+1]是?:sweat_smile:
分为两种情况:
- 当p[k] == p[j]时,有P[0 ~ k-1~k] == P[j-k ~ j-1~j],则有next[j+1] = k+1 = next[j] + 1;
- 当p[k] != p[j]时,这又是一个字符串匹配问题,整个模式串既是是主串又是模式串,应当将模式串中P[j]和P[next[k]]比较,由已知得,有next[k] = m,当p[j] == p[m],则有P[j-m+1 ~ j] = p[0 ~ m],即next[j+1] = m + 1 = next[k] + 1;当p[j] != p[m]时,依次类推,最后next[j+1] = 0(j >= 0);
最后,我们来看初始条件。有next[1] = 0,显然当模式串中第一个字符和主串的字符不匹配时,是应该从下标0开始比较。在求解next数组过程中,有公式next[j+1] = next[j] + 1,所以,next[0] = -1,也就是说,初始条件是,j = 0,k = -1。
根据求next数组的递推关系,求next数组的递归代码如下:
1 | //递归代码, arr是next数组 |
求next数组的非递归代码如下:
1 | //非递归代码 |
2.4 改进的next数组
2.3中求next数组存在另外的问题,比如主串s ‘aaabaaaab’和模式串p ‘aaaab’,next数组值如下,
j | 0 1 2 3 4 |
---|---|
模式 | a a a a b |
next[j] | -1 0 1 2 3 |
nextval[j] | 0 0 0 0 4 |
当i = 3,j = 3时,主串和模式串不匹配,根据求出的next数组,仍然会依次比较i = 3,j = 0、1、2,实际上因为p[0] == p[1] == p[2],是不需要再进行比较的,所以改进的next数组nextval。
求nextval数组的代码:
1 | void nextval(int arr[], int len, char *p) |
【未完待续】
3. 参考
[1] http://www.cnblogs.com/tangzhengyue/p/4315393.html
[2] 严书《数据结构》