2022-05-04 KMP算法,基于DFA的子字符串查找

懵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 ]:

Ptr0123
CharABAC
DFA[A][Ptr]1131
DFA[B][Ptr]0202
DFA[C][Ptr]0004

先暂时不要管这个数组是怎么来的,先看看它怎么用。

首先要匹配的字符串 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 对应的字母

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr 0123
DFA[A][patPtr] 1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

意味着 txtPtr = 1 时,txt 中 txtPtr 对应的字母 A ,比较的是 pat 中 patPtr = 1 时的字母 B,

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr 0 123
DFA[A][patPtr] 1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

DFA[A][1] 对应的数字是 1 , 那么 txtPtr 为 2 时,txt 中的字母 A ,比较的仍然是 pat 中 patPtr 为 1 时的字母 B

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr0123
DFA[A][patPtr]1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

当DFA[ txt.at ( txtPtr ) ] [ ] 连续匹配,patPtr 就会连续向后移动,每次一位

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr0123
DFA[A][patPtr]1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

继续匹配成功,下一次匹配为 patPtr = 3 指向 pat 的 C 与 txtPtr = 8 指向txt的 B

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr0123
DFA[A][patPtr]1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

没有匹配成功,patPtr 指针退回到 2,txtPtr 继续前进一位

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr0123
DFA[A][patPtr]1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

匹配成功,txtPtr 继续前进一位,patPtr 也前进一位。

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr0123
DFA[A][patPtr]1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

未能匹配,需要 patPtr 退到 2,txtPtr 继续前进一位

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr0123
DFA[A][patPtr]1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

匹配成功, patPtr 前进到 3,txtPtr 前进到 12

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr0123
DFA[A][patPtr]1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

所有匹配成功,此时 patPtr = 4 也就是 pat 的长度。

txtAAAAAABABABAC
txtPtr0123456789101112
patABAC
patPtr0123
DFA[A][patPtr]1131
DFA[B][patPtr]0202
DFA[C][patPtr]0004

OK,到这里已经比较清楚运作方式了,我们不比较字符,而是把字符当作数组的下标,不停的查找下一个状态,直到满足完全匹配,或 txt 结束。

DFA 到底怎么来的

接下来,我们开始啃硬骨头,DFA 状态机二维数组究竟是怎么来的。

还是用“ A B A C "举例,构建DFA模式识别数组 DFA[ Char ][ Ptr ]:

我们先初始化一个所有元素完全为 0 状态的二维数组 DFA[ Char ][ Ptr ]:

Ptr0123
CharABAC
DFA[A][Ptr]0000
DFA[B][Ptr]0000
DFA[C][Ptr]0000

第一步,是设置状态指针 Ptr =0 指向的首字符 A 在状态 Ptr = 0 时的迁移状态 `Ptr = 1

意味着如果 A 匹配成功 Ptr 后移一位。

Ptr0123
CharABAC
DFA[A][Ptr]1000
DFA[B][Ptr]0000
DFA[C][Ptr]0000

这里的意思是,如果比较的字符和首字母 A 相同,则状态指针 Ptr 后移一位,`Ptr = 1,用以比较 `Ptr 对应的下一个字符 B。

如果字符不一样,则仍然要从首字母 A 开始比较,下一个状态指针的指向不变 `Ptr = 0。

注意,下面的每一个字务必弄懂,否则不能理解 DFA 的构造。

我们要用刚刚构建的DFA数组,在PtrX = 0 的状态下,去匹配自己的 Ptr = 1 状态,

并把DFAX[ charX ][ 0 ]状态赋值给 DFA[ char ][ 1 ]。

Ptr0123
CharABAC
DFA[A][Ptr]1000
DFA[B][Ptr]0000
DFA[C][Ptr]0000
PtrX0
CharXA
DFA[A][Ptr]1
DFA[B][Ptr]0
DFA[C][Ptr]0

赋值

Ptr0123
CharABAC
DFA[A][Ptr]1100
DFA[B][Ptr]0000
DFA[C][Ptr]0000
PtrX0
CharXA
DFA[A][PtrX]1
DFA[B][PtrX]0
DFA[C][PtrX]0

赋值完成后分两步,

第一步,设置 DFA[ B ][ 1 ] = 2,因为如果比对字母为 B 就应该将状态指针 Ptr 后移 1 位。

Ptr0123
CharABAC
DFA[A][Ptr]1100
DFA[B][Ptr]0200
DFA[C][Ptr]0000
PtrX0
CharXA
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

Ptr0123
CharABAC
DFA[A][Ptr]1100
DFA[B][Ptr]0200
DFA[C][Ptr]0000
PtrX01
CharXAB
DFA[A][PtrX]11
DFA[B][PtrX]02
DFA[C][PtrX]00

将 DFAX[ charX ][ 0 ] 的状态赋值给 DFA[ char ][ 2 ]

Ptr0123
CharABAC
DFA[A][Ptr]1110
DFA[B][Ptr]0200
DFA[C][Ptr]0000
PtrX01
CharXAB
DFA[A][PtrX]11
DFA[B][PtrX]02
DFA[C][PtrX]00

如果 A 匹配,应迁移至 Ptr =3 的位置,所以赋值 DFA[ A ][ 2 ] = 3

Ptr0123
CharABAC
DFA[A][Ptr]1130
DFA[B][Ptr]0200
DFA[C][Ptr]0000
PtrX01
CharXAB
DFA[A][PtrX]11
DFA[B][PtrX]02
DFA[C][PtrX]00

Ptr 后移一位 `Ptr = 3, `PtrX = DFAX[ Char ][ PtrX ] = DFAX[ A ][ 0 ] = 1

注意这里的意思,因为 A 匹配成功,所以 `Ptr 指向的字符 C 比对的,将是 DFAX[ CharX ][ 1 ]

Ptr0123
CharABAC
DFA[A][Ptr]1130
DFA[B][Ptr]0200
DFA[C][Ptr]0000
PtrX01
CharXAB
DFA[A][PtrX]11
DFA[B][PtrX]02
DFA[C][PtrX]00

将 DFAX[ ][ 1 ] 的状态赋值给 DFAX[ ][ 3 ]

Ptr0123
CharABAC
DFA[A][Ptr]1131
DFA[B][Ptr]0202
DFA[C][Ptr]0000
PtrX01
CharXAB
DFA[A][PtrX]11
DFA[B][PtrX]02
DFA[C][PtrX]00

同时,如果 C 匹配成功 Ptr 应后移一位 即 `Ptr = 4

Ptr0123
CharABAC
DFA[A][Ptr]1131
DFA[B][Ptr]0202
DFA[C][Ptr]0004
PtrX01
CharXAB
DFA[A][PtrX]11
DFA[B][PtrX]02
DFA[C][PtrX]00

构造完成:

Ptr0123
CharABAC
DFA[A][Ptr]1131
DFA[B][Ptr]0202
DFA[C][Ptr]0004

是不是非常精美,用未完成的状态机数组 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 位开始的子字符串部分,都被设为前面子字符串对应的相应状态。

多琢磨琢磨,再看看下面这个状态数组,推演一遍。

Ptr0123
pat.at(Ptr)ABAC
DFA[A][Ptr]1131
DFA[B][Ptr]0202
DFA[C][Ptr]0004

代码非常简单,主要是不好理解。

#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;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
BF算法KMP算法都是串的模式匹配算法,但是它们的时间复杂度不同。BF算法的时间复杂度为O(m*n),其中m和n分别为主串和模式串的长度。而KMP算法的时间复杂度为O(m+n)。因此,当模式串较长时,KMP算法的效率更高。 下面是BF算法KMP算法的介绍和演示: 1. BF算法(暴力匹配算法) BF算法是一种朴素的模式匹配算法,它的思想是从主串的第一个字符开始,依次和模式串的每个字符进行比较,如果匹配成功,则继续比较下一个字符,否则从主串的下一个字符开始重新匹配。BF算法的时间复杂度为O(m*n)。 下面是BF算法的Python代码演示: ```python def BF(main_str, pattern_str): m = len(main_str) n = len(pattern_str) for i in range(m-n+1): j = 0 while j < n and main_str[i+j] == pattern_str[j]: j += 1 if j == n: return i return -1 # 测试 main_str = 'ababcabcacbab' pattern_str = 'abcac' print(BF(main_str, pattern_str)) # 输出:6 ``` 2. KMP算法(Knuth-Morris-Pratt算法KMP算法是一种改进的模式匹配算法,它的核心思想是利用已经匹配过的信息,尽量减少模式串与主串的匹配次数。具体来说,KMP算法通过预处理模式串,得到一个next数组,用于指导匹配过程中的跳转。KMP算法的时间复杂度为O(m+n)。 下面是KMP算法的Python代码演示: ```python def KMP(main_str, pattern_str): m = len(main_str) n = len(pattern_str) next = getNext(pattern_str) i = 0 j = 0 while i < m and j < n: if j == -1 or main_str[i] == pattern_str[j]: i += 1 j += 1 else: j = next[j] if j == n: return i - j else: return -1 def getNext(pattern_str): n = len(pattern_str) next = [-1] * n i = 0 j = -1 while i < n-1: if j == -1 or pattern_str[i] == pattern_str[j]: i += 1 j += 1 next[i] = j else: j = next[j] return next # 测试 main_str = 'ababcabcacbab' pattern_str = 'abcac' print(KMP(main_str, pattern_str)) # 输出:6 ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不停感叹的老林_<C 语言编程核心突破>

不打赏的人, 看完也学不会.

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值