KMP算法是用来匹配字符串的,比如在字符串** haystack:ABCACEABCABDA
中查询是否存在子串 needleABCAB
,这种问题可以暴力破解,即:将 haystack中所有长度与needle相等的字串与needle**比较。
KMP与暴力破解的区别在于:
- 暴力破解是** haystack的子串
ABCAC
(ABCACEABCABDA)与needle**ABCAB
比较后,由于不匹配,接下来会用BCACE
(ABCACEABCABDA)与ABCAB
比较。 - KMP相对“智能”一些,若
ABCAC
(ABCACEABCABDA)与ABCAB
比较不匹配后,会选ACEAB
(ABCACEABCABDA)与needle比较,至少,他们有相同的开头。那么,如何才能正确选择到相同开头
的字串是KMP的关键。
在KMP中通过部分匹配表
来实现相对智能选择合适的开头。部分匹配表
是对字符串needle的一种“描述”,取字符串“前缀”和后缀的最大共有长度,其规则如下:
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCA"的前缀为[A, AB, ABC],后缀为[BCA, CA, A],共有元素的长度为,共有元素为“A”,长度为1;
- "ABCAB"的前缀为[A, AB, ABC, ABCA],后缀为[BCAB, CAB, AB, A],共有元素为"AB",长度为2;
故needle的部分匹配表
为[0,0,0,1,2]
。ABCAC
(ABCACEABCABDA)与needleABCAB
比较后,下一次比较开始的位置为本次匹配开始的位置再向右偏移x
位,其中x
的值是ABCAC
与ABCAB
的最大匹配长度4(ABCA
)减去最后一个匹配的字母A
在部分匹配表中`所对应的值。即:
移动位数 = 已匹配的字符数 - 已匹配的最后一个字符对应匹配值
其中的原理大概描述是:
ABCAC
与ABCAB
比较,其中匹配的部分是ABCA
。由于最终还是不匹配,出于效率考虑,下次比较开始的位置需要尽可能靠后,那不如向右数4
位吧(ABCA
的长度),但这个万一ABCA
中某部分也可以作为下次比较的开头呢?通过“观察”发现ABCA
末尾的只有A
可以作为下一次的开始(如果是ABAB
,那么就是末尾的AB
可以作为下次比较的开始),那就向右数4-1
位吧。
上述过程的写成伪代码:
// 部分匹配表
int pi[needle长度]
// pi的计算
...
// 遍历
int i = 0, j = 0;
while (i < n) {
if (haystack[i] == needle[j]) {
j++;
i++;
} else {
if (j > 0) {
i = i + j - pi[j-1];
}
j = 0;
}
if (j == m) {
return i - j;
}
}
然而这并不是最好的方案,当haystack[i] != needle[j]
的时候,改变i
和j
不如单独改变j
,即ABCAC
(ABCACEABCABDA)与ABCAB
比较不匹配后,会选ACEAB
(ABCACEABCABDA)与needle比较,,也可以理解为haystack[4] != needle[4]
时,i==4
不变,j=1
,这样i==3
就不用再比较一次了,而匹配表中的数据恰好可以直接给赋值给j
。如下图:
伪代码:
// 部分匹配表
int pi[needle长度]
// pi的计算
...
// 遍历
int i = 0, j = 0;
while (i < n) {
if (haystack[i] == needle[j]) {
j++;
i++;
} else {
if (j == 0) {
i++;
} else {
j = pi[j-1];
}
}
if (j == m) {
return i - j;
}
}
好了,目前为止,至少KMP的原理是知道了,下面看看KMP的标准代码
int strStr(char* haystack, char* needle) {
int n = strlen(haystack), m = strlen(needle);
if (m == 0) {
return 0;
}
// 部分匹配表 pi
int pi[m];
pi[0] = 0;
// pi的计算
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && needle[i] != needle[j]) {
j = pi[j - 1];
}
if (needle[i] == needle[j]) {
j++;
}
pi[i] = j;
}
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && haystack[i] != needle[j]) {
j = pi[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == m) {
return i - m + 1;
}
}
return -1;
}
???!!!
什么鬼?pi
到底是怎么计算出来的?
有以下三个思考点。
- 计算过程可以看成
needle
与needle’
(needle == needle‘
)字母依次比较比较,只不过最开始是needle[i]与needle’[j], i == 1, j==0
且i > j
永远成立; - 动态规划:
pi[0]==0
且pi[i+1] <= pi[i]+1
。 - 有限状态自动机:
pi
中所保存的,是对应状态遇到不符合期望的条件将要切换到的状态。状态j
,0 <= j <= needle的长度
,当j==needle的长度
,检索成功。
但暂时本人对这些依然没有特别清晰的感受,并以此清晰描述其中的细节,待续。
参考资料:
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
网友评论