字符串匹配自动机的算法原理

来源:互联网 发布:java变量的命名规则 编辑:程序博客网 时间:2024/05/23 14:16

上一节,我们知道,如何构造一个有限状态机,用于字符串匹配,我们只给出了怎么做,这一节,我们详细说明一下,为什么要这么做,我们要从数学上验证上一节我们给出的算法逻辑是经得起考验的。

这里写图片描述

如上图所示,有限状态自动机有以下几个特点:
1. 它由一系列的状态节点组成,我们用Q来表示这些节点的集合
2. 状态机一开始就会处于初始状态,我们用q0来表示
3. 所以状态中,必有一个状态A Q 叫接收状态,例如上图的节点1.
4. 组成字符串的字符集, 例如上图中,字符集只包含a,b两个字符。
5. 当状态机处于某个状态,接收到一个输入字符时,会跳转到另一个状态,这种跳转我们用一个函数δ 来表示,例如,根据上图,当状态机处于状态0,输入是字符a时,状态转移到状态1,于是就有(0, a) = 1

我们再引入一个函数ϕ, 叫最终状态函数,它接收一个字符串,然后给出状态机读入该字符串后,最终会处于哪个状态,例如给定字符串”abba”,上面的状态机接收后,最终会处于状态1,于是就有 ϕ(“abba”) = 1, 如果给定的字符串是”aabb”, 状态机接收该字符串后,最终处于的状态是0,所以 ϕ(“aabb”) = 0

状态机一开始时,处于初始状态,也就是状态机什么都不接收时或接收空字符串时就处于初始状态,于是我们有:
ϕ(ϵ) = q0
假设w 是一个字符串,那么有:
ϕ(wa) = δ(ϕ(w), a)
上面这个公式需要好好解释一下,假定w=”aabb”, wa = “aabb” + ‘a’ = “aabba”.

当状态机接收字符串”aabb”后,处于状态0,于是就有 ϕ(“aabb”) = 0.状态机接收字符串wa后所达到的最终状态,相当于先接收字符串w,达到状态0后,再接收最后的字符a,使得状态机从状态0,再进行一次跳转,根据上图,状态机处于状态0,输入字符为a时,跳转到状态1,于是有δ(0, a) = 1, 又由于状态0是状态机接收字符串”aabb”后的最终状态,所以0 = ϕ(“aabb”), 代入上一个式子有δ( ϕ(“aabb”), a) = 1, 先接收字符串”aabb”,到达一个状态,然后再接收字符a到达另一个状态,这不就相当于字符串”aabba” = “aabb” + ‘a’, 所抵达的最终状态吗,所以就有:
ϕ(“aabba”) = δ(ϕ(“aabb”), a).

数学推理是一个比较烧脑的过程,想必上面的解释会让不少同学抓耳挠腮一阵子。

假设要查找的字符串,我们用P来表示,对于给定一个文本T, 如果P 的前k个字符所组成的字符串是T的后缀的话,我们就定义:
σ(T) = k,

例如 P=”abcdefg”, T = “hhhhhhhha”, 那么P的前1个字符所组成的字符串”a”是T的后缀,所以 σ(T) = 1

T=”hhhhhhab”, 那么P的前两个字符组成的字符串”ab”构成T的后缀,于是σ(T) = 2.

T=”hhhhhabc”, P的前3个字符组成的字符串”abc”构成T的后缀,于是有σ(T) = 3

依次类推。

上一节我们构造的状态机是满足以下条件的:
1. 如果P 含有m个字符,那么状态机就有m+1个状态节点,他们分别为{0,1,2…m}, 并且初始状态q0 = 0, 接收状态是m.
2. 当状态机处于状态q时,如果接收字符a, 那么状态机要跳转的下一个状态是:
δ(q, a) = σ(Pqa)

上一节我们给出的代码有这么一段:

 private void makeJumpTable() {       int m = P.length();       for (int q = 0; q <= m; q++) {           for (int k = 0; k < alphaSize; k++) {               char c = (char)('a' + k);               String Pq = P.substring(0, q) + c;               int nextState = findSuffix(Pq);               System.out.println("from state " + q + " receive input char " + c + " jump to state " + nextState);               HashMap<Character, Integer> map = jumpTable.get(q);               if (map == null) {                   map = new HashMap<Character, Integer>();               }               map.put(c, nextState);               jumpTable.put(q, map);           }       }

String Pq = P.substring(0, q) + c; 这一句代码的作用,其实就是构造字符串Pqa, 代码findSuffix(Pq); 其实就是计算σ(Pqa).

如果我们能够证明,我们前一节构造的状态机满足:
ϕ(Ti) = σ(Ti)

ϕ(Ti) 表示把文本T前i个字符构造的字符串输入到状态机后所达到的最终状态。

σ(Ti) 表示,从P的前k个字符可以组成字符串Ti的后缀,如果有某个i使得 ϕ(Ti) = m, 那么根据上面式子,字符串P将成为字符串Ti的后缀,而Ti又是字符串T的前缀,这不就意味着字符串P包含在T中了吗,所以,如果我们构造的状态机满足上面的等式,那么我们依次将T的字符输入到状态机中,当状态机跳转到状态m时,就表明字符串P包含在文本T中了。于是接下来我们将思考如何证明等式:

ϕ(Ti) = σ(Ti)

定理1:
对给定的匹配字符串P, 以及文本x, 还有任意字符a, 我们有:
σ(xa) <= σ(x) + 1

令r = σ(xa), 如果r = 0 ,上面的等式明显是成立的。因为σ(x)肯定是大于等于0的。如果r > 0, 也就是从P的第一个字符开始,连续r个字符所构成的字符串Pr能构成xa的后缀,xa = x + ‘a’,也就是字符串xa是以字符a结尾的, 那么Pr的最后一个字符也肯定是’a’, 如果我们同时将字符’a’从Pr和xa的末尾去掉,那么我们有Pr1是x的后缀,例如P=”bac”, x=”dddb”, 那么P2 = “ba” 是xa(“dddba”) 的后缀,去掉末尾的字符a后,P1=”b” 仍然是字符串x(“dddb”)的后缀。

由于Pr1 是x的后缀,而k=σ(x),表示最大的k,使得Pk是x的后缀,那么就有 r-1 <= k 也就是 r-1 <= σ(x), 调整一下就有 r <= σ(x) + 1, 由于r = σ(xa), 于是就有:
σ(xa) <= σ(x) + 1

定理2:
对匹配字符串P, 文本字符串x, 以及任意一个字符a, 如果 q = σ(x), 那么
σ(xa) = σ(Pqa)

先看个具体实例,P=”bacdb”, x=”ffffb”, 1 = q = σ(x), P1=”b”,
xa = “ffffba”, 于是2 = q = σ(xa), 而P1a = “ba”, 从字符串P第一个字符开始,连续2个字符所构成的字符串是P1a的后缀,于是有σ(P1a) = 2 = σ(xa).

如果 Pq是字符串x的后缀,那么Pqa仍然是字符串xa的后缀,如果令r=σ(xa), 也就是Pr是字符串xa的后缀,根据上一个定理有r <= q + 1. 由于字符串Pr含有r个字符, 字符串Pqa还有q + 1个字符, r <= q + 1, 也就是字符串Pr的长度比字符串Pqa要短,但是Pqa是字符串xa的后缀,同时字符串Pr也是字符串xa的后缀,于是就有PrPqa的后缀,因此就有r <= σ(Pqa), 由于 σ(xa) = r, 于是 σ(xa) <= σ(Pqa),

于此同时Pqa又是xa的后缀,于是又成立:
σ(Pqa) <= σ(xa)

综合两个不等式,我们有σ(Pqa) = σ(xa)

定理3:
给定匹配文本P, 以及要查找的文本T[1…n],那么对i=0, 1, 2…n 有:
ϕ(Ti) = σ(Ti).

也就是说,我们把字符串T[1..i]输入到自动机后,最终的状态编号k相当于匹配字符串P[1…k] 是字符串T[1..i]的后缀,如果k = m, 那么P就是T[1..i]的后缀,从而能P就包含在文本T中。

证明:
我们在i上用数学归纳法,当i=0时,结论显然成立,假设当i=k时也成立,那么有:
ϕ(Tk) = σ(Tk).
接下来我们需要证明等式ϕ(Tk+1) = σ(Tk+1)
令 q = ϕ(Tk), a = T[i+1].
ϕ(Tk+1) = ϕ(Tka) (T[1…i+1] 相当于字符串T[1…i]后面添加字符T[i+1])

ϕ(Tka) = δ(ϕ(Tk), a) = δ(q, a)
=σ(Pqa)(根据状态机的构造方法) = σ(Tia)(根据定理2和上面的假设) = σ(Ti+1).

于是我们便证明了,上一节我们所构造的状态机用于匹配字符串是正确的。

0 0