栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

模式匹配算法

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

模式匹配算法

Pattern Matching
  • 概念
    • 朴素算法
    • Hash 运算
    • KMP 算法


概念

形如 Java 的 String.indexOf(String),C 的 strstr(char*, char*) 这类子串定位运算,可称为模式匹配。

模式匹配是字符串中一种基本运算。

具体的来讲,给定字符串 S 1 [ 1 ∼ n ] S_{1}[1 sim n] S1​[1∼n]、 S 2 [ 1 ∼ m ] S_{2}[1 sim m] S2​[1∼m],要求求出所有使得 S 1 [ i ∼ i + m ] = S 2 [ 1 ∼ m ] S_{1}[i sim i + m] = S_{2}[1 sim m] S1​[i∼i+m]=S2​[1∼m] 的 i i i, i ≤ n − m i leq n - m i≤n−m,即子串匹配问题。

在模式匹配里 S 2 S_{2} S2​ 被称为模式 P P P, S 1 S_{1} S1​ 被称为目标 T T T,任务是在 T T T 中寻找若干 子串 P P P。

通常,我们只需求出最小的 i i i 即可。

为了方便接下来的实现,下标从0开始计算。

值得注意的是,当 P P P 为空串时,我们引用 Java 和 C 的做法,返回下标0。


模式串当然也可以多至 m m m 个,如果我们能在线性时间内处理完匹配,那么在 O ( n m ) O(nm) O(nm) 的复杂度下完成其实也是可以接受的,但可能会存在更好的方法,这里开个坑,不见得会填。


朴素算法

Brute Force


一种朴素的想法,就是我们拿出 T [ 1 ∼ n ] T[1 sim n] T[1∼n] 的所有长度为 m m m 子串 T [ 1 ∼ m ] 、 T [ 2 ∼ m + 1 ] 、 . . .    、 T [ n − m ∼ n ] T[1 sim m] 、 T[2 sim m + 1] 、... 、T[n - m sim n] T[1∼m]、T[2∼m+1]、...  、T[n−m∼n] 和 P [ 1 ∼ m ] P[1 sim m] P[1∼m] 一一对比,暴力的搜索出子串开始下标,这种做法时间复杂度在 O ( m n ) 。 O(mn)。 O(mn)。

    public int indexOf(String T, String P) {
        int n = T.length(), m = P.length();
        for (int i = 0; i <= n - m; i++) {
            boolean flag = true;
            for (int j = 0; j < m; j++)
                if (T.charAt(i + j) != P.charAt(j))
                    flag = false;
            if (flag) return i;
        }
        return -1;
    }

这段代码里显然有一个可以做常数优化的点,这里留给不熟悉字符串的读者自行思考。


Hash 运算

嗯赌


对于一个字符串,我们只需要线性时间内的预处理,然后在常数时间内就能拿到其子串的 hash值,也就是整个匹配时间能在 O ( n ) O(n) O(n) 意义下完成。

如果 T T T 的子串 hash值 和 P P P 的不等,那么它们一定不等。

如果 T T T 的子串 hash值 和 P P P 的相等,但它们不一定相等。

与 Hash 有关的内容这里便不再展开来讲,
这里只提供一个使用 Hash 模式匹配的示例。

    public final int p = 31;

    public int indexOf(String T, String P) {
        int n = T.length(), m = P.length();
        long[] THash = new long[n + 1];
        long PHash = 0, PPowM = 1;
        for (int i = 0; i < m; i++) {
            PPowM = PPowM * p;
            PHash = PHash * p + P.charAt(i);
        }
        for (int i = 0; i < n; i++)
            THash[i + 1] = THash[i] * p + T.charAt(i);
        for (int i = m; i <= n; i++) {
            long k = THash[i] - THash[i - m] * PPowM;
            if (THash[i] - THash[i - m] * PPowM == PHash)
                return i - m;
        }
        return -1;
    }

发生 hash 碰撞后,可以改用其他质数,或调整为BF算法。


KMP 算法

Knuth-Morris-Pratt Algorithm


大的要来咯

首先定义前缀函数 f f f,以及其使用。

出于惯例我们使用整型数组next保存前缀函数的结果。

这里使用名词前缀函数是为了避免与后缀数组混淆,
也就是这里计算出的 “前缀数组” 并非和后缀数组相近的概念。

我们还需要了解非前缀子串的含义:

对于字符串 S [ 1 ∼ n ] S[1 sim n] S[1∼n],它的非前缀子串有 { S [ i ∼ j ]   ∣   1 < i ≤ j ≤ n } { S[i sim j] | 1 < i leq jleq n} {S[i∼j] ∣ 1

前缀函数 f ( i ) f(i) f(i) 的值为以第 i i i 个字符结尾的非前缀子串与原串的最大匹配长度。

出于惯例, f ( 1 ) f(1) f(1) 记做 − 1 -1 −1。


我们思考一下朴素匹配的过程,给定串 T T T、 P P P:

当匹配到 T [ 1 ∼ 8 ] T[1 sim 8] T[1∼8] 比较到 P 5 P_{5} P5​ 时,匹配已经失败,
比较直接开始匹配下一个子串。

和顺序跳过已匹配距离 4 4 4 减去 f ( 4 ) f(4) f(4) 个待匹配子串。

当然现在已经匹配失败了,

但是到这里我们就能发现,
在匹配失败发生时,我们将 T T T 左 或 P P P 右 移 f ( 已 匹 配 距 离 ) f(已匹配距离) f(已匹配距离)个单位,最后的结果仍然是正确的。

也就是说在这之间 T T T 的子串绝对不可能和 P P P 相同。

严格来讲:

T [ k ∼ k + m ] T[k sim k + m] T[k∼k+m] 与 P [ 1 ∼ m ] P[1 sim m] P[1∼m] 的 [ 1 ∼ i − 1 ] [1 sim i - 1] [1∼i−1] 处相同, i i i, i ≤ m i leq m i≤m 处相异,则 T [ k + j ∼ k + m + j ] T[k + j sim k + m + j] T[k+j∼k+m+j], j ∈ [ 0 ,   i − f ( x ) ) j in [0, i - f(x)) j∈[0, i−f(x)) 必不可能与 P P P 相等。

因为 f ( i ) f(i) f(i) 是 P P P 以第 i i i 个字符结尾的非前缀子串与原串的最大匹配长度,
若存在 j 0 j_{0} j0​, i > j 0 > f ( i ) i > j_{0} > f(i) i>j0​>f(i)使得 T [ k + i − j 0 ∼ k + i ] = P [ 1 ∼ j 0 ] T[k + i - j_{0} sim k + i] = P[1 sim j_{0}] T[k+i−j0​∼k+i]=P[1∼j0​],这就意味着 j 0 j_{0} j0​ 可使 P [ 1 ∼ j 0 ] = P [ i − j 0 ∼ i ] P[1 sim j_{0}] = P[i - j_{0} sim i] P[1∼j0​]=P[i−j0​∼i],这与前缀函数匹配长度的最大性相悖。

也因此在 T [ k + j ∼ k + m + j ] T[k + j sim k + m + j] T[k+j∼k+m+j] 中每个子串都与 P P P 的前缀相异,故必不可能与 P P P 相等。


我们先朴素的求出所有 f ( x ) f(x) f(x),将其保存到next数组里,随后还会安装上述这个性质对next的求法进行一定的优化,使其能在线性时间内完成。

    public int indexOf(String T, String P) {
        int n = T.length(), m = P.length();
        int[] next = new int[m];
        for (int i = 0; i < m; i++)
            for (int j = 0; j < i; j++)
                if (P.substring(0, j).equals(P.substring(i - j, i)))
                    next[i] = j;
        for (int i = 0, j = 0; i < n;) {
            if (j == 0) {
                if (T.charAt(i) == P.charAt(j))
                { i++; j++; }
                else i++;
            } else if (T.charAt(i) == P.charAt(j))
            	{ i++; j++; }
            else j = next[j];
            if (j == m) return i - j;
        }
        return -1;
    }

可以看到在这段程序中,处理next的复杂度为 O ( m 2 ) O(m^{2}) O(m2),整个查找过程中, j j j 的减少次数不会超过 j j j 的增加次数,故 j j j 的总变化次数至多为 2 ( n + m ) 2(n + m) 2(n+m),整个算法时间复杂度为 O ( n + m 2 ) O(n + m^{2}) O(n+m2)。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/318150.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号