字符串模式匹配- KMP 主讲:朱佳 博士 华南师范大学计算机学院.

Slides:



Advertisements
Similar presentations
第十二章 小组评估 本章重点问题: 评估的设计 测量工具的选择和资料的收集 与分析.
Advertisements

电子成绩单项目实现.
我的家乡 潍坊.
C语言基础——指针的高级应用 Week 05.
第4章 数组 数组是由一定数目的同类元素顺序排列而成的结构类型数据 一个数组在内存占有一片连续的存储区域 数组名是存储空间的首地址
专题研讨课二: 数组在解决复杂问题中的作用
第8章 字元與字串處理 8-1 C語言的字元檢查函數 8-2 C語言的字串 8-3 字串的輸入與輸出 8-4 指標與字串
第4章 串 串的基本概念和C语言的串函数 串的存储结构 动态数组实现的顺序串 串的模式匹配算法——BF算法 主要知识点.
数据结构 Data Structure 主讲人:王国军,郑瑾 中南大学 中南大学信息院计科系
第一章 程序设计入门.
高级语言程序设计 主讲人:陈玉华.
Zn 模式匹配与KMP算法 Zn
資料大樓 --談指標與陣列 綠園.
函數(一) 自訂函數、遞迴函數 綠園.
东北林业大学 陈宇 ACM程序设计 东北林业大学 陈宇
补充内容 结构体 概述 定义结构体类型和定义结构体变量 结构体变量的引用 结构体变量的初始化 指针与结构体 用typedef定义类型的别名.
程序设计II 第三讲 字符串处理.
C++语言程序设计 C++语言程序设计 第六章 指针和引用 第十一组 C++语言程序设计.
数据结构 第4章 串.
C++语言程序设计 C++语言程序设计 第四章 数组及自定义数据类型 C++语言程序设计.
计算概论 第十八讲 C语言高级编程 结构与习题课 北京大学信息学院.
Object-Oriented Programming in C++ 第一章 C++的初步知识
程式撰寫流程.
Chap 8 指针 8.1 寻找保险箱密码 8.2 角色互换 8.3 冒泡排序 8.4 电码加密 8.5 任意个整数求和*
流程控制、陣列 台南市聖功女子高級中學 毛全良.
第5章 堆疊(Stacks) 5-1 堆疊的基礎 5-2 堆疊的表示法 5-3 堆疊的應用 - 運算式的計算與轉換
第7章 编译预处理 本章要求: 本章重点: 本章难点: 掌握用#define定义无参数宏和带有参数宏定义和调用方法;
第3讲 C++程序控制结构 3.1 顺序结构 3.2 分支结构 3.3 循环结构 3.4 转向控制 3.5 综合案例分析.
C++程序设计 string(字符串类) vector(容器类).
数 据 结 构 Ch.4 串 计 算 机 学 院 肖明军
THE C PROGRAMMING LANGUAGE
字符串和字符数组 字符串的输入和输出 字符串的基本操作
程序的三种基本结构 if条件分支语句 switch多路开关语句 循环语句 循环嵌套 break,continue和goto语句
第四章串 4.1 串类型定义 4.2 串的表示和实现 4.3 串的模式匹配算法 4.4 串操作应用举例.
第四章 串.
C++语言程序设计 C++语言程序设计 第六章 指针和引用 第十一组 C++语言程序设计.
切換Dev c++顯示語言 工具->環境選項(V)->介面->language (Chinese TW)
第6章 预 处 理.
第三节 堆及其应用.
第1章 概述 本章要点: C语言程序结构和特点 C语言程序的基本符号与关键字 C语言程序的编辑及运行 学习方法建议:
C++大学基础教程 第5章 数组 北京科技大学 信息基础科学系.
第五章 串和数组 5.1 串的定义和操作 5.2 串的表示和实现 5.3 字符串应用 5.4 字符串匹配算法 5.5 数组
7.1 广义表的概念 广义表是n(n≥0)个数据元素组成的序列,其中每个数据元素或是单个数据元素(简称原子),或仍然是一个广义表 。
資料結構與C++程式設計進階 排序與搜尋 講師:林業峻 CSIE, NTU 6/ 14, 2010.
C语言复习3----指针.
C++语言程序设计 C++语言程序设计 第三章 控制语句 第十一组 C++语言程序设计.
第五章 串和数组 5.1 串的定义和操作 5.2 串的表示和实现 5.3 字符串应用 5.4 字符串匹配算法 5.5 数组
C语言大学实用教程 第6章 数组 西南财经大学经济信息工程学院 刘家芬
函式庫補充資料.
物件導向程式設計 CH2.
7.1 C程序的结构 7.2 作用域和作用域规则 7.3 存储属性和生存期 7.4 变量的初始化
第11章 從C到C++語言 11-1 C++語言的基礎 11-2 C++語言的資料型態與運算子 11-3 C++語言的輸出與輸入
C++语言程序设计 C++语言程序设计 第二章 基本数据类型与表达式 第十一组 C++语言程序设计.
第4章 数 组.
C++语言程序设计 C++语言程序设计 第八章 继承 C++语言程序设计.
資料結構與C++程式設計進階 遞迴(Recursion) 講師:林業峻 CSIE, NTU 6/ 17, 2010.
项目1 C程序设计起步 学习目标: 通过该项目你可以知道: C语言的用途。 C语言的基本符号和关键字。 C语言程序的结构及特点。
第6章 预 处 理.
第二章 类型、对象、运算符和表达式.
C/C++基礎程式設計班 C++: 物件的使用、參考、重載函式 講師:林業峻 CSIE, NTU 3/28, 2015.
#include <iostream.h>
C++语言程序设计 C++语言程序设计 第二章 基本数据类型与表达式 第十一组 C++语言程序设计.
第七章  数 组.
C++语言程序设计 C++语言程序设计 第十章 多态 第十一组 C++语言程序设计.
C++语言程序设计 C++语言程序设计 第九章 类的特殊成员 第十一组 C++语言程序设计.
C++语言程序设计 C++语言程序设计 第十一章 异常处理 C++语言程序设计.
資料!你家住哪裏? --談指標 綠園.
第三章 流程控制 程序的运行流程 选择结构语句 循环结构语句 主讲:李祥 时间:2015年10月.
C++语言程序设计 C++语言程序设计 第二章 基本数据类型与表达式 第十一组 C++语言程序设计.
Presentation transcript:

字符串模式匹配- KMP 主讲:朱佳 博士 华南师范大学计算机学院

KMP算法 提纲 一.由朴素匹配到KMP 二. KMP背景介绍 三.KMP核心——跳转表next[] 四. next[]的计算

问题描述 给两个字符串S,T,如S=“aaaaaaaaaaaaaaaaaaab”, T= “aaaaaaaab”,现在要求T是否是S的字串。

朴素匹配算法 一般匹配字符串时,我们从目标字符串str(假设长度为n)的第一个下标选取和ptr长度(长度为m)一样的子字符串进行比较,如果一样,就返回开始处的下标值,不一样,选取str下一个下标,同样选取长度为n的字符串进行比较,直到str的末尾(实际比较时,下标移动到n-m)。这样的时间复杂度是O(n*m)。 https://blog.csdn.net/starstar1992/article/details/54913261

朴素匹配算法 int Index_BF ( char S [ ], char T [ ], int pos ) { /* 若串 S 中从第pos(S 的下标0≤pos<StrLength(S))个字符 起存在和串 T 相同的子串,则称匹配成功,返回第一个 这样的子串在串 S 中的下标,否则返回 -1    */ int i = pos, j = 0; while ( S[i+j] != '\0'&& T[j] != '\0') if ( S[i+j] == T[j] ) j ++; // 继续比较后一字符 else i ++; j = 0; // 重新开始新的一轮匹配 } if ( T[j] == '\0') return i; // 匹配成功   返回下标 return -1; // 串S中(第pos个字符起)不存在和串T相同的子串

O(nm)的解法 那么还有没有更快的方法呢? 首先很容易想到的就是O(nm)的解法,S串的长度为n, T串的长度为m。直接两重循环枚举,看是否会匹配。 那么还有没有更快的方法呢?

KMP算法 二.KMP背景介绍 在文本编辑中,我们经常要在一段文本中某个特定的位置找出某个特定的字符或模式。再比如,在HTTP协议里的字节流,有各种关键的字节流字段,对HTTP数据进行解释就需要用到模式匹配算法。由此,便产生了字符串的匹配问题。 KMP算法是由Knuth,Morris,Pratt三人共同提出的模式匹配算法,其对于任何模式和目标序列,都可以在线性时间内完成匹配查找,而不会发生退化,是一个非常优秀的模式匹配算法。 为何简化了时间复杂度:  充分利用了目标字符串ptr的性质(比如里面部分字符串的重复性,即使不存在重复字段,在比较时,实现最大的移动量),Knuth–Morris–Pratt algorithm, 大神knuth https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm KMP算法:可以实现复杂度为O(m+n) 为何简化了时间复杂度:  充分利用了目标字符串ptr的性质(比如里面部分字符串的重复性,即使不存在重复字段,在比较时,实现最大的移动量)。  上面理不理解无所谓,我说的其实也没有深刻剖析里面的内部原因。

KMP算法 二.由朴素匹配到KMP 假设有目标字符串(Target)T=b a b c b a b c a b c a a b c a b c a b c a c a b c,我们要在其中找到模式字符串(Pattern)P=a b c a b c a c a b。 如何做更为高效呢? 朴素匹配的时间复杂度为O(m*n); KMP的时间复杂度为O(m+n)。 由朴素匹配,我们要做16次,而KMP算法仅匹配了6次就找到了模式字符串 实际效果,后面可以会仔细讲。 Kmp6次,普通的16次

三.KMP算法核心——跳转表next[] KMP算法 其实,模式串往往含有一定的信息——前缀包含。 对于模式串而言,其前缀字符串,有可能也是模式串中的非前缀子串,这个问题我称之为前缀包含问题。 以模式串a b c a b c a c a b为例,其前缀的4个字符a b c a,正好也是模式串的一个子串a b c (a b c a) c a b,所以当目标串与模式串执行匹配的过程中,如果直到第 8 个字符才匹配失败,同时也意味着目标串当前字符之前的 4 个字符,与模式串的前 4 个字符是相同的,所以当模式串向后移动的时候,可以直接将模式串的第 5 个字符与当前字符对齐,执行比较,这样就实现了模式串一次性向前跳跃多个字符。 所以当模式串向后移动的时候,可以直接将模式串的第 5 个字符与当前字符对齐,不需要像朴素那样一个个排序对齐, 那么每次要跳跃多少呢?这与跳转表next[]存储的数值相关。

三.KMP算法核心——跳转表next[] KMP算法 模式字符串P= a b c a b c a c a b,其跳转表为: 观察,是否看的明白?如何计算next[j], 后面再详细说, 其实这里跳转表是计算错误的,只是为了下图讲解方便,先不要在意, 假设已有数组 void  getNext(const char P[],int next[]){     int  m=strlen(P);     int i=0,j;     j=next[0]=-1;     while(i<m){       while(-1!=j && P[i]!=P[j])j=next[j];       next[++i]=++j;     }   }

三.KMP算法核心——跳转表next[] 所以关键是next[] 的计算,也就是所谓的部分匹配值 KMP算法 此时 pattern 串的第 1 个字母与 target[6]对齐,从 6 向后依次匹配目标串,到 target[13]时发现 target[13]='a',而 pattern[8]='c',匹配失败,此时 next[8]=5,所以将模式串向后移动 8-next[8] = 3 个字符,将pattern[5]与 target[13]对齐,然后由 target[13]依次向后 执行匹 要仔细搞懂这里,黑板演算,移动位数 = 已匹配的字符数 - 对应的部分匹配值 = j-next[j],所以8-5 = 3, ,next 是对应的部分匹配之 2) 5-next[5]=5-1=4,所以向后移动4位 3)变成pattern[1]与target[13]对齐,这时又是第8位与target[20]不匹配,因此往前挪3位,将pattern[5]与 target[20]对齐。 所以关键是next[] 的计算,也就是所谓的部分匹配值 配操作。在整个匹配过程中,无论模式串如何向后滑动,目标串的输入字符都不会回溯,直到找到模式串,或者遍历整个目标串都没有发现匹配模式为止。 所以关键是next[] 的计算,也就是所谓的部分匹配值

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度 KMP算法 四. next[]的计算——部分匹配值 "部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

四. next[]的计算——部分匹配值 KMP算法 "部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,所以,要实现KMP算法,必须要计算出p串各个子串的部分匹配长度。通常KMP算法教材所说的next数组(或者其他名字)本质上指的就是这个“部分匹配值”,因为知道next数组的意义,所以最笨的方法就是枚举+检验找出部分匹配值,当然这样做显得太low了。 其实你可以把这个问题单独考虑成一个动态规划问题——“求一个字符串s的最大前缀后缀匹配长度”。

ababaca 四. next[]的计算——部分匹配值练习 KMP算法 *注意最长前缀:是说以第一个字符开始,但是不包含最后一个字符。 写出next 表 比如aaaa相同的最长前缀和最长后缀是aaa。**  对于目标字符串ptr,ababaca,长度是7,所以next[0],next[1],next[2],next[3],next[4],next[5],next[6]分别计算的是  a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀的长度。由于a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀是“”,“”,“a”,“ab”,“aba”,“”,“a”,所以next数组的值是[-1,-1,0,1,2,-1,0],这里-1表示不存在,0表示存在长度为1,2表示存在长度为3。这是为了和代码相对应

四. next[]的计算——举例说明 KMP算法 “部分匹配值”就是“前缀”和“后缀”的最长的共有元素的长度。以“ABCDABD”为例,所以,要实现KMP算法,必须要计算出p串各个子串的部分匹配长度。通常KMP算法教材所说的next数组(或者其他名字)本质上指的就是这个“部分匹配值”,next[i]表示从0到i的子串的部分匹配值。有时候将next[0]设为-1,那是为了简化编程实现,他们没有本质区别,看这个实际例子,s是目标,p匹配(pattern) 第一个图,绿色为匹配好的,知道黄色字符发现不匹配红色字符。第二个图右移一定距离后看是否能匹配红色字符。如果刚好匹配,那么就继续下一个字符匹配,没什么好说的。

四. next[]的计算——举例说明 再次尝试匹配红色字符 部分匹配值 KMP算法 在例子里体现部分匹配值,知道了部分匹配多少,就知道右移多少, 如何算? 部分匹配就是next[i],你可以像我们之前那样全部枚举,但效率不高 部分匹配值

四. next[]的计算——举例说明 右移长度 KMP算法 理解为求一个字符串s的最大前缀后缀匹配长度,先假设我们已经求出next[0]到next[i]的值,而(关键)现在要求next[i+1]。如下图所示,绿色的部分是next[i]表示的部分匹配长度,也就是从0到i的子串的部分匹配长度。红色小块代表第i+1个元素。假设我们用两个p为例子来求,实际想知道next[i+1]和next[i]的关系

四. next[]的计算——举例说明 ??? KMP算法 失配说明要继续右移,理解为求一个字符串s的最大前缀后缀匹配长度, 先假设我们已经求出next[0]到next[i]的值,而现在要求next[i+1]。如下图所示,绿色的部分是next[i]表示的部分匹配长度,也就是从0到i的子串的部分匹配长度。红色小块代表第i+1个元素。 ???

四. next[]的计算——举例说明 KMP算法 失配情况下,于是我们将p右移一段距离,为了区分,这里把下面的p称作p‘,他们其实是一样的。部分匹配长度next[i] = 右移长度 + 部分匹配长度x,黑板 “部分匹配长度x” = 上图中蓝色部分的部分匹配长度 = next[j]。 注意,next[j]是已知的(因为j一定小于i,而且我们假设next[0]到next[i]事先已经求出来了),所以回想刚才的公式 next[i] = 右移长度 + next[j],我们可以计算出“右移长度了”! J是多少,其实**** j = next[i]****。回想next数组的定义,next[i]表示子串p[0..i]的部分匹配长度,而j刚好就是子串p[0..i]的部分匹配长度。 J是部分匹配长度, next(j)是部分匹配长度的部分匹配长度

四. next[]的计算——举例说明 KMP算法 J=next[i]

四. next[]的计算——代码 处理匹配 处理失配 KMP算法

四. next[]的计算——代码 KMP算法 是不是和求next数组的代码很像,大致总结一下求next数组的特点:

KMP 时间上的优势 KMP算法可以做到匹配过程接近O(n)的复杂度,预 处理过程接近O(m)的复杂度。比之前提到的暴力匹 配做了很大的优化。 O(m+n)

最简单的KMP题,找出第一个字符串在第二个字符串中出现次数,Oulipo为Ouvroir de littérature potentielle缩写,音译“乌力波”,直译“潜在文学工场”,是一个由作家和数学家等组成的打破文本界限的松散的国际写作团体

描述法国作家乔治·佩雷克(Georges Perec,1936-1982)曾写过一本名为“La disparition”的书,没有字母“e”。他是Oulipo集团的成员。 这本书的引用:Tout avait Pair normal, mais tout s’affirmait faux. Tout avait Fair normal, d’abord, puis surgissait l’inhumain, l’affolant. Il aurait voulu savoir où s’articulait l’association qui l’unissait au roman : stir son tapis, assaillant à tout instant son imagination, l’intuition d’un tabou, la vision d’un mal obscur, d’un quoi vacant, d’un non-dit : la vision, l’avision d’un oubli commandant tout, où s’abolissait la raison : tout avait l’air normal mais… 佩雷克可能会在接下来的比赛中获得高分(或者更低)。人们被要求在一些主题上写一个甚至是有意义的文本,尽可能少地出现一个给定的“单词”。我们的任务是为陪审团提供一个计算这些事件的程序,以获得竞争对手的排名。这些竞争对手经常写出非常长的文本,带有无意义的含义;一系列500,000连续‘T’并不罕见。他们从不使用空格。 因此,我们希望快速找出文本中出现单词(即给定字符串)的频率。更正式地:给出字母{A‘,’B‘,’C‘,...,’Z‘}和该字母表上的两个有限字符串,单词W和文本T,计算T中W的出现次数,W的所有连续字符必须与T的连续字符完全匹配。事件可能重叠。 最简单的KMP题,找出第一个字符串在第二个字符串中出现次数

最简单的KMP题,找出第一个字符串在第二个字符串中出现次数,三个测试样例

#include <stdio.h> #include <string.h> int next[10005];      int next[10005];      char str1[1000005],str2[10005];     int cnt;     void get_next(int len2)//生成next数组      {         int i = 0,j = -1;         next[0] = -1;         while (i<len2)         {             if(j == -1 || str2[i] == str2[j])             {                 i++;                 j++;        next[i] = j;        }             else                 j = next[j];         }     }     最简单的KMP题,找出第一个字符串在第二个字符串中出现次数

int kmp(int len1,int len2)//kmp算法, 根据next 移位 { int i=0,j=0;   {         int i=0,j=0;          get_next(len2);      while(i<len1)          {              if(j==-1||str1[i]==str2[j])          {               ++i;     ++j;     }             else                  j=next[j];     if(j == len2)             {                  cnt++;                 j = next[j];             }         }     }    int main()     {         int n;          int len1,len2;       scanf("%d",&n);       getchar();         while(n--)         {             gets(str2);           gets(str1);             len1 = strlen(str1);             len2 = strlen(str2);           cnt = 0;             kmp(len1,len2);           printf("%d\n",cnt);         }         return 0;     }  最简单的KMP题,找出第一个字符串在第二个字符串中出现次数, 但是这代码里str2是匹配字符串,str1 是目标字符串。

思路:KMP,next表示模式串如果第i位(设str[0]为第0位)与文本串第j位不匹配则要回到第next[i]位继续与文本串第j位匹配。则模式串第1位到next[n]与模式串第n-next[n]位到n位是匹配的。所以思路和上面一样,如果n%(n-next[n])==0,则存在重复连续子串,长度为n-next[n]。 例如:a    b    a    b    a    b next:-1   0    0    1    2    3    4 next[n]==4,代表着,前缀abab与后缀abab相等的最长长度,这说明,ab这两个字母为一个循环节,长度=n-next[n];

if(len%(len-next[len])==0) int main(){     while(~scanf("%s",s)){       if(s[0]=='.')break;       Memset(next,0);       getNext(s,next);       int  len=strlen(s);       if(len%(len-next[len])==0) printf(“%d\n”,len/(len-next[len])); //因为要满足s=a^n       else printf("1\n");     }     return 0;   }  #include <iostream>   #include <cstdio>   #include <cstring>   #define Memset(x, a) memset(x, a, sizeof(x))   using  namespace std;   const int N=1e6+10;   int next[N];   char  s[N];      void  getNext(const char P[],int next[]){     int  m=strlen(P);     int i=0,j;     j=next[0]=-1;     while(i<m){       while(-1!=j && P[i]!=P[j]) j=next[j];       next[++i]=++j;     }   }   思路:KMP,next表示模式串如果第i位(设str[0]为第0位)与文本串第j位不匹配则要回到第next[i]位继续与文本串第j位匹配。意思直到模式串第1位到next[n]与模式串第n-next[n]位到n位是匹配的。所以思路和上面一样,如果n%(n-next[n])==0,则存在重复连续子串,长度为n-next[n]。 例如:a    b    a    b    a    b next:-1   0    0    1    2    3    4 next[n]==4,代表着,前缀abab与后缀abab相等的最长长度,这说明,ab这两个字母为一个循环节,长度=n-next[n];