• [Algorithm] Pattern Matching


    1 蛮力算法

    1.1 算法描述

      蛮力串匹配是最直接最直觉的方法。可假想地将文本串和模式串分别写在两条印有等间隔方格的纸带上,文本串对应的纸带固定,模式串纸带的首字符与文本串纸带的首字符对齐,两个都沿水平方向放置。于是,只需将P(长度m)与T(长度n)中长度为m的n - m + 1个子串逐一对比,即可确定可能的匹配位置。

      不妨按自左向右的次序考查各子串。在初始状态下,T的前m个字符将与p的m个字符两两对齐。接下来,自左向右检查相互对齐的这m对字符:若当前字符对相互匹配,则转向下一对字符;反之一旦失配,则说明在此位置文本串与模式串不可能完全匹配,于是可将P对应的纸带右移一个字符,然后从其首字符开始与T中对应的新子串重新对比。若经过检查,当前的m对字符均匹配,则意味着整体匹配成功,从而返回匹配子串的位置。

      蛮力算法的正确性显而易见:既然只有在某一轮的m次比对全部成功之后才成功返回,故不至于误报;反过来,所有对齐位置都会逐一尝试,故亦不至漏报。

    1.2 算法实现

      给出蛮力算法的两个实现版本。两者原理相同、过程相仿,但分别便于引入后续的不同改进算法,故在此先做一比较。

    // #1
    int match(char* P, char* T) {
        size_t n = strlen(T), i = 0;
        size_t m = strlen(P), j = 0;
        while (j < m && i < n) {
            if (T[i] == P[j]) {
                i++;
                j++;
            }
            else {
                i -= j;
                j = 0;
            }
        }
        return i - j;
    }

      如上版本1借助整数i和j,分别指示T和P中当前接受比对的字符T[i]和P[j]。若当前字符对匹配,则i和j同时递增以指向下一个字符。一旦j增长到m则意味着发现了匹配,即可返回P相对于T的对齐位置i - j。一旦当前字符对失配,则i回退并指向T中当前对齐位置的下一字符,同时j复位P的首字符处,然后开始下一轮比对。

    // #2
    int match(char* P, char* T) {
        size_t n = strlen(T), i = 0;
        size_t m = strlen(P), j;
        for (i = 0; i < n - m + 1; i++) {
            for (j = 0; j < m; j++) 
                if (T[i + j] != P[j])
                    break;
            if (j >= m)
                break;
        }
        return i;
    }

      如上版本2借助整数i指示P相对于T的对齐位置,且随着i不断递增,对齐的位置逐步右移。在每一对齐位置i处,另一整数j从0递增至m - 1,依次指示当前接收比对的字符为T[i + j]与P[j]。因此,一旦发现匹配,即可直接返回当前的对齐位置i。

    1.3 时间复杂度

      从理论上讲,蛮力算法之多迭代n - m + 1轮,且各轮至多需进行m次比对,故总共只需做不超过(n - m + 1) * m次比对。因为m << n,渐进的时间复杂度为O(n * m)。

    2 KMP算法

    2.1 构思

      蛮力算法在最坏情况下所需时间,为文本串长度与模式串长度的乘积,故无法应用于规模稍大的应用环境,很有必要改进。

      不难发现,问题在于存在大量的局部匹配:每一轮的m次比对中,仅最后一次可能失配。而一旦发现失配,文本串、模式串的字符指针都要回退,并从头开始下一轮尝试。

      实际上,这类重复的字符比对操作没有必要。既然这些字符在前一轮迭代中已经接受过比对并且成功,那么就可以利用这些信息对蛮力算法进行改进。

    2.2 next表

      一般地,假设前一轮比对终止于T[i] != P[j]。指针i不必回退,而是将T[i]与P[t]对齐并开始下一轮比对。那么t应该如何选取呢?

      经过前一轮的比对,已经确定匹配范围应为:P[0, j) = T[i - j, i)

      于是,若模式串P经适当右移以后,能够与T的某一个(包含T[i]在内的)子串完全匹配,则一项必要条件是: P[0, t) = T[i - t, i) = P[j - t, j),也就是说在P[0, j)中长度为t的真前缀,应与长度为t的真后缀完全匹配。

      故t必来自集合: N(P, j) = { 0 <= t < j | P[0, t) = P[j - t, j) }

      一般地,该集合可能包含多个这样的t。但需要特别注意的是,其中具体由哪些t值构成,仅取决于模式串P以及前一轮比对的首个失配位置P[j],而与文本串T无关!

      若下一轮比对将从T[i]与P[t]的比对开始,这等效于将P右移j - t个单元,位移量与t成反比。因此,为保证P与T的对齐位置(指针i)绝不倒退,同时又不致遗漏任何可能的匹配,应在集合N(P, j)中挑选最大的t。也就是说,当有多个值值得试探的右移方案时,应该保守的选择其中移动距离最短者,于是,令next[j] = max( N(P, j) ),则一旦法线P[j]与T[i]失配,即可转而将P[next[j]]与T[i]彼此对准,并从这一位置开始继续下一轮比对。

      既然集合N[P, j]仅取决于模式串P与失配位置j,而与文本串无关,作为其中的最大元素,next[j]也必然具有这一性质。于是,对于任一模式串P,不妨通过预处理提前计算出所有位置j所对应的next[j]值,并整理为表格以便以后反复查询。

    2.3 KMP算法

      这里假定同过buildNext()构造出模式串P的next表,对照版本1的蛮力算法,只是在else分支对失配情况的处理有所不同,这一是KMP算法的精髓所在。

    int match(char* P, char* T) {
        int* next = buildNext(P);
        int n = (int)strlen(T), i = 0;
        int m = (int)strlen(P), j = 0;
        while (j < m && i < n) 
            if (0 > j || T[i] == P[j]) {
                i++;
                j++;
            }
            else {
                j = next[j];
            }
        delete[] next;
        return i - j;
    }

    2.4 构造next表

    int* buildNext(char* P) {
        size_t m = strlen(P), j = 0;
        int* N = new int[m];
        int t = N[0] = -1;
        while (j < m - 1) 
            if (0 > t || P[j] == P[t]) {
                j++;
                t++;
    //          N[j] = (P[j] != P[t] ? t : N[t]); 
            }
            else
                t = N[t];
        return N;
    }

    注释部分为改进版本

  • 相关阅读:
    AndroidStudio修改程序的包名,可以修改com.example.xxx之类的详解
    【Android】Android开发点击查看手机电量的小功能。学习广播的一个小技能小Demo
    linux系统工程师修改打开文件数限制代码教程。服务器运维技术
    修改linux操作系统的时间可以使用date指令 运维系统工程师必会技术
    MySQL常用指令,java,php程序员,数据库工程师必备。程序员小冰常用资料整理
    android开发之java代码中字符串对比忽略大小写。java程序员必回,可用来比对验证码等问题
    android开发之使edittext输入弹出数字软键盘。亲测可用。手机号登陆注册常用。
    java工具类去掉字符串String中的.点。android开发java程序员常用工具类
    JS——tab函数封装
    JS——样式类的添加
  • 原文地址:https://www.cnblogs.com/immjc/p/7211219.html
Copyright © 2020-2023  润新知