懵B的KMP DFA算法
这是迄今为止,我个人认为,除了红黑树外,最为难以理解的算法,基于DFA的KMP算法。
我已经与此算法纠缠多日,翻遍无数博客,包括不限于CSDN,StackOverflow,知乎等等,依然一团雾水,本着书读百遍其意自现的精神,又将算法4相关部分读了十几遍,逐渐有了一些理解,于是赶紧记下来,省的忘了,各位啃算法的同学,也可以参考。
首先,可以粗浅的认为,此算法的核心是模式识别。
而模式识别的的核心,是将子字符串 pat 变为状态机二维数组 DFA[ txtchar ][ patPtr ],
将被匹配的字符串 txt 的字符 txtchar 顺序作为状态机数组二维下标,
再通过结合状态指针 patPtr 作为一维下标,找到下一个状态指针 `patPtr,
同时定位 txtchar 的 txtPtr 前进一位,与刚得到的 `patPtr 指向的 patchar 比较,一直循环,直至完全匹配。
看完上面一段话,如果以前没接触过这种算法的同学,一定是懵逼的。
举例说明
OK,我们拿个例子说明:
从“ A A A A A A B A B A B A C " 中寻找 " A B A C "
第一步,用“ A B A C "构建DFA模式识别数组 DFA[ Char ][ Ptr ]:
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 3 | 1 |
DFA[B][Ptr] | 0 | 2 | 0 | 2 |
DFA[C][Ptr] | 0 | 0 | 0 | 4 |
先暂时不要管这个数组是怎么来的,先看看它怎么用。
首先要匹配的字符串 txt,子字符串是 pat,要找到 pat 在 txt 中的位置。
规则,每次比较txt的指针都向后移动一位,从0,1,2,3,4,5 到 12.
根据 patPtr 及 DFA 数组中来自用于比较的 txt 中的字母来确定移动后的 txtPtr 指向 txt 字母所比较的 pat 字母。
如下表:
txtPtr = 0 指向 txt 字母 A, 状态指针 patPtr = 0 指向 pat 字母 A,下一个状态指针 `patPtr = DFA[A][0] = 1,数组的 A 是 txt 中 txtPtr 对应的字母
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
意味着 txtPtr = 1 时,txt 中 txtPtr 对应的字母 A ,比较的是 pat 中 patPtr = 1 时的字母 B,
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
DFA[A][1] 对应的数字是 1 , 那么 txtPtr 为 2 时,txt 中的字母 A ,比较的仍然是 pat 中 patPtr 为 1 时的字母 B
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
当DFA[ txt.at ( txtPtr ) ] [ ] 连续匹配,patPtr 就会连续向后移动,每次一位
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
继续匹配成功,下一次匹配为 patPtr = 3 指向 pat 的 C 与 txtPtr = 8 指向txt的 B
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
没有匹配成功,patPtr 指针退回到 2,txtPtr 继续前进一位
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
匹配成功,txtPtr 继续前进一位,patPtr 也前进一位。
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
未能匹配,需要 patPtr 退到 2,txtPtr 继续前进一位
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
匹配成功, patPtr 前进到 3,txtPtr 前进到 12
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
所有匹配成功,此时 patPtr = 4 也就是 pat 的长度。
txt | A | A | A | A | A | A | B | A | B | A | B | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
txtPtr | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
pat | A | B | A | C | |||||||||
patPtr | 0 | 1 | 2 | 3 | |||||||||
DFA[A][patPtr] | 1 | 1 | 3 | 1 | |||||||||
DFA[B][patPtr] | 0 | 2 | 0 | 2 | |||||||||
DFA[C][patPtr] | 0 | 0 | 0 | 4 |
OK,到这里已经比较清楚运作方式了,我们不比较字符,而是把字符当作数组的下标,不停的查找下一个状态,直到满足完全匹配,或 txt 结束。
DFA 到底怎么来的
接下来,我们开始啃硬骨头,DFA 状态机二维数组究竟是怎么来的。
还是用“ A B A C "举例,构建DFA模式识别数组 DFA[ Char ][ Ptr ]:
我们先初始化一个所有元素完全为 0 状态的二维数组 DFA[ Char ][ Ptr ]:
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 0 | 0 | 0 | 0 |
DFA[B][Ptr] | 0 | 0 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
第一步,是设置状态指针 Ptr =0 指向的首字符 A 在状态 Ptr = 0 时的迁移状态 `Ptr = 1
意味着如果 A 匹配成功 Ptr 后移一位。
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 0 | 0 | 0 |
DFA[B][Ptr] | 0 | 0 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
这里的意思是,如果比较的字符和首字母 A 相同,则状态指针 Ptr 后移一位,`Ptr = 1,用以比较 `Ptr 对应的下一个字符 B。
如果字符不一样,则仍然要从首字母 A 开始比较,下一个状态指针的指向不变 `Ptr = 0。
注意,下面的每一个字务必弄懂,否则不能理解 DFA 的构造。
我们要用刚刚构建的DFA数组,在PtrX = 0 的状态下,去匹配自己的 Ptr = 1 状态,
并把DFAX[ charX ][ 0 ]状态赋值给 DFA[ char ][ 1 ]。
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 0 | 0 | 0 |
DFA[B][Ptr] | 0 | 0 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
PtrX | 0 | |||
CharX | A | |||
DFA[A][Ptr] | 1 | |||
DFA[B][Ptr] | 0 | |||
DFA[C][Ptr] | 0 |
赋值
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 0 | 0 |
DFA[B][Ptr] | 0 | 0 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
PtrX | 0 | |||
CharX | A | |||
DFA[A][PtrX] | 1 | |||
DFA[B][PtrX] | 0 | |||
DFA[C][PtrX] | 0 |
赋值完成后分两步,
第一步,设置 DFA[ B ][ 1 ] = 2,因为如果比对字母为 B 就应该将状态指针 Ptr 后移 1 位。
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 0 | 0 |
DFA[B][Ptr] | 0 | 2 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
PtrX | 0 | |||
CharX | A | |||
DFA[A][PtrX] | 1 | |||
DFA[B][PtrX] | 0 | |||
DFA[C][PtrX] | 0 |
第二步,如果将 pat: “A B A C” 作为被匹配的字符串,
那么模式字符串 patX 的 PtrX 状态指针的下一个状态 `PtrX = DFAX[ B ][ 0 ] = 0,
移动 Ptr 指针从 1 到 `Ptr = 2
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 0 | 0 |
DFA[B][Ptr] | 0 | 2 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
PtrX | 0 | 1 | ||
CharX | A | B | ||
DFA[A][PtrX] | 1 | 1 | ||
DFA[B][PtrX] | 0 | 2 | ||
DFA[C][PtrX] | 0 | 0 |
将 DFAX[ charX ][ 0 ] 的状态赋值给 DFA[ char ][ 2 ]
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 1 | 0 |
DFA[B][Ptr] | 0 | 2 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
PtrX | 0 | 1 | ||
CharX | A | B | ||
DFA[A][PtrX] | 1 | 1 | ||
DFA[B][PtrX] | 0 | 2 | ||
DFA[C][PtrX] | 0 | 0 |
如果 A 匹配,应迁移至 Ptr =3 的位置,所以赋值 DFA[ A ][ 2 ] = 3
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 3 | 0 |
DFA[B][Ptr] | 0 | 2 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
PtrX | 0 | 1 | ||
CharX | A | B | ||
DFA[A][PtrX] | 1 | 1 | ||
DFA[B][PtrX] | 0 | 2 | ||
DFA[C][PtrX] | 0 | 0 |
Ptr 后移一位 `Ptr = 3, `PtrX = DFAX[ Char ][ PtrX ] = DFAX[ A ][ 0 ] = 1
注意这里的意思,因为 A 匹配成功,所以 `Ptr 指向的字符 C 比对的,将是 DFAX[ CharX ][ 1 ]
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 3 | 0 |
DFA[B][Ptr] | 0 | 2 | 0 | 0 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
PtrX | 0 | 1 | ||
CharX | A | B | ||
DFA[A][PtrX] | 1 | 1 | ||
DFA[B][PtrX] | 0 | 2 | ||
DFA[C][PtrX] | 0 | 0 |
将 DFAX[ ][ 1 ] 的状态赋值给 DFAX[ ][ 3 ]
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 3 | 1 |
DFA[B][Ptr] | 0 | 2 | 0 | 2 |
DFA[C][Ptr] | 0 | 0 | 0 | 0 |
PtrX | 0 | 1 | ||
CharX | A | B | ||
DFA[A][PtrX] | 1 | 1 | ||
DFA[B][PtrX] | 0 | 2 | ||
DFA[C][PtrX] | 0 | 0 |
同时,如果 C 匹配成功 Ptr 应后移一位 即 `Ptr = 4
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 3 | 1 |
DFA[B][Ptr] | 0 | 2 | 0 | 2 |
DFA[C][Ptr] | 0 | 0 | 0 | 4 |
PtrX | 0 | 1 | ||
CharX | A | B | ||
DFA[A][PtrX] | 1 | 1 | ||
DFA[B][PtrX] | 0 | 2 | ||
DFA[C][PtrX] | 0 | 0 |
构造完成:
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
Char | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 3 | 1 |
DFA[B][Ptr] | 0 | 2 | 0 | 2 |
DFA[C][Ptr] | 0 | 0 | 0 | 4 |
是不是非常精美,用未完成的状态机数组 DFAX[ ][ ] 迭代匹配 pat 的字符串,将值赋给 DFA[ ][ ] 自己,查找部分重复的部分,将未匹配的状态赋值,同时将匹配字母在此 Ptr 状态对应的值设置为 Ptr + 1,这样如果匹配,则后移一位。
先写到这里,脑子有点转不动了。
回来继续,现在想一想,我们上面到底做了什么。
我们凭空的建立一个二维数组 DFA[3][4],
用 DFA[Char][0] 代表处于 0 位的字母 A ,
用 DFA[Char][1] 代表处于 1 位的字母 B ,
用 DFA[Char][2] 代表处于 2 位的字母 A ,
用 DFA[Char][3] 代表处于 3 位的字母 C ,
我们发现,每个字母都是有状态的,表面上就是字符在字符串中的位置,和字符本身,
更深的则是碰到任意字母,下一步转向 DFA 数组的哪一位。
我们赋值 DFA[A][0] = 1,数组其他值为0。意味着如果有字符串匹配了 0 位 A,状态指针会升位为 1。
用代表 A 的 DFA[Char][0] 去赋值第 1 位的 B,并更新 DFA[B][1] 为 2,因为如果 B 被匹配,状态指针要升位为 2。
更新状态指针,用代表 0 位 A 的 DFA[Char][0] 去套 B,也就是 Ptr = DFA[B][0],意味着匹配不成功,下一个字符,也就是第 2 位的 A 会匹配退回到第 0 位的 A。
然后将 0 位 A 的状态赋给 2 位 A ,同时 A = A 匹配,所以 2 位 A 的状态 DFA[A][2] = 3 进行升位。
更新状态指针,用代表 0 位 A 的 DFA[Char][0] 去套 A,也就是 Ptr = DFA[A][0],意味着匹配成功,下一个字符,也就是第 3 位的 C 会匹配前进到第 1 位的 B。
将 1 位 B 的状态赋值给 3 位 C,将 3 位 C 的 状态 DFA[C][3] 设为 4。
递推的逻辑是用 A 匹配 A B 的 B,用 A B 匹配 A B A 的 B A, 用 A B A 匹配 A B A C 的 B A C,而具体的匹配状态,由前一个匹配结果确定。
这样的结果是找到被匹配的字符串中,所有后面子字符串中,包含自 0 位开始的子字符串部分,都被设为前面子字符串对应的相应状态。
多琢磨琢磨,再看看下面这个状态数组,推演一遍。
Ptr | 0 | 1 | 2 | 3 |
---|---|---|---|---|
pat.at(Ptr) | A | B | A | C |
DFA[A][Ptr] | 1 | 1 | 3 | 1 |
DFA[B][Ptr] | 0 | 2 | 0 | 2 |
DFA[C][Ptr] | 0 | 0 | 0 | 4 |
代码非常简单,主要是不好理解。
#include <iostream>
#include <string>
#include <utility>
#include <vector>
struct KMP
{
explicit KMP(std::string strpat) : pat(std::move(strpat))
{
int patSize = static_cast<int>(pat.size());
const int alphSize = 256;
dfa =
std::vector<std::vector<int>>(alphSize, std::vector<int>(patSize));
dfa[pat.at(0)][0] = 1;
if (patSize < alphSize)
{
for (int X = 0, patPtr = 1; patPtr != patSize; ++patPtr)
{
for (int i = 0; i != patSize; ++i)
{
dfa[pat.at(i)][patPtr] = dfa[pat.at(i)][X];
}
dfa[pat.at(patPtr)][patPtr] = patPtr + 1;
X = dfa[pat.at(patPtr)][X];
}
}
else
{
for (int X = 0, patPtr = 1; patPtr != patSize; ++patPtr)
{
for (int ch = 0; ch != alphSize; ++ch)
{
dfa[ch][patPtr] = dfa[ch][X];
}
dfa[pat.at(patPtr)][patPtr] = patPtr + 1;
X = dfa[pat.at(patPtr)][X];
}
}
}
auto search(const std::string &txt) const -> int
{
int txtPtr = 0;
int patPtr = 0;
int txtSize = static_cast<int>(txt.size());
int patSize = static_cast<int>(pat.size());
for (txtPtr = 0, patPtr = 0; txtPtr != txtSize && patPtr != patSize;
++txtPtr)
{
patPtr = dfa[txt.at(txtPtr)][patPtr];
}
if (patPtr == patSize)
{
return txtPtr - patSize;
}
return txtSize;
}
private:
std::string pat;
std::vector<std::vector<int>> dfa;
};