算法---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,一经查实,立即删除!

相关文章

cache初接触,并利用了DataView

我们在写代码的时候,如果数据控件要获得数据,一般方法,Conn.Open();OleDbCommand cmd;cmd new OleDbCommand(sql, Conn);GridView1.DataSource dbcenter.accessGetDataSet(sql);GridView1.DataBind();Conn.close();但如果多个数据控件要绑定数据,则比较频繁打开数据库,效率一…

Java ByteArrayInputStream reset()方法及示例

ByteArrayInputStream类reset()方法 (ByteArrayInputStream Class reset() method) reset() method is available in java.util package. reset()方法在java.util包中可用。 reset() method is used to reset this ByteArrayInputStream to the last time marked position and …

回文数猜想

问题描述&#xff1a; 一个正整数&#xff0c;如果从左向右读&#xff08;称之为正序数&#xff09;和从右向左读&#xff08;称之为倒序数&#xff09;是一样的&#xff0c;这样的数就叫回文数。任取一个正整数&#xff0c;如果不是回文数&#xff0c;将该数与他的倒序数相加…

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

文件上传 带进度条 多种风格 非常漂亮&#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;让用户根据自己的需要来动态…

线性插值算法实现图像_C程序实现插值搜索算法

线性插值算法实现图像Problem: 问题&#xff1a; We are given an array arr[] with n elements and an element x to be searched amongst the elements of the array. 给定一个数组arr []&#xff0c;其中包含n个元素和一个要在该数组的元素中搜索的元素x 。 Solution: 解&…

hdu 1197

地址&#xff1a;http://acm.hdu.edu.cn/showproblem.php?pid1197 题意&#xff1a;求一个数转换成10&#xff0c;12&#xff0c;16进制后各个位上的数的和是否相等。 mark&#xff1a;模拟进制转换。 代码&#xff1a; #include <stdio.h>int zh(int a, int n) {int su…

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

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

博客上一些项目相关源码链接

GitHub&#xff1a;https://github.com/beyondyanyu/Sayingyy

重新开启Ctrl+Alt+Backspace快捷键

UBUNTU老用户知道CtrlAltBackspace这个快捷键是用来快速重启X的在9.04中被默认关闭了&#xff0c;那如何来打开它呢&#xff1f;在终端中输入&#xff1a;sudo gedit /etc/X11/xorg.conf在其中加入&#xff1a;Section “ServerFlags”Option “DontZap” “false”EndSection退…

Java LocalDate类| 带示例的getDayOfYear()方法

LocalDate类的getDayOfYear()方法 (LocalDate Class getDayOfYear() method) getDayOfYear() method is available in java.time package. getDayOfYear()方法在java.time包中可用。 getDayOfYear() method is used to get the day-of-year field value of this LocalDate obje…

火腿三明治定理

定理&#xff1a;任意给定一个火腿三明治&#xff0c;总有一刀能把它切开&#xff0c;使得火腿、奶酪和面包片恰好都被分成两等份。 而且更有趣的是&#xff0c;这个定理的名字真的就叫做“火腿三明治定理”&#xff08;ham sandwich theorem&#xff09;。它是由数学家亚瑟•斯…

如何给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)模式下…

[转帖]纯属娱乐——变形金刚vs天网

[转帖]变形金刚2的影评-《变形金刚3 天网反击战》有一个问题困扰了我足足二十年&#xff1a;为什么汽车人要帮地球人&#xff1f;光用“所有有感知的生物都应享有自由”这个法则是根本说不过去的&#xff0c;因为猪也有感知&#xff0c;但人类就把猪圈养起来&#xff0c;随意杀…

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;部署完毕 …

标题:明码

转载&#xff1a;https://blog.csdn.net/u011747888/article/details/79781040 标题&#xff1a;明码 汉字的字形存在于字库中&#xff0c;即便在今天&#xff0c;16点阵的字库也仍然使用广泛。 16点阵的字库把每个汉字看成是16x16个像素信息。并把这些信息记录在字节中。 一…

C语言多维数组

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