IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 模式匹配算法 -> 正文阅读

[数据结构与算法]模式匹配算法


概念


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

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

具体的来讲,给定字符串 S 1 [ 1 ~ n ] S_{1}[1 \sim n] S1?[1n] S 2 [ 1 ~ m ] S_{2}[1 \sim m] S2?[1m],要求求出所有使得 S 1 [ i ~ i + m ] = S 2 [ 1 ~ m ] S_{1}[i \sim i + m] = S_{2}[1 \sim m] S1?[ii+m]=S2?[1m] i i i i ≤ n ? m i \leq n - m in?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[1n] 的所有长度为 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[1m]T[2m+1]...??T[n?mn] P [ 1 ~ m ] P[1 \sim m] P[1m] 一一对比,暴力的搜索出子串开始下标,这种做法时间复杂度在 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[1n],它的非前缀子串有 { S [ i ~ j ] ? ∣ ? 1 < i ≤ j ≤ n } \{ S[i \sim j] \ | \ 1 < i \leq j\leq n\} {S[ij]??1<ijn},也就是并非以字符串头开头的子串。

前缀函数 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[18] 比较到 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[kk+m] P [ 1 ~ m ] P[1 \sim m] P[1m] [ 1 ~ i ? 1 ] [1 \sim i - 1] [1i?1] 处相同, i i i i ≤ m i \leq m im 处相异,则 T [ k + j ~ k + m + j ] T[k + j \sim k + m + j] T[k+jk+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[1j0?],这就意味着 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[1j0?]=P[i?j0?i],这与前缀函数匹配长度的最大性相悖。

也因此在 T [ k + j ~ k + m + j ] T[k + j \sim k + m + j] T[k+jk+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)

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-10-13 11:40:49  更:2021-10-13 11:41:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 6:30:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码