KMP算法原理详解_论文解读版

2023-10-27

1. KMP算法

KMP算法是一种保证线性时间的字符串查找算法,由Knuth、Morris和Pratt三位大神发明,而算法取自这三人名字的首字母,因而得名KMP算法。

那发明这样的字符串查找算法又有什么用?在当时计算机本身非常昂贵,计算资源更是极其稀缺,而仅仅进行大文本字符查找的响应时间就很长,没法充分利用计算资源。计算机可是拿来算更有意义的事的,光为了找个文本就得浪费这么多时间,不行啊,这得优化啊。1970年,S.Cook在理论上证明了一个某种特定类型抽象计算机理论。这个理论暗示了一种在最坏情况下时也只是与M+N成正比的解决子字符串查找问题的算法。D.E.Knuth和V.R.Pratt改进了Cook证明定理的框架,并提炼为一个相对简单而使用的算法,算法最终在1976年发表。

 

首先一个例子,这里使用暴力算法进行求解(即每次查找失败时,移动一个位置,一直查找,直到找到完全匹配的字符):其中,文本txt[0:9]=“AAAAAAAAAB”,查找的字符pat[0:4]=“AAAAB”。

  • i=0时, txt[0:3]=pat[0:3],而txt[4]≠pat[4],匹配失败
  • i=1时, txt[1:4]=pat[0:3],而txt[5]≠pat[4],匹配失败
  • ...
  • i=4时, txt[4:7]=pat[0:3],而txt[8]≠pat[4],匹配失败
  • ...

暴力算法在匹配失败时每次都要回退到开头,而其实是可以避免回退这么多,那么有没有什么方法,在模式匹配失败时进回退一部分呢?

brute-force-worst-case
图1 暴力匹配算法

2. KMP原理

KMP算法的主要思想是提前判断重新开始查找的位置,而这种判断方式的生成只取决于模式本身。这里来证明其匹配模式的正确性。
 

先做以下几个符号定义

  • 待查找文本为text[1:n],长度为n
  • 模式字符串为pat[1:m],长度为m
  • k为文本当前所指位置,如text[k]
  • j为模式串所指位置,如pat[j]

假设文本和模式串匹配的起始位置为p+1,则有k=p+j,即匹配到当前位置时有text[p+j]=pat[j]

在匹配过程中,有以下两种情况

  1. j>m,即大于模式串的长度时,表示文本和模式串完全匹配,这里匹配结束。
  2. 1\leqslant j\leqslant m时,表示还在匹配,但发生了失配,接下来主要讨论这种情况。

text[p+1:p+j-1] = pat[1:j-1],但text[p+j]\neq pat[j]时(即匹配了前j-1个字符,但第j个字符不匹配),假设存在一个最小的偏移量(不存在时另外考虑),能满足pat[1:i-1]=pat[j-i+1:j-1], pat[i]\neq pat[j],即能够让偏移后的字符能在在失配处尽可能多的匹配文本。就前面这么短小精悍的一句话,是整个KMP算法的精髓所在,以下举两个例子解释这里的意思(例子1可能比较抽象,推荐先看例子2)。

例子1:

i:       1 2 3 4 5 6 7 8 9 10 11

text:  b c a b c a a b c a   b   c

pat:      c a b c a b c a c

匹配失败时,text[7]\neq pat[6],p=1, j=6 

首先匹配失败时,当前的模式串为"c a b c a b",此时文本为"c a b c a x", 其中x\neq b;为了能尽可能多的和文本“c a b c a x”的后部分内容进行匹配,需要找到最小的偏移量。

 

为什么以这种方式匹配?而且最小偏移量也可能不存在。

原因如下:失配在文本text的"c a b c a x"处,而将模式串pat偏移最小的量,使其再找到一个这样的位置,满足再一次模式串和文本的匹配"* * * x"的情况,这时再将文本中的x的值与移动后的模式串进行比较,如下所示。简单的说,在哪里跌倒就在哪里爬起来,只不过需要换一个姿势。还有一种情况其实是找不到最小偏移量,就将整个模式串大幅向右平移。

i:       1 2 3 4 5 6 7 8 9 10 11

text:  b c a b c a a b c  a   b   c

pat:               c a b c a b c a c

这样问题就简化为如何对于给定模式串,计算其最小偏移量的问题。偏移后字符能满足这个条件即可进行下一次匹配pat[1:i-1]=pat[j-i+1:j-1], pat[i]\neq pat[j]
例子2:
看了例子1,可能还没想明白,即为什么非要寻找这么一个最小偏移量不可,这是论文全文中最关键也是最精华的地方。

首先做一个很重要的假设:假设存在这么一个最小偏移量。

对于以下的文本,原先的模式串pat[5]\neq text[6],那么对于在新位置的模式串{pat}',必须满足前两位能和text匹配,本质最终还是逃不过在text[6]处再次进行决一死战。我想这里作者们为了简化问题,对text[6]失配的情况延后考虑了,避免了text参与偏移量计算造成算法更加复杂,因此只要满足pat[1:i-1]=pat[j-i+1:j-1], pat[i]\neq pat[j]的条件即可。

到了这里,问题被简化为:转化为求模式串前缀和后缀能匹配的最大长度。

只要求出这个长度,就能得出需要偏移的量了!

Wonderful!接下来就是将这个思路化为程序即可。

3. KMP实例

求模式串前缀和后缀能匹配的最大长度,我使用了以下两种方式:

  1. 最直接的理解方式,用递归的方式获取子长度进行匹配,如果不合符则缩小子长度,进行下一次匹配,直到长度为零,详情见函数“calcLongestFixed”。
  2. 论文中作者所说的方式,先计算pat[1:i-1]=pat[j-i+1:j-1]的情况,再计算pat[i]\neq pat[j]

为了写出第三节中的程序,足足花了两个晚上的时间,来来回回调了N次,就差梦里也在调了。算法相关为计算机的关键部分,今后继续加强将算法转换为计算机语言的能力!算法下所示,更全面的源码请见Github

#include <iostream>
#include <cstring>
#include <vector>
#include <cassert>
using namespace std;

// 我的计算方法
int calcLongestFixed(string strMismatch, string pattern, int max_index) {	
	if (max_index < 1)
		return -1;

	int subpos = strMismatch.length() - max_index;
	// 从最长的子字符串开始,进行匹配	
	string subSuffix = strMismatch.substr(subpos, max_index);
	string strPrefix = pattern.substr(0, max_index);

	int M = subSuffix.length();	

	string sub_true_suffix = subSuffix.substr(0, M - 1);
	string sub_true_prefix = strPrefix.substr(0, M - 1);

	char pos_i_char = strPrefix[M - 1]; // 新位置
	char pos_j_char = subSuffix[M - 1]; // 原失配处	

	// 找到pat[1, i - 1] = pat[j - i + 1, j - 1],并满足
	// pat[i] != pat[j]的情况
	if (sub_true_suffix.compare(sub_true_prefix) == 0
		&& pos_i_char != pos_j_char){
		return sub_true_suffix.length();
	} else {
		return calcLongestFixed(strMismatch, pattern, max_index - 1);
	}
}

int calcLongestFixed(string strMismatch, string pattern ){
	int i = strMismatch.length();
	int max_index = i - 1;
	return calcLongestFixed(strMismatch, pattern, max_index);
}

vector<int> InitVectorNext_my_method(string& pattern) {
	vector<int> vecNext;
	for (int i = 0; i < pattern.length(); i++) {
		string substring = pattern.substr(0, i + 1);
		int pos = calcLongestFixed(substring, pattern);

		vecNext.push_back(pos);
	}
	return vecNext;
}

// 作者论文中所描述的方法
vector<int> InitVectorNext_author_method(string &pattern)
{
	int N = pattern.length();
	vector<int> next;	
	next.resize(N, 0); 
	// 初始条件:j=0时,i肯定是不存在的定义为-1,其他位置值任意。
	next[0] = -1;

	// 优化前的代码
	vector<int> f;
	f.resize(N, -1);
	// 初始条件:j=0时,i肯定是不存在的定义为-1,其他位置值任意。
	f[0] = -1;

	for (int j = 0; j < N-1;) {
		// 先找到pat[1,i-1]=pat[j-i+1,j-1]的情况
		int t = f[j];
		while (t > -1 && pattern[j] != pattern[t])
			t = next[t];
		f[j + 1] = t + 1;
		j++;

		// 判断pat[i]和pat[j]的情况
		if (pattern[j] == pattern[f[j]])
			next[j] = next[f[j]];
		else
		{
			next[j] = f[j];
		}
	}		
	return next;
}

int search(string& strText, string& pattern, vector<int> &vecNext) {
	int i = 0, j = 0;
	int N = strText.length();
	int M = pattern.length();
	for (; i < N && j < M;) {
		if (j == -1 || strText[i] == pattern[j]) {
			j++; i++;
			if (j >= M)
				return i - M;
		} else {
			j = vecNext[j];
		}
	}
	return -1;
}

void testCalcLongestFixed();

int main()
{
	testCalcLongestFixed();
	
	/
	cout << "test 1" << endl;
	cout << "Expected: -1 0 0 0 -1 0 2" << endl;
	string patter_ryf = "ABCDABD";
	vector<int> vecNextRYF = InitVectorNext_author_method(patter_ryf);
	for (int i = 0; i < vecNextRYF.size(); i++) {
		cout << vecNextRYF[i] << " ";
	}
	cout << endl;
	/
	cout << "test 2" << endl;
	cout << "Expected: -1 0 0 -1 0 0 -1 4 -1 0" << endl << "Actual: ";
	string pattern_paper = "abcabcacab";
	vector<int> vecNext_author = InitVectorNext_author_method(pattern_paper);
	for (int i = 0; i < vecNext_author.size(); i++) {
		cout << vecNext_author[i] << " ";
	}
	cout << endl;
	/
	{
		cout << "========My method============" << endl;
		string txt1 = "aabracadabra abacadabrabracabracadabrabrabracad";
		string pattern1 = "abracadabra";
		cout << "===========================" << endl;
		vector<int> vecNext = InitVectorNext_my_method(pattern1);
		cout << search(txt1, pattern1, vecNext) << endl;

		string txt2 = "abacadabrabracabracadabrabrabracad";
		//string txt2 = "rrabasdsfsdasdfra";
		string pattern2 = "rab";
		cout << "===========================" << endl;
		vector<int> vecNext2 = InitVectorNext_my_method(pattern2);
		cout << search(txt2, pattern2, vecNext2) << endl;
	}	
	{
		cout << "========Author's method============" << endl;
		string txt1 = "aabracadabra abacadabrabracabracadabrabrabracad";
		string pattern1 = "abracadabra";
		cout << "===========================" << endl;
		vector<int> vecNext = InitVectorNext_author_method(pattern1);
		cout << search(txt1, pattern1, vecNext) << endl;

		string txt2 = "rrarabasdsfsdasdfra";
		string pattern2 = "rab";
		cout << "===========================" << endl;
		vector<int> vecNext2 = InitVectorNext_author_method(pattern2);
		cout << search(txt2, pattern2, vecNext2) << endl;
	}	
}

void testCalcLongestFixed()
{
	string pattern = "aaabc";
	string s1 = "aaac"; // aaax处失配
	assert(calcLongestFixed(s1, pattern) == 2);

	string s2 = "aaabd"; // aaabx处失配
	cout << calcLongestFixed(s2, pattern);
	assert(calcLongestFixed(s2, pattern) == 0);
}

4.小结

KMP算法主要优化字符查找的效率出发,通过观察和假设,将问题转化为寻找一个最小偏移量的问题,之后进一步将问题转化为寻找模式串中前缀和后缀的最大匹配长度。最后通过这个最大匹配长度,反向计算出最小偏移量,得到的问题的解。问题的转化和化简,循序渐进,最终得到了这个问题的一个高效解O(M+N)!要不是前前后后翻来覆去的看论文的前几节的描述,差点这个过程擦肩而过了。

欢迎一起探讨相关问题!

5. 引用文献

1. 论文:FAST PATTERN MATCHING IN STRINGS, DONALD E. KNUTHf, JAMES H. MORRIS

2. 字符串匹配的KMP算法——阮一峰

3. 从头到尾彻底理解KMP(2014年8月22日版)——高阅读量的,不过我感觉还是没看明白

4. KMP算法证明

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

KMP算法原理详解_论文解读版 的相关文章

随机推荐

  • 牛客网前端编程:合并数组 arr1 和数组 arr2。不要直接修改数组 arr,结果返回新的数组...

    方法有很多 但是思想就几种 1 两个字符直接连接起来 2 先将一个数组的字符给A 再将另一个数组的字符赋给A 本文只提供几个参考方法 方法一 使用concat function concat arr1 arr2 var arr arr ar
  • cuda 编译报错:Unresolved extern function 'cuda_tran_addr'

    出现这种问题的原因是在一个 cu文件中调用了另外一个 cu文件中的带有 device 修饰符的函数 在visual studio中需要做如下修改 如果是linux环境下需要加 dc编译选项
  • Android 10.0 系统服务之ActivityMnagerService-AMS启动流程-[Android取经之路]

    Android取经之路 的源码都基于Android Q 10 0 进行分析 Android取经之路 系列文章 系统启动篇 Android系统架构Android是怎么启动的Android 10 0系统启动之init进程Android10 0系
  • 总线周期

    3 8086执行了一个总线周期 是指8086做了哪些可能的操作 基本总线周期如何组成 在一个典型的读存储器总线周期中 地址信号 ALE信号 RD 信号 数据信号分别在何时产生 答 1 是指8086对片外的存储器或I O接口进行了一次访问 读
  • opnet 路由表

    数据结构IpT Rte Module Data是一个ip dispatch和其子进程所共有的一个数据结构 下面列出的和路由表相关的 struct IpT Rte Module Data IpT Cmn Rte Table ip route
  • 升降压电路的工作原理

    1 升压电路也叫自举电路 是利用自举升压二极管 自举升压电容等电子元件 使电容放电电压和电源电压叠加 从而使电压升高 有的电路升高的电压能达到数倍电源电压 开关直流升压电路 即所谓的boost或者step up电路 原理 the boost
  • Windows10下使用Caffe训练神经网络步骤一

    一 生成Caffe数据库 1 准备数据 参考链接中的作者提供了一些图片 共有500张图片 分为大巴车 恐龙 大象 鲜花和马五个类 每个类100张 编号分别以3 4 5 6 7开头 各为一类 其中每类选出20张用作测试 其余80张用作训练 因
  • 程序员思维

    程序员思维 起因 首先简单说一下 为什么我会想到这个话题 主要有这么几方面的原因 当我试图回过头去总结大学在计算机专业所学习的一些理论和知识的时候 发现 在学校里面学习的一些东西 走了两个极端 一个极端是偏向了细节 比如我们学习的那些 程序
  • 如何看mysql版本_如何查看mysql版本的四种方法,MySQL版本查看

    1 在终端下 mysql V 以下是代码片段 shengting login mysql V mysql Ver 14 7 Distrib 4 1 10a for redhat linux gnu i686 2 在mysql中 mysql
  • 什么是UKey?Ukey在密评中的应用 双因素身份认证 安当加密

    Ukey是什么及用途 UKey 又叫智能密码钥匙 是一种通过USB 通用串行总线接口 直接与计算机相连 具有密码验证功能 可靠高速的小型存储设备 ukey 是对现行的网络安全体系的一个极为有力的补充 基于可信计算机及智能卡技术把易用性 便携
  • ovs+dpdk 三级流表(microflow/megaflow/openflow)

    本文介绍在ovs dpdk下 三级流表的原理及其源码实现 普通模式ovs的第一和二级流表原理和ovs dpdk下的大同小异 三级流表完全一样 基本概念 microflow 最开始openflow流表是在kernel中实现的 但是因为在ker
  • uni-app发请求放在哪个生命周期函数好

    1 onLoad 页面加载了才发请求 在onLoad中发送请求是比较科学的 2 onShow 每次渲染页面就会发请求 会多次触发 会重复触发 页面隐藏显示也会触发 所以在这里发送请求不科学 3 onReady 页面初次渲染完成了 但是渲染完
  • 解决引入新的项目右边栏没有maven的问题

    解决IntejIdea引入新的项目时右侧边栏不显示maven项目的问题 导入新的idea项目的时候需要导入maven依赖 但是很多时候导入项目发现找不到maven 如图 应该怎么办呢 在help下找到find action 然后输入mave
  • <通信接口篇> I2C介绍

    目录 01 I2C总线介绍 总线物理连接 通信模式 I2C协议整体时序 02 I2C读写时序介绍 I2C写时序 主机 I2C读时序 主机 03 文章总结 大家好 这里是程序员杰克 一名平平无奇的嵌入式软件工程师 作为嵌入式开发人员 无论是硬
  • Java计算下一次提醒时间的简单算法

    Java计算下一次提醒时间的简单算法 需求分析 算法分析 代码清单 核心算法 计数器对象 工具类 代码下载地址 需求分析 在生产实践过程中 我们接到了一个这样的需求 客户接到系统做工作单后 按照要求客服人员要定时进行回访 回访提醒必须在工作
  • 2.2.2新版Banner轮播图实现

    随着Android弃用了jcenter库以后 Banner的使用也大大的和以前不同 下面就来介绍一下2 2 2版本banner的使用和Demo 文章目录 一 改进内容 二 Demo效果图 二 步骤 1 引入库 依赖banner 2 xml文
  • OSS对象存储

    文章目录 OSS 1 存储分类 2 对象存储OSS 2 1 概念组成 2 2 存储优势 2 3 与自建服务器的优势 2 4 Bucket存储空间 2 5 存储类型 2 6 对象 2 6 1 命名规范 2 6 2 存储空间与对象关系 2 7
  • Android编译系统-envsetup和lunch代码篇

    Android系统启动篇 1 android系统启动流程简介 2 android init进程启动流程 3 android zygote进程启动流程 4 Android SystemServer进程启动流程 5 android launch
  • mysql组成部分

    一 mysql组成部分 组成部分 1 连接池组件 2 管理服务和工具组件 3 SQL接口组件 4 查询分析器组件 5 优化器组件 6 缓存 cache 组件 7 插件式存储引擎 8 物理文件 1 mysql存储引擎 1 1 InnoDB存储
  • KMP算法原理详解_论文解读版

    1 KMP算法 KMP算法是一种保证线性时间的字符串查找算法 由Knuth Morris和Pratt三位大神发明 而算法取自这三人名字的首字母 因而得名KMP算法 那发明这样的字符串查找算法又有什么用 在当时计算机本身非常昂贵 计算资源更是