串_1 2016.4.27

一、串及串匹配

如何在字符串数据中,监测和提取以字符串形式给出的某一局部特性

这类操作都属于串模式匹配(string pattern matching)范畴,简称串匹配

一般地,即:

对基于同一字符表的任何文本串T(|T| = n)和模式串P(|P| = m):

判定T中是否存在某一子串与P相同

若存在(匹配),则报告该子串在T中的起始位置

串的长度n和m本身通常都很大,但相对而言n更大,即满足2 << m << n

比如,若:

T = "Now is the time for all good people to come"

P = "people"

则匹配的位置应该是29

二、暴力匹配(Brute‐force)

最直接最直觉的方法

构思:

bool Match(char* P, char *T) //暴力串匹配算法(版本1)
{
    int n = strlen(T);  //文本串长度
    int m = strlen(P);  //模式串长度
    int i = 0, j = 0;
    while (j < m && i < n) {    //自左向右逐个比对字符
        if (T[i] == P[j]) {     //若匹配
            ++i;                //则转到下一字符
            ++j;
        } else {                //文本串回退,模式串复位
            i  -= (j-1);
            j = 0;
        }
    }
    if (i-j < n-m+1) {  //i - j < n - m + 1时匹配;否则失配
        return true;
    }
    return false;
}
bool Match(char* P, char *T) //暴力串匹配算法(版本2)
{
    int n = strlen(T);  //文本串长度
    int m = strlen(P);  //模式串长度
    int i, j;
    for (i=0; i<n-m+1; ++i) {   //文本串从第i个字符起,与模式串中对应的字符逐个比对
        for (j=0; j<m; ++j) {
            if (T[i+j] != P[j]) {   //若失配,模式串整体右移一个字符,再做一轮比对
                break;
            }
        }
        if (j == m) {   //找到匹配子串
            break;
        }
    }
    if (i < n-m+1) {    //i<n-m+1时匹配;否则失配
        return true;
    }
    return false;
}

最好情况: 只经过一轮比对,即确定匹配

#比对 = m = O(m)

最坏情况: 每轮都比对至P的末字符,且反复如此

每轮循环:#比对 = m‐1(成功) + 1(失败) = m

循环次数 = n ‐ m + 1

一般 m ? n,故总体地,#比对 = m × (n‐m+1) = O(n×m)

三、KMP算法

(1)构思

暴力算法应用于规模大的应用环境,很有必要改进

为此,不妨从分析以上最坏情况入手

稍加观察不难发现,问题在于这里存在大量的局部匹配:

每一轮的m次对比中,仅最后一次可能失配,文本串、模式串的字符指针都要回退,并从头开始下一轮尝试

实际上,这类重复的字符比对操作没有必要

既然这些字符在前一轮迭代中已经接受过比对并且成功,我们也就掌握了它们的所有信息

那么,如何利用这些信息,提高匹配算法的效率呢?

记忆 = 经验= 预知力

经过前一轮对比,我们已经清楚地知道,子串 T[i - j, i] 完全由 ‘0‘ 组成

记住这一性质我们便可预测出:

在回退之后紧接着的下一轮对比中,前 j - 1 次比对必然都会成功

因此,可直接 ”令 i 保持不变、j = j - 1 “ ,然后继续比对

如此,下一轮只需 1 次比对,共减少 j - i 次!

上述 ”令 i 保持不变、j = j - 1 “ 的含义,可理解为 “令 P 相对于 T 右移一个单元,然后从前一失配位置继续比对”:

实际上这一技巧可推而广之:

利用以往的成功比对所提供的信息(记忆),不仅可避免文本串字符指针的回退,而且可使模式串尽可能大跨度地右移(经验)

一般实例

再来考查一个更具一般性的实例

本轮对比进行到发现 T[i] = ‘E‘ ≠ ‘O‘ - P[4] 失配后,在保持 i 不变的同时,应将模式串 P 右移几个单元呢?

有必要逐个单元地右移吗?

不难看出,在这一情况下移动一个或两个单元都是徒劳的

事实上,根据此前的比对结果,此时必然有

T[i - 4, i) = P[0,4) = "REGR"

若在此局部能够实现匹配,则至少紧邻于 T[i] 左侧的若干字符均得到匹配

比如,当 P[0] 与 T[i - 1] 对齐时,即属于这种情况

进一步地,若注意到 i - 1 是能够如此匹配的最左侧位置,即可直接将 P 右移 4 - 1 = 3 个单元(等效于 i 保持不变,同时令 j = 1),然后继续比对

(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] 值,并整理为表格以便以后反复查询

亦即,将“记忆力”转化为“预知力”

(3)KMP算法

Knuth 和 Pratt 师徒,与 Morris 几乎同时发明了这一算法

他们稍后联合署名发表该算法,并以其姓氏首字母命名

这里,假定可通过 Build_Next() 构造出模式串 P 的 next 表

对照“暴力串匹配算法(版本1)”,只是在 else 分支对失配情况的处理手法有所不同,这也就是 KMP 算法的精髓所在

bool Match(char* P, char *T) //KMP主算法(待改进版)
{
    Build_Next(P);
    int n = strlen(T);  //文本串长度
    int m = strlen(P);  //模式串长度
    int i = 0, j = 0;
    while (j < m && i < n) {    //自左向右逐个比对字符
        if (j < 0 || T[i] == P[j]) {     //若匹配,或P已移出最左侧(两个判断的次序不可交换)
            ++i;                //则转到下一字符
            ++j;
        } else {
            j = next[j];        //模式串右移(注意:文本串不用回退)
        }
    }
    if (i-j < n-m+1) {  //i - j < n - m + 1时匹配;否则失配
        return true;
    }
    return false;
}

(4)next[0] = -1

不难看出,只要 j > 0 则必有 0 ∈ N(P, j)

此时 N(P, j) 非空,从而可以保证“在其中取最大值”这一操作的确可行

但反过来,若 j = 0,则即便集合 N(P, j) 可以定义,也必是空集

此种情况下,又如何定义 next[j = 0] 呢?

反观串匹配的过程

若在某一轮对比中首对字符即失配,则应将 P 直接右移一个字符,然后启动下一轮对比

因此,不妨假想地在 P[0] 的左侧“附加”一个 P[-1] ,则该字符与任何字符都是匹配的

就实际效果而言,这一处理方法完全等同于“令 next[0] = -1”

巧妙的使用哨兵,可以

-简化代码

-统一理解

(5)next[j + 1]

那么,若已知 next[0, j] ,如何才能递推地计算出 next[j + 1]?

是否有高效方法?

由 next 表的功能定义,next[j + 1] 的下一候选者应该依次是 next[ next[j] ] + 1,

next[ next[ next[j] ] ] + 1,. . .

因此,只需反复用 next[t] 替换 t (即令 t = next[t]),即可按优先次序遍历以上候选者

一旦发现 P[j] 与 P[t] 匹配(含与 P[t = -1] 的通配),即可令 next[j + 1] = next[t] + 1

既然总有 next[t] < t,故在此过程中 t 必然严格递减

同时,即便 t 降至 0 ,亦必然会终止于通配的 next[0] = -1 ,而不致下溢

如此,该算法的正确性完全可以保证

(6)构造 next 表

void Build_Next(char* P)    //构造模式串 P 的 next 表
{
    int m = strlen(P);
    int j = 0;  //“主”串指针
    int t = next[0] = -1;      //模式串指针
    while (j < m-1) {
        if (t < 0 || P[j] == P[t]) {    //匹配
            ++j;
            ++t;
            next[j] = t;   //此句可改进. . .
        } else {    //失配
            t = next[t];
        }
    }
}

可见,next 表的构造算法与 KMP 算法几乎完全一致

实际上按照以上分析,这一构造过程完全等效于模式串的自我匹配,因此两个算法在形式上的近似亦不足为怪

(7)性能分析

为此,请留意 KMP 算法中用作字符指针的变量 i 和 j

若令 k = 2i - j 并考查 k 在 KMP 算法过程中的变化趋势,则不难发现:

while 循环每迭代一轮,k 都会严格递增

实际上,对应于 while 循环内部的 if-else 分支,无非两种情况:

若转入 if 分支,则 i 和 j 同时加一,于是 k = 2i - j 必将增加

反之若转入 else 分支,则尽管 i 保持不变,但在赋值 j = next[j] 之后 j 必然减小,于是 k = 2i - j 也必然会增加

纵观算法的整个过程:

启动时有 i = j = 0,即 k = 0 ,算法结束时 i ≤ n 且 j ≥ 0,故有 k ≤ 2n

在此期间尽管整数 k 从 0 开始持续地严格递增,但累计增幅不超过 2n,故 while 循环至多执行 2n 轮

另外,while 循环体内部不含任何循环或调用,故只需 O(1) 时间

因此,若不计构造 next 表所需的时间,KMP 算法本身的运行时间不超过 O(n)

既然 next 表构造算法的流程与 KMP 算法并无实质区别,故按照上述分析可知,next 表的构造仅需 O(m) 时间

综上可知,KMP 算法的总体运行时间为 O(n + m)

(8)继续改进

尽管以上 KMP 算法已可保证线性的运行时间,但在某些情况下仍有进一步改进的余地

考查模式串 P = “000010”

在 KMP 算法过程中,假设前一轮对比因 T[i] = ‘1‘ ≠ ‘0‘ = P[3] 失配而中断

于是按照 next 表,接下来 KMP 算法将依次将 P[2]、P[1] 和 P[0] 与 T[i] 对准并做比对

这三次比对都报告“失配”

那么,这三次比对的失败结果属于偶然吗?

进一步地,这些比对能否避免?

实际上,即便说 P[3] 与 T[i] 的比对还算必要,后续的这三次比对却都是不必要的

实际上,它们的失败结果早已注定

只需注意到 P[3] = P[2] = P[1] = P[0] = ‘0‘ ,就不难看出这一点

既然经过此前的比对已经发现 T[i] ≠ P[3] ,那么继续将 T[i] 和那些与 P[3] 相同的字符做比对,既重蹈覆辙,更徒劳无益

记忆 = 教训= 预知力

就算法策略而言,引入 next 表的实质作用,在于帮助我们利用以往成功对比所提供的“经验”,将记忆力转化为预知力

然而实际上,此前已进行过的比对还远不止这些,确切地说还包括那些失败的比对----作为“教训”,它们同样有益,但可惜此前一直被忽略了

以往所做的失败比对,实际上已经为我们提供了一条极为重要的信息---- T[i] ≠ P[3] ---- 可惜我们却未能有效地加以利用

原算法之所以会执行后续三次本不必要的比对,原因在于未能充分汲取教训

改进

为把这类“负面”引入 next 表,只需将集合 N(P, j) 的定义修改为:

N(P, j) = { 0 ≤ t < j  |  P[0, t) = P[j - t, j) 且 P[t] ≠ P[j] }

也就是说,除“对应于自匹配长度”以外,t 只有还同时满足“当前字符对不匹配”的必要条件,方能归入集合 N(P, j) 并作为 next 表项的候选

void Build_Next(char* P)    //构造模式串 P 的 next 表(改进版本)
{
    int m = strlen(P);
    int j = 0;  //“主”串指针
    int t = next[0] = -1;      //模式串指针
    while (j < m-1) {
        if (t < 0 || P[j] == P[t]) {    //匹配
            ++j;
            ++t;
            next[j] = (P[j] != P[t] ? t : next[t]); //注意此句与未改进之前的区别
        } else {    //失配
            t = next[t];
        }
    }
}

改进后的算法与原算法的唯一区别在于,每次在 P[0, j) 中发现长度为 t 的真前缀和真后缀相互匹配之后,还需进一步检查 P[j] 是否等于 P[t]

唯有 P[j] ≠ P[t] 时,才能将 t 赋予 next[j],否则,需转而代之以 next[t]

改进后的 next 表的构造算法同样只需 O(m) 时间

BJFUOJ1553 易彰彪的一张表

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int maxn = 30 + 10;
char T[maxn * maxn];
char P[maxn * maxn];
int next[maxn * maxn];
int n, m;
int T_len, P_len;

void Build_Next(char* P);
bool Match(char* P, char *T);

int main()
{
//    freopen("in.txt", "r", stdin);

    while (scanf("%d%d", &n, &m) != EOF) {
        T_len = n*m;
        for (int i=0; i<T_len; ++i) {
            cin>>T[i];
        }
        strlwr(T);
        scanf("%s", P);
        P_len = strlen(P);
        strlwr(P);
        if (Match(P, T)) {
            printf("YES\n");
        } else {
            printf("NO\n");
        }
    }
    return 0;
}

void Build_Next(char* P)
{
    int j = 0;
    int t = next[0] = -1;
    while (j < P_len-1) {
        if (t < 0 || P[j] == P[t]) {
            ++j;
            ++t;
            next[j] = (P[j] != P[t] ? t : next[t]);
        } else {
            t = next[t];
        }
    }
}

bool Match(char* P, char *T)
{
    Build_Next(P);
    int i = 0, j = 0;
    while (j < P_len && i < T_len) {
        if (j < 0 || T[i] == P[j]) {
            ++i;
            ++j;
        } else {
            j = next[j];
        }
    }
    if (i-j < T_len-P_len+1) {
        return true;
    }
    return false;
}
时间: 2024-11-15 06:19:37

串_1 2016.4.27的相关文章

2016/1/27 1, File 创建 删除 改名 换路径 2,输出流 不覆盖 换行输入 3,输入流

1, File  创建  删除  改名  换路径 1 package Stream; 2 3 import java.io.File; 4 5 import java.io.IOException; 6 7 public class OutIn { 8 9 /** 10 * 输入输出 11 */ 12 public static void main(String[] args) { 13 File f1 = new File("d:/book.txt"); // 第一种 实例化文件名称

2016.9.27小程序---数据库练习1

1 -- createbbc, 1, 50 2 DROP TABLE bbc; 3 4 5 CREATE INDEX bbc_region ON bbc(region); 6 7 GO 8 -- tabbbc, 1, 50 9 INSERT INTO bbc VALUES ('Afghanistan','South Asia',652225,26000000,NULL); 10 INSERT INTO bbc VALUES ('Albania','Europe',28728,3200000,66

2016.2.27日(开学)学习总结

复习时间:2016.2.26 复习地点:南书院 复习内容:PHP Web程序设计与Ajax技术 知识点: 1)c/s结构与b/s结构的比较 c/s不易于部署,升级困难,同时对客户端的操作系统也有要求: b/s结构响应速度明显不如c/s结构,每次访问(前进或者后退)都会导致页面的刷新. 2)web应用程序与网站 网站侧重于将数据的展示形式,而web应用程序的真正核心功能是对数据库进行处理(例如b/s的管理信息系统) 3)HTTP通信协议是浏览器与web服务器之间通信的语言  浏览器只能解析HTML

angular2 组件之间通讯-使用服务通讯模式 2016.10.27 基于正式版ng2

工作中用到ng2的组件通讯 奈何官方文档言简意赅 没说明白 自己搞明白后 整理后分享下 rxjs 不懂的看这篇文章 讲很详细 http://www.open-open.com/lib/view/open1462525661610.html 以下是服务代码 1 import { Injectable } from '@angular/core'; 2 import {Subject}from"rxjs/Subject"; 3 @Injectable() 4 export class CS

【2016.5.27】再见,软件工程,你好,软件工程。

今天是5月27号,软件工程课已经结了一个星期了,后天就是软件工程期末考试的日子了,想着这一个学期的走来,从新奇,到抵触,到接受,到理解,其中的酸甜苦辣,只有自己知道. 今天距离考研还有210天,说实话,如果没有考研,大创,还有一些乱七八糟的比赛的话,我会更加投入到软件工程的学习中来,很可惜,也很无奈.就像水奔流到海不回头一样,我们也没有后悔药可以吃,既然选择,就必须承担起他引发的一切后果. 我的兴趣在于嵌入式,作为一个讲究设计,思考的方向,当时我感觉,软件工程的理论实用性不是那么的强,但是现在,

2016.11.27

今天是我第一次开始打算记录自己的计划,现在回头看看,从6月12号毕业到今天也已经度过了169天了,毕业之后一直在尝试投简历,刚开始还是打算在家附近找一份工作,后来觉得家附近并没有很好前景的企业,当时也只是漫无目的的打算找一份过得去的工作先做起来,便开始扩大范围的投简历,就这样在家待了大概三周后,我没有了起初的悠哉,开始焦虑起来,以至于我居然去了所谓的类似P2P公司面试,现在想想确实有些好笑,当时也没太认真,总感觉毕业了还歇在家怎么也说不过去. 两周后. 度过了难熬的时间,我有了第一份工作,是一家

2016.5.27 php测试中敏感度高,怎么调整

在测试PHP代码的过程中,会遇到这样的问题:PHP提示Notice: Undefined variable,遇到这样的问题很纠结,但是很容易解决. 今天晚上,我就遇到了这样的问题,到网上搜索了很多解决方法,整理如下,仅供参考,同时再次感谢网上各位大牛给大家提供的各式各样的方法! PHP默认的配置会报这个错误,虽然有利于发现错误,但同时字现实实践中会出现很多问题. 解决方法如下: 一.修改php.ini配置文件 error_reporting设置:            找到error_repor

2016/4/27 网络编程

OSI:开放系统互联参考模型 是IOS提出为异构系统互联提供了概念性的框架 : 分为七层  应用  表示  会话  传输  网络  数据连接  物理 网络编程  编写能直接或间接的通过网络协议与其他计算机上的某个程序进行通讯 : Telnet协议  23端口        简单邮件传输   25端口     文件传输  21端口    超文本传输  80端口 TCP 可靠的双流向协议 可以发送任意数量的数据 能够确认消息检测消息的的错误 并恢复 UDP 协议 不太可靠 单项传输 数据可能丢失 i

2016/02/27 codes

b2Math.b2Abs = function(a){return a > 0.0 ? a : -a ;};b2Math.b2AbsV = function(a){var b = new b2Vec2(b2Math.b2Abs(a.x),b2Math.b2Abs(a.y));return b};b2Math.b2AbsM=function(A){var B=new b2Mat22(0,b2Math.b2AbsV(A.col1),b2Math.b2AbsV(A.col2));return B;};