算法---KMP算法

字符串

  • 1 KMP算法
    • 状态机概述
    • 构建状态转移

1 KMP算法

原文链接:https://zhuanlan.zhihu.com/p/83334559

先约定,本文用pat表示模式串,长度为M,txt表示文本串,长度为N,KMP算法是在txt中查找子串pat,如果找到,返回这个子串的起始索引,否则返回-1.

用一张图演示一下KMP算法:
请添加图片描述
KMP算法永不回退txt中的索引i,不走回头路。而是借助一个数组dp,dp数组中存储的信息把pat移到正确的位置继续匹配。

KMP的难点在于计算dp数组,计算这个dp数组,之和pat有关。意思是说,只要给我个pat,我就能通过这个模式串计算出dp数组,与txt无关系。

dp 数组只和 pat 有关,那么我们这样设计 KMP 算法就会比较漂亮:

int KMP(char *pat,char *txt)
{/*第一步获取dp数组*/dp = getDP(char *pat);/* 第二步搜索*/return_value = search(char *txt,char *pat);/* 第三步 */return return_value;
}

状态机概述

为什么说KMP算法与状态机有关呢?是这样的,我们可以认为pat的匹配就是状态的转移,比如pat=“ABABC”:
请添加图片描述
如上图,圆圈内的数字就是状态,状态0是起始状态,状态5是终止状态。开始匹配时pat处于起始状态,一旦转移到终止状态,就说明在txt中找到了pat。比如说当前处于状态2,说明字符AB被匹配。请添加图片描述
另外,当处于一个状态时,接下来匹配的字符不同,pat 状态转移的行为也不同。比如说假设现在匹配到了状态 4,如果遇到字符 A 就应该转移到状态 3,遇到字符 C 就应该转移到状态 5,如果遇到字符 B 就应该转移到状态 0:
请添加图片描述

KMP算法最关键的步骤就是构造这个状态转移,要确定状态转移的行为,得确定两个变量,一个是当前的匹配状态,另一个是遇到的字符。确定了这两个变量后,就可以知道这个情况下应该转移到那个状态。

为了描述状态转移图,我们定义一个二维 dp 数组,它的含义如下:

dp[j][c] = next
0 <= j < M,代表当前的状态
0 <= c < 256,代表遇到的字符(ASCII 码)
0 <= next <= M,代表下一个状态dp[4]['A'] = 3 表示:
当前是状态 4,如果遇到字符 A,
pat 应该转移到状态 3
因为状态3之前的字符和状态4之前的字符相同dp[1]['B'] = 2 表示:
当前是状态 1,如果遇到字符 B,
pat 应该转移到状态 2

根据我们这个dp数组和刚才状态转移的过程,我们可以先写出KMP算法的search函数:

int search(char *txt,char *pat)
{int M = strlen(pat);int N = strlen(txt);//pat的初始态为0int  j = 0;for(int i=0;i<N;i++){/* 当前状态是j,遇到字符txt[i],pat应该转移到那个状态? */j = dp[j][txt[i]];/* 如果到达终止态,返回匹配开头的索引 */if(j==M) return i-M+1;}/* 没有到达终止态,返回-1表示匹配失败 */return -1;
}

构建状态转移

回想刚才说的:要确定状态转移的行为,必须明确两个变量,一个是当前的匹配状态,另一个是遇到的字符,而且我们已经根据这个逻辑确定了 dp 数组的含义,那么构造 dp 数组的框架就是这样:

for 0 <= j < M: # 状态for 0 <= c < 256: # 字符dp[j][c] = next

这个next状态应该怎么求呢?显然,如果遇到的字符c和pat[j]匹配,状态就应该向前推进一个,也就是说next=j+1,我们不妨称这种情况为状态推进:

请添加图片描述
如果字符c和pat[j]不匹配,状态就要回退(或者原地不动),我们不妨称这种情况为状态重启:
请添加图片描述
那么,如何得知在哪个状态重启呢?解答这个问题之前,我们再定义一个名字:影子状态(我编的名字),用变量 X 表示。所谓影子状态,就是和当前状态具有相同的前缀。比如下面这种情况:
请添加图片描述
当前状态j=4,其影子状态为X=2,它们都有相同的前缀AB,因为状态X和状态j存在相同的前缀,所以当状态j准备进行状态重启(遇到的字符c和pat[j]不匹配)的时候,可以通过X的状态转移来获得最近的重启位置。
比如说刚才的情况,如果状态 j 遇到一个字符 “A”,应该转移到哪里呢?首先只有遇到 “C” 才能推进状态,遇到 “A” 显然只能进行状态重启。状态 j 会把这个字符委托给状态 X 处理,也就是 dp[j]['A'] = dp[X]['A']
请添加图片描述

为什么这样可以呢?因为:既然 j 这边已经确定字符 “A” 无法推进状态,只能回退,而且 KMP 就是要尽可能少的回退,以免多余的计算。那么 j 就可以去问问和自己具有相同前缀的 X,如果 X 遇见 “A” 可以进行「状态推进」,那就转移过去,因为这样回退最少。

所以计算bp数组的伪代码就有了:

int X # 影子状态
for 0 <= j < M:for 0 <= c < 256:if c == pat[j]:# 状态推进dp[j][c] = j + 1else: # 状态重启# 委托 X 计算重启位置dp[j][c] = dp[X][c] # 更新 XX = dp[X][c]

为什么更新X使用的是: X = dp[X][c],我们首先要理解dp的含义。看下图:

请添加图片描述

dp[4]['A'] = 3 表示:
当前是状态 4,如果遇到字符 A,
pat 应该转移到状态 3
因为状态3之前的字符和状态4之前的字符相同

状态3前面的字符是ABA,状态四前面有ABA(最后一个A是要匹配的),所以dp[4]['A'] = 3dp[状态n][匹配的字符y]的值实际代表的含义是在转态n下pat字符串前面和后面能匹配的字符数(也就是前面说的影子)。

X = dp[X][c]表示在原来状态下匹配字符c,如果相同的话,影子状态就是增加1,如上图,假设现在在状态3匹配,则X=1,接下来就会计算dp[4]状态下的转移,在计算状态4转移前,首先要计算出状态4对应的影子状态,dp[X][c]就是比较X=1对应的字符B和状态3对应的字符B是否相同,如果相同就是要增加影子长度,不相同就要转移,计算对应的影子。

所以,getDP函数实现如下:

void getDP(char *pat,int M)
{/* dp[状态][字符] = 下个状态 */dp[0][pat[0]]= 1;int X=0;for(int j=1;j<M;j++){for(int c=0;c<256;c++){/* 和字符c匹配了 */if(pat[j]==c)dp[j][c]=j+1;else   /* 和字符c不匹配 */dp[j][c]=dp[X][c];}/* 更新 X*/X = dp[X][pat[j]];}
}

我们整合并化简两个程序,形成一个KMP算法。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>int dp[256][256];void getDP(char *pat,int M)
{/* dp[状态][字符] = 下个状态 */dp[0][pat[0]]= 1;int X=0;for(int j=1;j<M;j++){for(int c=0;c<256;c++){/* 和字符c匹配了 */if(pat[j]==c)dp[j][c]=j+1;else   /* 和字符c不匹配 */dp[j][c]=dp[X][c];}/* 更新 X*/X = dp[X][pat[j]];}
}int search(char *txt,char *pat)
{int M = strlen(pat);int N = strlen(txt);//pat的初始态为0int  j = 0;for(int i=0;i<N;i++){/* 当前状态是j,遇到字符txt[i],pat应该转移到那个状态? */j = dp[j][txt[i]];/* 如果到达终止态,返回匹配开头的索引 */if(j==M) return i-M+1;}/* 没有到达终止态,返回-1表示匹配失败 */return -1;
}int main(void)
{char *txt = "BCDABABC";char *pat = "ABABC";getDP(pat,strlen(pat));int count = search(txt,pat);printf("count = %d\n",count);return 0;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/379566.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

文件上传 带进度条(多种风格)

文件上传 带进度条 多种风格 非常漂亮&#xff01; 友好的提示 以及上传验证&#xff01; 部分代码&#xff1a; <form id"form1" runat"server"><asp:ScriptManager ID"scriptManager" runat"server" EnablePageMethods&quo…

同步---自旋锁

1 自旋锁的基本概念 自旋锁最多只能被一个可执行线程持有&#xff0c;如果一个执行线程试图获得一个已经被使用的自旋锁&#xff0c;那么该线程就会一直进行自旋&#xff0c;等待锁重新可用。在任何时刻&#xff0c;自旋锁都可以防止多余一个的执行线程同时进入临界区。 Linu…

实习日志----4.播放时段参数设置

由于客户在下发广告时&#xff0c;一则广告可在多个时段播放&#xff0c;这就需要设置多个播放时段的参数。 但在这种情况下&#xff0c;我并不知道用户每次需要下发几个时段&#xff0c;所以前台不能设定死。 因此我要实现这么一个功能&#xff0c;让用户根据自己的需要来动态…

linux系统编程---线程总结

线程总结1 线程的实现线程创建线程退出线程等待线程清理2 线程的属性线程的分离线程的栈地址线程栈大小线程的调度策略线程优先级3 线程的同步互斥锁读写锁条件变量信号量线程是系统独立调度和分配的基本单位。同一进程中的多个线程将共享该进程中的全部系统资源&#xff0c;例…

如何给Linux操作系统(CentOS 7为例)云服务器配置环境等一系列东西

1.首先&#xff0c;你得去购买一个云服务器&#xff08;这里以阿里云学生服务器为例&#xff0c;学生必须实名认证&#xff09; 打开阿里云&#xff0c;搜索学生服务器点击进入即可 公网ip为连接云服务器的主机 自定义密码为连接云服务器是需要输入的密码 购买即可 点击云服…

Linux系统编程---I/O多路复用

文章目录1 什么是IO多路复用2 解决什么问题说在前面I/O模型阻塞I/O非阻塞I/OIO多路复用信号驱动IO异步IO3 目前有哪些IO多路复用的方案解决方案总览常见软件的IO多路复用方案4 具体怎么用selectpollepolllevel-triggered and edge-triggered状态变化通知(edge-triggered)模式下…

c#中textbox属性_C#.Net中的TextBox.MaxLength属性与示例

c#中textbox属性Here we are demonstrating use of MaxLength property of TextBox. 在这里&#xff0c;我们演示了TextBox的MaxLength属性的使用。 MaxLength property of TextBox is used to set maximum number of character that we can input into a TextBox. Limit of M…

IIS7 MVC网站生成、发布

(1)生成。 确保System.Web.Mvc.dll在bin目录下 (2)发布网站到文件系统 (3)在IIS中为网站添加应用程序池&#xff08;一个虚拟目录&#xff0c;一个应用程序池&#xff09; (4)添加在默认网站下添加虚拟目录 &#xff08;5&#xff09;转换为应用程序 至此&#xff0c;部署完毕 …

C语言多维数组

文章目录多维数组数组名下标指向数组的指针作为函数参数的多维数组指针数组小结多维数组 如果某个数组的维数超过1&#xff0c;它就被称为多维数组&#xff0c;例如&#xff0c;下面这个声明&#xff1a; int matrix[6][10]创建了一个包含60个元素的矩阵。但是&#xff0c;它…

fwrite函数的用法示例_C语言中的fwrite()函数(带有示例)

fwrite函数的用法示例C中的fwrite()函数 (fwrite() function in C) Prototype: 原型&#xff1a; size_t fwrite(void *buffer, size_t length, size_t count, FILE *filename);Parameters: 参数&#xff1a; void *buffer, size_t length, size_t count, FILE *filenameRetu…

伙伴算法、slab机制、内存管理函数

文章目录1 伙伴算法页框操作alloc_pages()2 slabslab机制要解决的问题使用高速缓存3 内存管理函数kmallockzallocvmallocvzalloc区别参考文章内核使用struct page结构体描述每个物理页&#xff0c;也叫页框。内核在很多情况下&#xff0c;需要申请连续的页框&#xff0c;而且数…

Javaweb---监听器

1.什么是监听器 监听器就是监听某个对象的状态变化的组件。 事件源&#xff1a;被监听的对象 ----- 三个域对象 request session servletContext 监听器&#xff1a;监听事件源对象 事件源对象的状态的变化都会触发监听器 ---- 62 注册监听器&#xff1a;将监听器与事件源进行…

Linux中的Ramdisk和Initrd

Ramdisk简介先简单介绍一下ramdisk&#xff0c;Ramdisk是虚拟于RAM中的盘(Disk)。对于用户来说&#xff0c;能把RAM disk和通常的硬盘分区&#xff08;如/dev/hda1&#xff09;同等对待来使用&#xff0c;例如&#xff1a;redice # mkfs.ext2 /dev/ram0mke2fs 1.38 (30-Jun-200…

slab下kmalloc内核函数实现

文章目录kmalloc的整体实现获取高速缓存高速缓存获取index总结https://blog.csdn.net/qq_41683305/article/details/124554490&#xff0c;在这篇文章中&#xff0c;我们介绍了伙伴算法、slab机制和常见的内存管理函数&#xff0c;接下来&#xff0c;我们看看kmalloc内核函数的…

标题:三羊献瑞

标题&#xff1a;观察下面的加法算式&#xff1a; 其中&#xff0c;相同的汉字代表相同的数字&#xff0c;不同的汉字代表不同的数字。 请你填写“三羊献瑞”所代表的4位数字&#xff08;答案唯一&#xff09;&#xff0c;不要填写任何多余内容。 思路分析&#xff1a; 首先…

进程虚拟地址管理

文章目录1 地址分布实际使用中的内存区域2 进程的虚拟地址描述用户空间mmap线程之间共享内存地址的实现机制1 地址分布 现在采用虚拟内存的操作系统通常都使用平坦地址空间&#xff0c;平坦地址空间是指地址空间范围是一个独立的连续空间&#xff08;比如&#xff0c;地址从0扩…

标题:加法变乘法

标题&#xff1a;我们都知道&#xff1a;123 … 49 1225 现在要求你把其中两个不相邻的加号变成乘号&#xff0c;使得结果为2015 比如&#xff1a; 123…10*1112…27*2829…49 2015 就是符合要求的答案。 请你寻找另外一个可能的答案&#xff0c;并把位置靠前的那个乘号左…

【翻译】eXpressAppFramework QuickStart 业务模型设计(四)—— 实现自定义业务类...

这一讲&#xff0c;你将学到如何从头开始实现业务类。为此&#xff0c;将要实现Department和Position业务类。这些类将被应用到之前实现的Contact类中。你将学到引用对象自动生成用户界面的基本要素。 在此之前&#xff0c;我建议你去阅读一下 【翻译】eXpressAppFramework Qui…

内存重映射

文章目录1 kmap2 映射内核内存到用户空间使用remap_pfn_range使用io_remap_pfn_rangemmap文件操作建立VMA和实际物理地址的映射mmap 之前分配 一次性映射mmap 之前分配 Page FaultPage Fault 中分配 映射内核内存有时需要重新映射&#xff0c;无论是从内核到用户空间还是从内…

math.sqrt 有问题_JavaScript中带有示例的Math.sqrt()方法

math.sqrt 有问题JavaScript | Math.sqrt()方法 (JavaScript | Math.sqrt() Method) The Math.sqrt() method is inbuilt in JavaScript to find the square root of a number. In this tutorial, we will learn about the sqrt() method with examples. JavaScript中内置了Mat…