《数据结构》—— 串,KMP模式算法(转载)
本文内容转载自:
KMP 算法(1):如何理解 KMP
KMP 算法(2):其细微之处
一:背景展开目录
给定一个主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出现的位置,此即串的模式匹配问题。
Knuth-Morris-Pratt 算法(简称 KMP)是解决这一问题的常用算法之一,这个算法是由高德纳(Donald Ervin Knuth)和沃恩 · 普拉特在 1974 年构思,同年詹姆斯 ·H· 莫里斯也独立地设计出该算法,最终三人于 1977 年联合发表。
在继续下面的内容之前,有必要在这里介绍下两个概念:真前缀 和 真后缀。
由上图所得, "真前缀" 指除了自身以外,一个字符串的全部头部组合;"真后缀" 指除了自身以外,一个字符串的全部尾部组合。(网上很多博客,应该说是几乎所有的博客,也包括我以前写的,都是 “前缀”。严格来说,“真前缀” 和“前缀”是不同的,既然不同,还是不要混为一谈的好!)
二:朴素字符串匹配算法展开目录
初遇串的模式匹配问题,我们脑海中的第一反应,就是朴素字符串匹配(即所谓的暴力匹配),代码如下:
/* 字符串下标始于 0 */
int NaiveStringSearch(string S, string P)
{int i = 0; // S 的下标int j = 0; // P 的下标int s_len = S.size();int p_len = P.size();while (i < s_len && j < p_len){if (S[i] == P[j]) // 若相等,都前进一步{i++;j++;}else // 不相等{i = i - j + 1;j = 0;}}if (j == p_len) // 匹配成功return i - j;return -1;
}复制代码
暴力匹配的时间复杂度为O(nm),其中n为 S 的长度,m为 P 的长度。很明显,这样的时间复杂度很难满足我们的需求。
接下来进入正题:时间复杂度为Θ(n+m)的 KMP 算法。
三:KMP 字符串匹配算法展开目录
3.1 算法流程展开目录
以下摘自阮一峰的字符串匹配的 KMP 算法,并作稍微修改。
(1)
首先,主串 "BBC ABCDAB ABCDABCDABDE" 的第一个字符与模式串 "ABCDABD" 的第一个字符,进行比较。因为 B 与 A 不匹配,所以模式串后移一位。
(2)
因为 B 与 A 又不匹配,模式串再往后移。
(3)
就这样,直到主串有一个字符,与模式串的第一个字符相同为止。
(4)
接着比较主串和模式串的下一个字符,还是相同。
(5)
直到主串有一个字符,与模式串对应的字符不相同为止。
(6)
这时,最自然的反应是,将模式串整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把 "搜索位置" 移到已经比较过的位置,重比一遍。
(7)
一个基本事实是,当空格与 D 不匹配时,你其实是已经知道前面六个字符是 "ABCDAB"。KMP 算法的想法是,设法利用这个已知信息,不要把 "搜索位置" 移回已经比较过的位置,而是继续把它向后移,这样就提高了效率。
(8)
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
模式串 | A | B | C | D | A | B | D | '\0' |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
怎么做到这一点呢?可以针对模式串,设置一个跳转数组int next[]
,这个数组是怎么计算出来的,后面再介绍,这里只要会用就可以了。
(9)
已知空格与 D 不匹配时,前面六个字符 "ABCDAB" 是匹配的。根据跳转数组可知,不匹配处 D 的 next 值为 2,因此接下来从模式串下标为 2 的位置开始匹配。
(10)
因为空格与C不匹配,C 处的 next 值为 0,因此接下来模式串从下标为 0 处开始匹配。
(11)
因为空格与 A 不匹配,此处 next 值为 - 1,表示模式串的第一个字符就不匹配,那么直接往后移一位。
(12)
逐位比较,直到发现 C 与 D 不匹配。于是,下一步从下标为 2 的地方开始匹配。
(13)
逐位比较,直到模式串的最后一位,发现完全匹配,于是搜索完成。
3.2 next 数组是如何求出的展开目录
next 数组的求解基于 “真前缀” 和“真后缀”,即next[i]
等于P[0]...P[i - 1]
最长的相同真前后缀的长度(请暂时忽视 i 等于 0 时的情况,下面会有解释)。我们依旧以上述的表格为例,为了方便阅读,我复制在下方了。
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
模式串 | A | B | C | D | A | B | D | '\0' |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
(1):i = 0,对于模式串的首字符,我们统一为next[0] = -1
;
(2):i = 1,前面的字符串为A
,其最长相同真前后缀长度为 0,即next[1] = 0
;
(3):i = 2,前面的字符串为AB
,其最长相同真前后缀长度为 0,即next[2] = 0
;
(4):i = 3,前面的字符串为ABC
,其最长相同真前后缀长度为 0,即next[3] = 0
;
(5):i = 4,前面的字符串为ABCD
,其最长相同真前后缀长度为 0,即next[4] = 0
;
(6):i = 5,前面的字符串为ABCDA
,其最长相同真前后缀为A
,即next[5] = 1
;
(7):i = 6,前面的字符串为ABCDAB
,其最长相同真前后缀为AB
,即next[6] = 2
;
(8):i = 7,前面的字符串为ABCDABD
,其最长相同真前后缀长度为 0,即next[7] = 0
。
那么,为什么根据最长相同真前后缀的长度就可以实现在不匹配情况下的跳转呢?举个代表性的例子:假如i = 6
时不匹配,此时我们是知道其位置前的字符串为ABCDAB
,仔细观察这个字符串,首尾都有一个AB
,既然在i = 6
处的 D 不匹配,我们为何不直接把i = 2
处的 C 拿过来继续比较呢,因为都有一个AB
啊,而这个AB
就是ABCDAB
的最长相同真前后缀,其长度 2 正好是跳转的下标位置。
有的读者可能存在疑问,若在i = 5
时匹配失败,按照我讲解的思路,此时应该把i = 1
处的字符拿过来继续比较,但是这两个位置的字符是一样的啊,都是B
,既然一样,拿过来比较不就是无用功了么?其实不是我讲解的有问题,也不是这个算法有问题,而是这个算法还未优化,关于这个问题在下面会详细说明,不过建议读者不要在这里纠结,跳过这个,下面你自然会恍然大悟。
思路如此简单,接下来就是代码实现了,如下:
/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{int p_len = P.size();int i = 0; // P 的下标int j = -1; next[0] = -1;while (i < p_len){if (j == -1 || P[i] == P[j]){i++;j++;next[i] = j;}elsej = next[j];}
}复制代码
一脸懵逼,是不是。。。上述代码就是用来求解模式串中每个位置的next[]
值。
下面具体分析,我把代码分为两部分来讲:
(1):i 和 j 的作用是什么?
i 和 j 就像是两个” 指针 “,一前一后,通过移动它们来找到最长的相同真前后缀。
(2):if...else... 语句里做了什么?
假设 i 和 j 的位置如上图,由next[i] = j
得,也就是对于位置 i 来说,区段 [0, i - 1] 的最长相同真前后缀分别是 [0, j - 1] 和[i - j, i - 1],即这两区段内容相同。
按照算法流程,if (P[i] == P[j])
,则i++; j++; next[i] = j;
;若不等,则j = next[j]
,见下图:
next[j]
代表 [0, j - 1] 区段中最长相同真前后缀的长度。如图,用左侧两个椭圆来表示这个最长相同真前后缀,即这两个椭圆代表的区段内容相同;同理,右侧也有相同的两个椭圆。所以 else 语句就是利用第一个椭圆和第四个椭圆内容相同来加快得到 [0, i - 1] 区段的相同真前后缀的长度。
细心的朋友会问 if 语句中j == -1
存在的意义是何?第一,程序刚运行时,j 是被初始为 - 1,直接进行P[i] == P[j]
判断无疑会边界溢出;第二,else 语句中j = next[j]
,j 是不断后退的,若 j 在后退中被赋值为 - 1(也就是j = next[0]
),在P[i] == P[j]
判断也会边界溢出。综上两点,其意义就是为了特殊边界判断。
四:完整代码展开目录
/*** * author : 刘毅(Limer)* date : 2017-03-05* mode : C++ */#include <iostream>
#include <string>using namespace std;/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{int p_len = P.size();int i = 0; // P 的下标int j = -1; next[0] = -1;while (i < p_len){if (j == -1 || P[i] == P[j]){i++;j++;next[i] = j;}elsej = next[j];}
}/* 在 S 中找到 P 第一次出现的位置 */
int KMP(string S, string P, int next[])
{GetNext(P, next);int i = 0; // S 的下标int j = 0; // P 的下标int s_len = S.size();int p_len = P.size();while (i < s_len && j < p_len){if (j == -1 || S[i] == P[j]) // P 的第一个字符不匹配或 S[i] == P[j]{i++;j++;}elsej = next[j]; // 当前字符匹配失败,进行跳转}if (j == p_len) // 匹配成功return i - j;return -1;
}int main()
{int next[100] = { 0 };cout << KMP("bbc abcdab abcdabcdabde", "abcdabd", next) << endl; // 15return 0;
}复制代码
五:算法复杂度分析展开目录
在GetNext()
和KMP()
中,我们观察i
的移动,一直往前不回溯,所以它们所耗的时间都是线性的,两者相加为
KMP 算法的时间复杂度还是很稳定的。
平均时间复杂度为
Θ(m+n)。最好时间复杂度为
O(m+(n−m))=O(n)。它发生在主串和模式串字符都不相同的情况下,例如,主串为abcdefghijk
,模式串为+-*/
。最差时间复杂度为
O(m+n)。它发生在主串和模式串都为相同的字符的情况下,例如,主串为aaaaaaaaaaaaaaaaaaaaa
,模式串为aaaa
。
六:KMP 优化展开目录
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
模式串 | A | B | C | D | A | B | D | '\0' |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
以 3.2 的表格为例(已复制在上方),若在i = 5
时匹配失败,按照 3.2 的代码,此时应该把i = 1
处的字符拿过来继续比较,但是这两个位置的字符是一样的,都是B
,既然一样,拿过来比较不就是无用功了么?这我在 3.2 已经解释过,之所以会这样是因为 KMP 不够完美。那怎么改写代码就可以解决这个问题呢?很简单。
/* P 为模式串,下标从 0 开始 */
void GetNextval(string P, int nextval[])
{int p_len = P.size();int i = 0; // P 的下标int j = -1; nextval[0] = -1;while (i < p_len){if (j == -1 || P[i] == P[j]){i++;j++;if (P[i] != P[j])nextval[i] = j;elsenextval[i] = nextval[j]; // 既然相同就继续往前找真前缀}elsej = nextval[j];}
}复制代码
在此也给各位读者提个醒,KMP 算法严格来说分为 KMP 算法(未优化版)和 KMP 算法(优化版),所以建议读者在表述 KMP 算法时,最好告知你的版本,因为两者在某些情况下区别很大,这里简单说下。
KMP 算法(未优化版): next 数组表示最长的相同真前后缀的长度,我们不仅可以利用 next 来解决模式串的匹配问题,也可以用来解决类似字符串重复问题等等,这类问题大家可以在各大 OJ 找到,这里不作过多表述。
KMP 算法(优化版): 根据代码很容易知道(名称也改为了 nextval),优化后的 next 仅仅表示相同真前后缀的长度,但不一定是最长(我个人称之为 “最优相同真前后缀”)。此时我们利用优化后的 next 可以在模式串匹配问题中以更快的速度得到我们的答案(相较于未优化版),但是上述所说的字符串重复问题,优化版本则束手无策。
所以,该采用哪个版本,取决于你在现实中遇到的实际问题。
一:起始下标之 “争”:0 和 1展开目录
/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{int p_len = P.size();int i = 0; // P 的下标int j = -1; // 相同真前后缀的长度next[0] = -1;while (i < p_len){if (j == -1 || P[i] == P[j]){i++;j++;next[i] = j;}elsej = next[j];}
}/* 在 S 中找到 P 第一次出现的位置 */
int KMP(string S, string P, int next[])
{GetNext(P, next);int i = 0; // S 的下标int j = 0; // P 的下标int s_len = S.size();int p_len = P.size();while (i < s_len && j < p_len){if (j == -1 || S[i] == P[j]) // P 的第一个字符不匹配或 S[i] == P[j]{i++;j++;}elsej = next[j]; // 当前字符匹配失败,进行跳转}if (j == p_len) // 匹配成功return i - j;return -1;
}复制代码
上述代码的起始下标都是从 0 开始的,但每个人对数组起始位置的编码习惯不同,分为两类:0 和 1。对于上面的代码,起始位置如果改为 1 的话又是怎样呢?
但它们的区别并不止如此。我们知道,KMP 算法的 next[i] 表示最长的相同真前后缀,但这对起始位置为 1 的 next[i] 却不再适用。
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
模式串 | A | B | C | D | A | B | D | '\0' |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
模式串 | A | B | C | D | A | B | D | '\0' |
next[i] | 0 | 1 | 1 | 1 | 1 | 2 | 3 | 1 |
上面两个表格表展示的是:相同模式串下不同起始位置的 next 值对比。
相比之下,起始位置为 1 的 next 值比起始位置为 0 的 next 值多了 1。多 1,不是巧合,而是必然。这很容易证明。
在 GetNext() 中,j 从 0 开始(起始位置为 1),在走了相等步后停下依次赋值给 next[i],因此相较于起始位置为 0 的 next 总是多 1。这又引起了我们的思考,多了 1 后在模式匹配中,next 还会正确的实现跳转么?当然会了,next 多 1,同时模式串的起始位置也多了 1,这就好比数学中,从 a=b 转化为 a+1=b+1,形式不同但完全等价。
二:next[i] 里最不起眼处的妙用展开目录
先来看一个问题,在主串 S 中找到模式串 P 所有可以完全匹配的位置。
很简单,典型的 KMP 模式匹配。
假设起始位置都是从 0 开始,对于上图,若已找到主串的第一个完全匹配位置即 0--4,那么请问接下来模式串如何移动?
不知道各位读者有没有注意过模式串最后末尾处的 next 值代表什么?(末尾即为字符串的结尾标志:'\0')
它代表整个模式串的最长相同真前后缀。
利用这个 next 值,我们直接可以实现跳转,更快地找到下一个匹配点。
《数据结构》—— 串,KMP模式算法(转载)相关推荐
- 数据结构 串 KMP 模式匹配详解 通俗易懂
KMP 模式匹配详解通俗易懂 KMP 模式匹配是解决字符串匹配的问题 一.原始的字符串暴力匹配 要点:子串的第一个字符匹配成功主串的字符后就依次匹配子串后面的字符,直到子串匹配结束 代码: publi ...
- 数据结构-串-KMP算法详解(Next数组计算)(简单易懂)
文章目录 KMP介绍 一.求Next数组 前后缀表 求最长公共前后缀 最长相等前后缀表转Next数组 二.使用Next数组来匹配字符串 总结 本文章就专讲kmp,暴力匹配就不讲了(我相信能搜索kmp的 ...
- 数据结构:KMP算法 串的模式匹配算法(全网最详细)
目录 KMP模式匹配算法 简述 KMP模式匹配算法原理 如果人眼来优化的话,怎样处理 接下来我们自己来发现j的移动规律: 这一段公式证明了我们为什么可以直接将j移动到k而无须再比较前面 ...
- 【数据结构】字符串 模式匹配算法的理解与实现 Brute Force算法(BF算法)与KMP算法 (C与C++分别实现)
#笔记整理 若不了解串的定义,可至: 串(string)的定义与表示 查看 串的模式匹配算法 求子串位置的定位函数 Index(S, P, pos) 求子串的定位操作通常称作串的模式匹配(其中子串P称 ...
- Java数据结构-串及其应用-KMP模式匹配算法
1.前言 KMP算法是我们数据结构串中最难也是最重要的算法.难是因为KMP算法的代码很优美简洁干练,但里面包含着非常深的思维.真正理解代码的人可以说对KMP算法的了解已经相当深入了.而且这个算法的不少 ...
- 【考研】串的模式匹配算法——KMP算法(含真题)
前言 本文内容源于对<数据结构(C语言版)>(第2版).王道讲解学习所得心得.笔记整理和总结,以便复习. 可搭配以下链接一起学习: [考研]<数据结构>知识点总结.pdf_考研 ...
- java中KMP模式,Java数据结构-串及其应用-KMP模式匹配算法
串(string)是由零个或多个宇符组成的有限序列,又名叫字符串. 定义的解释: ??串中的字符数目n称为串的长度,定义中谈到"有限"是指长度n是一个有限的数值. ??零个字符的串 ...
- 数据结构—串的详细解释(含KMP算法)
1.1串的定义 串:串是由零个或多个字符组成的有限序列,又叫字符串(其的存储结构包含顺序表存储.单链表存储的形式.) 一般记为s="a1a2a3....an"(n>=0),其 ...
- 王道数据结构课代表 - 考研数据结构 第四章 串-KMP(看毛片算法) 究极精华总结笔记(C版本)
本篇博客是考研期间学习王道课程 传送门 的笔记,以及一整年里对数据结构知识点的理解的总结.希望对新一届的计算机考研人提供帮助!!! 关于对 串 章节知识点总结的十分全面,涵括了<王道数据结构 ...
最新文章
- 【vcenter】redhat虚拟机时间莫名异常
- 【Android 界面效果10】Android中View,ViewGroup,Window之间的关系
- 上线4年从畅销Top 200到Top 20,这款刀塔传奇like卡牌正在美国市场逆流而上
- 美丽新世界:当代日本视觉文化展
- Vmware中的centos虚拟机克隆之后没有eth0
- 实例37:python
- win10你的组织已关闭自动更新问题怎么解决?
- ios php tpbase64编码,iOS Base64编码
- python将图片原比例缩小_Python批量按比例缩小图片脚本分享
- python求解三角形第三边长
- AVC与H264 区别
- Kotlin教程:Kotlin入门
- STM32F4 之STM32CubeMx编程学习
- 内网服务器使用代理上网
- 全球AI泰斗Stuart Rusell:研究人工智能太诱人了,不可抗拒
- 极限中0除以常数_0的美好
- Numpy 相关函数详解
- Tomcat 在本地创建虚拟目录myapp
- Elasticsearch中的倒排索引和读写操作原理解析
- 美国贝勒大学计算机科学专业怎么样,贝勒大学专业排名一览(含历年专业排名信息,USNEWS美国大学排名版)...
热门文章
- java 保存后中文乱码_sublime编写java程序保存编译,运行之后中文显示乱码
- vue全局注册组件实例
- java jxl之Excel的创建
- PythonGIS可视化—Matplot basemap工具箱
- 0xc000007b:vs2012+Opencv2.4.4出现0xc000007b问题
- java mvc 导入_Java SpringMVC文件导入和导出
- 0基础如何入门Python编程
- cas实现单点登录原理
- 设计模式之Composite模式(笔记)
- 模拟器显示屏上方的信号和电池图标不显示设置