字符串匹配之KMP(KnuthMorrisPratt)算法(图解)

文章目录

  • 最长相等前后缀
  • next数组
    • 概念
    • 代码实现
    • 图解GetNext中的回溯
    • 改进
    • 代码实现
  • 代码
  • 复杂度分析


最长相等前后缀

给出一个字符串 ababa

前缀集合:{a, ab, aba, abab}

后缀集合:{a, ba, aba, baba}

相等前后缀 即上面用同样颜色标识出来的集合元素,最长相等前后缀 也就是所有 相等前后缀 中最长的那一个,也就是上面的 aba 。用图片举例:
在这里插入图片描述
最长相等前后缀 就是 KMP 算法滑动的依据。我们用 next 数组存储 最长相等前后缀,以避免每次需要用到 最长相等前后缀 时都需要遍历寻找的繁琐。


next数组

概念

next[i]=j 的含义是:下标 i 之前的字符串其 最长相等前后缀 的长度为 jnext[0]= -1 (前面没有字符串单独处理)。

ababacd
next[0] = -1next[1] = 0next[2] = 0next[3] = 1next[4] = 2next[5] = 3next[6] = 0

在这里插入图片描述
s1[5] != s2[5] 时,移动 s2,让 s2 的前缀(ababa)匹配 s1 的后缀(ababa),即比较 s1[5]s2[next[5]] 。移动的距离是 不匹配位置下标相等前缀 之间的字符数量,即 5-3=2
在这里插入图片描述

从上面的例子中可以看出,next 的作用有两个:

  1. 表示该处字符不匹配时应该回溯到的字符的下标。
  2. 上文提到的:下标 i 之前的字符串其 最长相等前后缀 的长度。

代码实现

class Solution {public:void GetNext(const string& s, vector<int>& next) {int i = 0, j = -1;next[0] = -1; // 下标为0的字符前没有字符串while (i < next.size() - 1) { // 因为函数体中每次先对i++,再对next[i]进行赋值// 因此i需要小于next.size() - 1,以保证自增时不越界if (j == -1 || s[i] == s[j]) {i++;j++; /* 关于 j *//*s[i] == s[j]成立时,next[i] 在 next[i - 1] 的值(j)的基础上 + 1换言之,也就意味着相等前后缀的长度+1,新后缀结尾 i+1 对应的前缀结尾为 j+1*//* j == -1成立时,说明不存在相等前后缀,因此 i 之前的字符串的相等前后缀长度为 next[i] = (-1)++ = 0 */ next[i] = j;}else {j = next[j];// next[j] 是回溯的位置,是 j 指向的字符 之前的字符串的最长相等前后缀的长度// 该操作为了将前缀移动到后缀的位置上,假设 相等长度为 m// 相当于将 (0, j-m)、(1, j-m+1)...(m-1, j-1)匹配上// 举个例子:// 字符串:a  b a b a c d// next:  -1 0 0 1 2 3//                j   i// 由于 j 指向的 字符b 其之前的 字符串 aba 最长相等前后缀的长度为 1,// 下标1 作为 新j 就将(0, j-1)匹配上了// 换言之,只需要将 下标1 作为 新j 即可将求 ababac 最长相等前后缀问题转换为// 求 abac 最长相等前后缀的问题。}}}void getNext(const string& pattern, vector<int>& next){ // 另一种写法int i, j = 0;next[0] = -1;   //第一个位置不存在数据,为-1for (int i = 1; i < next.size(); i++){//如果当前位置没有匹配前缀,则回溯到求当前后缀的最长可匹配前缀while (j != 0 && pattern[j] != pattern[i]){j = next[j];}//如果该位置匹配,则在next数组在上一个位置的基础上加一if (pattern[j] == pattern[i]){j++;}next[i] = j;}}
};

关于提到的另一种写法,这里不多做分析,可以阅读凌桓大佬的博客。


图解GetNext中的回溯

举个直观的例子:

  • 红色部分分别为:最长相等前、后缀。

  • 蓝色部分为双指针指向的,待匹配的元素。

  • 黑色部分为未开始匹配的部分。

  • 绿色部分为 next 数组。
    在这里插入图片描述

  • 如果 s[i] == s[j] ,双指针同时后移,红色区域变大。

  • 如果不匹配,必须在 红色部分 重新寻找 相等前后缀,新的相等前后缀长度必然缩短。

紫色部分是红色部分的最长相等前后缀,可以看到,四个紫色部分都是完全相等的。同时,改变 j 的指向,回溯后 j = next[j]
在这里插入图片描述

  1. 此时,若 s[i] == s[j],又因 j 前面的紫色部分和 i 前面的紫色部分完全相等。则最长相等前后缀长度+1。
  2. 不等则进行下一次回溯。图中下一次回溯时不再有相等前后缀,因此不再有紫色部分,不断地回溯,直到 j 指向 -1,此时触发判定条件,执行 j++; i++; next[i]=j;

改进

举个例子:

  • 主串 s = “aaaabaaaaac”
  • 子串 t = “aaaac”

bc 不匹配时应该 b 与下标为 next[c] = 3 的元素 a 比,这显然是不匹配的,继续回溯,next[next[c]] 回溯后的字符依然是 a 。此时已经 没有必要再将进行回溯了。

节省效率的做法应该是当 bnext[3] 不匹配时,就直接回溯到首个 anext 指向(即下标 -1)。即:

下标01234
元素aaaac
next-10123
fail-1-1-1-13

规则:

  • 如果 i 位字符与它 next 值指向的 j 位字符相等,则该 i 位的 fail 就指向 j 位的 fail 值;
  • 如果不等,则该 i 位的 fail 值就是它自己的 next 值。

举个其他例子 ababaaab,进一步体会:

下标01234567
元素ababaaab
next-10012311
fail-10-10-1310

代码实现

这里用 next 表示上面提到的 fail 数组。

void getNext(const string& s, vector<int>& fail) {int i = 1, j = 0;fail[0] = -1; // 下标为0的字符前没有字符串while (i < fail.size()) {if (s[i] != s[j]) { // 字符不匹配fail[i] = j; // 不等时,fail[i] = next[i] = jj = fail[j]; // 回溯}else { // 字符匹配fail[i] = fail[j]; // i 指向的字符与 j 指向字符相等}j++;i++;}
}

输出结果:
在这里插入图片描述

在这里插入图片描述


代码

class Solution {
public:void GetNext(const string& s, vector<int>& next) {int i = 0, j = -1;next[0] = -1; // 下标为0的字符前没有字符串while (i < next.size()-1) { if (j == -1 || s[i] == s[j]) {i++;j++;next[i] = j;}else {// 如果当前位置没有匹配前缀,则回溯到求当前后缀的最长可匹配前缀j = next[j];}}}void getNext(const string& s, vector<int>& fail) { // 改进的next数组int i = 1, j = 0;fail[0] = -1; // 下标为0的字符前没有字符串while (i < fail.size()) {if (s[i] != s[j]) { // 字符不匹配fail[i] = j; // 不等时,fail[i] = next[i] = jj = fail[j]; // 回溯}else { // 字符匹配fail[i] = fail[j]; // i 指向的字符与 j 指向字符相等}j++;i++;}}int knuthMorrisPratt(const string& query, const string& pattern) {//不满足条件则直接返回falseif (query.empty() || pattern.empty() || query.size() < pattern.size()){return -1;}int i = 0, j = 0;int len1 = query.size(), len2 = pattern.size();vector<int> next(pattern.size(), -1); // next数组GetNext(pattern, next);while (i < len1 && j < len2){if (j == -1 || query[i] == pattern[j]){i++;j++; // i、j各增1}else j = next[j]; // i不变,j回溯}if (j == len2)return(i - len2); // 返回匹配模式串的首字符下标elsereturn -1; // 返回不匹配标志}
};

复杂度分析

  • 空间复杂度O(M): 需要借助到一个 next 数组,数组长度为 MMMMMM 为模式串长度。
  • 时间复杂度O(N + M): 时间复杂度主要包含两个部分,next 数组的构建以及对主串的遍历:next 数组构建的时间复杂度为 O(M);后面匹配中主串不回溯,循环时间复杂度为 O(N),所以 KMP 算法的时间复杂度为 O(N + M)。

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

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

相关文章

Android入门(一) | Android Studio的配置与使用

文章目录安装配置Android Studio使用Android Studio模拟器更改Android SDK的路径Hello World&#xff01;安装配置Android Studio 从这一步开始&#xff1a; 一直点 next 即可&#xff0c;直到存储路径的选择上&#xff0c;可以放到非 C 盘&#xff0c;这里我放到 D 盘了&am…

Android 入门(四) | Intent 实现 Activity 切换

文章目录Intent显式 Intent定义两个 xml 文件android:orientationmatch_parent 和 wrap_contentIntent函数定义两个 Activity隐式 Intent更多隐式 Intent 的用法用隐式 Intent 打开系统浏览器自建 Activity 以响应打开网页的 Intent向下一个活动传递数据返回数据给上一个活动In…

Android入门(二) | 项目目录及主要文件作用分析

文章目录项目目录分析app目录分析AndroidManifest.xml 分析MainActivity.kt 分析build.gradle 分析最外层目录下的 build.gradleapp 目录下的 build.gradle项目目录分析 我们来看一下 src/main/res 下的一些文件&#xff1a; .gradle 和 .idea &#xff1a;这两个目录下放置…

Android入门(三) | Android 的日志工具 Logcat

文章目录日志工具类 android.util.LogLogcat 中的过滤器日志工具类 android.util.Log Log 从属日志工具类 android.util.Log &#xff0c;该类提供了五个方法供我们打印日志&#xff1a; Log.v() &#xff1a;用于打印那些最为琐碎的、意义最小的日志信息。对应级别 verbose&…

Android入门(五) | Activity 的生命周期

文章目录Activity 的状态及生命周期实现管理生命周期FirstActivitySecondActivityDialogActivity运行结果旧活动被回收了还能返回吗&#xff1f;Activity 的状态及生命周期 Android 的应用程序运用 栈&#xff08;Back Stack&#xff09; 的思想来管理 Activity&#xff1a; …

Android入门(六) | Activity 的启动模式 及 生产环境中关于 Activity 的小技巧

文章目录Activity 的启动模式standardsingleTopsingleTasksingleInstance技巧了解当前界面是哪个 Activity随时随地退出程序启动活动的最佳写法Activity 的启动模式 standard&#xff1a;默认的启动方式&#xff0c;每次启动一个活动都会重新创建singleTop&#xff1a;如果该活…

Android入门(七) | 常用控件

文章目录TextView 控件&#xff1a;文本信息Button 控件&#xff1a;按钮EditText 控件&#xff1a;输入框ImageView 控件&#xff1a;图片ProgressBar 控件&#xff1a;进度条AlertDialog 控件&#xff1a;提示框ProgressDialog 控件&#xff1a;带有进度条的提示框TextView 控…

Android入门(八) | 常用的界面布局 及 自定义控件

文章目录LinearLayout &#xff1a;线性布局android:layout_gravity &#xff1a;控件的对齐方式android:layout_weight&#xff1a;权重RelativeLayout &#xff1a;相对布局相对于父布局进行定位相对于控件进行定位边缘对齐FrameLayout &#xff1a;帧布局Percent &#xff1…

Android入门(九)| 滚动控件 ListView 与 RecyclerView

文章目录ListView内置类型的简单运用定制数据类型提升效率点击事件RecyclerView布局管理器点击事件ListView 内置类型的简单运用 由于手机屏幕空间有限&#xff0c;能够一次性在屏幕上显示的内容不多&#xff0c;当我们的程序有大量数据需要显示的时候就可以借助 ListView 来…

Android入门(10)| Fragment碎片详解

文章目录为什么要使用碎片&#xff08;Fragment&#xff09;实例布局文件FragmentActivity动态添加碎片布局文件FragmentActivity碎片通信Fragment布局文件Activity生命周期为什么要使用碎片&#xff08;Fragment&#xff09; 我们在手机上看新闻可能是这样的&#xff1a; Re…

Android开发(1) | Fragment 的应用——新闻应用

文章目录Item&#xff1a;标题子项布局文件Java代码标题碎片布局文件Java代码新闻内容碎片布局文件Java代码新闻内容活动布局文件Java代码首界面布局文件Java代码Item&#xff1a;标题子项 布局文件 news_item.xml&#xff1a; <TextViewxmlns:android"http://schema…

Android入门(11)| 全局广播与本地广播

文章目录广播概念接收广播动态注册实例静态注册实例发送广播发送标准广播广播的跨进程特性发送有序广播本地广播广播概念 Android 中的每个应用程序都可以对自己感兴趣的广播进行注册&#xff0c;这样该程序就只会接收到自己所关心的广播内容&#xff0c;这些广播可能是来自系…

Android开发(2) | 广播 Broadcast 的应用——强制下线功能

文章目录功能简介关闭所有活动登陆界面发送强制下线的广播广播接收器AndroidManifest.xml运行结果功能简介 强制下线功能只需要弹出一个对话框&#xff0c;让用户只能点击确定按钮&#xff0c;回到登录界面。 如果在每一个活动中添加一个对话框的话太过繁琐&#xff0c;用广播…

Android入门(12)| 数据持久化

文章目录数据持久化文件存储将数据存储进文件实例从文件中读取数据实例SharedPreferences存储将数据存储进文件实例从文件中读取数据实例实现记住密码的功能SQLite数据库存储创建自己的帮助类调用自己的帮助类补全 onUpgrade() 方法增删查改增&#xff1a;SQLiteDatabase.inser…

Android入门(13)| Android权限 与 内容提供器

文章目录普通权限与危险权限运行时申请权限内容提供器运用安卓封装好的内容提供器自实现的内容提供器概念实现普通权限与危险权限 主要用于不同应用程序之间在保证被访数据的安全性的基础上&#xff0c;实现数据共享的功能。 在 Android 6.0 开始引入了运行时权限的功能&…

Android入门(14)| 通知

文章目录创建通知点击效果其它小功能实例创建通知 创建通知的步骤&#xff1a; 管理通知的 NotificationManager&#xff0c;通常通过当前 Context 的 getSystemService() 获取实例。它接受一个字符串参数用于确定获取系统的什么服务。Android 8.0(O) 版本后需要通知通道&…

Android开发(3) | 权限和内容提供器的应用——调用相机和相册

文章目录拍照并保存到 ImageView 控件布局文件 notice_layout.xml按钮 button_takePhoto 的点击操作隐式 Intent 启动后的回调AndroidManifest.xml从相册选取照片并在 ImageView 控件中显示布局文件 notice_layout.xml按钮 button_takePhoto 的点击操作自定义打开相册的方法 op…

Android开发(4) | 系统权限、MediaPlayer类 和 VideoView类 的应用——播放多媒体文件

文章目录MediaPlayer类播放音频的实例VideoView类播放视频的实例MediaPlayer类 对多种格式的音频文件提供了全面的控制方法&#xff1a; 如何获得MediaPlayer实例&#xff1f; 通过构造函数&#xff1a; MediaPlayer mp new MediaPlayer();调用 MediaPlayer.create() 方法&…

Android入门(15)| 网络

文章目录WebViewHTTP使用HttpURLConnection使用OkHttp封装网络操作封装HttpURLConnection封装OkHttpWebView WebView 可以在 应用程序中&#xff08;而不是浏览器&#xff09; 展示一些网页。 布局文件 web_layout.xml&#xff1a; <LinearLayoutxmlns:android"http…

Android入门(16)| 服务

文章目录概念Android 多线程继承 Thread继承 Runable 接口匿名类异步消息处理AsyncTask使用服务框架启动/停止服务绑定/解绑服务服务的生命周期前台服务IntentService完整版下载示例下载过程的回调接口&#xff1a;DownloadListener继承 AsyncTask 实现下载功能&#xff1a;Dow…