C数据结构:KMP算法详解(呕心沥血)
KMP算法
作者心声
(* ̄︶ ̄)
首先希望各位不要被KMP算法吓到,理解之后其实很容易上手。
这篇博客的目的是希望通过鄙人的方式写出一些通俗易懂的“ 妙手 ”想法去理解,所以在观看本篇博客前请三思~
因为网上对于KMP算法的解释很多,比我解释的还准确,所以如果你想理解更深更透彻的话可以搜索KMP算法,找到浏览次数最高的即可,若你只是想知道或者只是想解决掉KMP算法是如何执行和认识该算法的运行过程或许我这篇博客能帮到你。
我尽可能的把我在学习过程中遇到的困难和不理解的地方都写在这篇博客上,所以初衷还是希望用自己的理解方式来帮助大家理解KMP算法。
(说不定我之前遇到的问题就是你一直不理解的难点呢~)
了解暴力求解(必需会)
假设我们有一段特别长的文本内容称之为主串,里面全是字母,现我给你一个模式串,要求你在主串中找到这个模式串在主串中的位置。
举个栗子:主串:a p a c a p a c k p a c c
要找的模式串为: p a c k p a c
a p a c a p a c k p a c c | 主串 |
---|---|
^ ^ ^ ^^ p a c k p a c | 模式串首字母 p 在主串的位置是:6 |
我们肉眼很快就能找到该单词所在位置,
(在主串数组下标就是5)
按照 p a c k p a c z 中的首字母 p 出现的位置作为该 p a c k p a c z 在主串中的位置是第6个
我们在学习KMP算法之前必须对暴力求解的方法有一个了解。
#什么是暴力求解#:
i 为主串的下标
j 为模式串的下标
假设现在从主串的第一个字符开始和模式串匹配
所以 i 和 j 下标为0 (数组中0是首元素)
(i 和 j 现在都是0) i 和 j 在遍历的时候,当 主串 中一个字符和模式串中的不一样,i 就立马 回溯到 i + 1 的位置重新开始和模式串匹配,
然而 模式串 只要匹配错误就立马 j = 0,直接返回数组首元素位置。
再次拿上面的例子解释:
| a p a c a p a c k p a c z | ----------主串
| p a c k p a c z | ------------------模式串
现在 i 和 j 都为 0 下标,一开始 0 就不匹配了,
所以 i 回溯到 0 + 1 下标的位置重新开始匹配
j 在每一次匹配失败的都变为 0 也是重头开始匹配
然后如果又匹配错误,i 回溯到2 ,然后 j 又重新变为0。
以此类推下去,最坏的情况就是 i 一直回溯
@@不难推出,两个串要匹配成功 i 需要回溯 3 次@@
(可以自己试着推一下)
注:虽然该暴力解法很easy很合情合理,但是当这个主串非常大,甚至不局限与字符串而是要应用在大量数据的时候就显得非常鸡肋,因为匹配的时间实在是太长了。
OK现在开始学习KMP算法了,上面的暴力求解应该理解了吧?
( 没能理解恕鄙人无能为力…… )
如果理解了,那么恭喜你,成功把KMP算法理解了三分之一了,因为这个和KMP算法有着异曲同工之处。
KMP算法详解
记住我这段话(你会爱上它的)← :
KMP算法其实就是主串一直i++下去,不用回溯
,然后模式串回溯的位置变成看前后缀相同的个数作为回溯的下标位置,
比如前后缀有一个相同的,就回溯到1的位置,
abcas,假如到了abca的时候下一个s匹配错误,
那么这时候可以看到s前面的前后缀有一个相同的
这时候模式串回溯到b的位置,下标为1,
也就是相同前后缀的个数作为下标进行回溯。
如此一来,KMP算法就大大降低了回溯的时间复杂度,因为i不再需要回溯了,
而模式串只需要根据前后缀进行回溯,也不用直接回溯到 j = 0.
(请务必带着我这段话去看下面解释,看完看继续回头看这一段!!)
①前后缀及其用处
前缀和后缀
前缀:不包括字符串尾字母
后缀:不包括字符串为首字母
有一串字符串↓
例: abcd
前缀:a ab abc
后缀:bcd cd d
## 这里有一个 混淆 点 ##:
我们KMP中的前后缀其实质是给模式串服务的,我看其他博客解释都有带上主串,我觉得太累赘了,其实你想想,当你模式串中某一部分匹配成功的时候,其实主串已经和你的模式串有一部分是一样的了,我们只需要知道前后缀给模式串服务就行,不需要附带上主串给自己的大脑增添没有必要的负担。
(这个混淆看不懂没关系,坚持住往后看就会恍然大悟)
接着再次拿上面的例子解释:
主串 i 回溯第一次
| a p a c k p a c k p a c z | ----------主串
| ^ p a c k p a c z | ------------------模式串
这时候可以看到模式串的前后缀有三个相同的字母,
还记得我让你记住的那一段话吗,有三个相同前后缀的字母应该要怎样?
答:应该是要把模式串下标 j 回溯到下标为3的位置继续和主串 i 的位置继续比较,这里回溯的第一次 ,i 是处于 k 的这个位置,所以当模式串回溯到 3 的位置 也就是 pack中 k 的这位置开始和主串继续匹配,很明显下面我们就能和主串匹配成功了
想象一下,如果我们用暴力求解, 在回溯第一次后,我们下一步肯定是把 i 回溯到第二位 i = 2 的位置,因为我们是从i = 1 的位置匹配的,这个位置开始匹配不行,暴力方法就很无脑的把 i = 2 从第三个位置 a 重新开始匹配, 然后模式串 j = 0。这样一来,如果是暴力方法的话,我们还需要4次才能真正的匹配成功,而刚刚KMP算法直接用一步到胃,省了的时间可不止一半了喔~
不知道你理解了没有,如果还没理解的话我真的没法子了。
下面开始介绍前后缀怎么求
②求出前后缀的next数组
还记得我们说过前后缀是为谁服务的吗?可以思考一下~
答案是
*
*
*
*
前后缀当然是为模式串服务的啦。
所以我们需要一个做一个函数,把模式串放进去,将一个int类型的next数组放进去
该数组是用来存放模式串对应位置前面的字符串的相同前后缀个数
( ↑ 请再读一遍这句话)
因此该数组每一个下标对应存放的信息也是和模式串下标每一个字符对应服务的
在这个函数中,我们只需要两个参数,一个数组,一个模式串
下面开始举例子理解next数组
a b c g a k
现在 k 对应的字符串下标为 5
所以next数组在对应着下标为5的空间下存放一个数 1,
因为现在这个字符所处的位置的前面的字符串只有一个相同的前后缀
再出一题昂,看看你做的对不对
ilovewzlilove y yds
问:第一个y所对应的next数组存放的数字应该是?
/
/
/
/
/
/
/
/
/
/
答案是:5
因为i love 是五个字母 (额…好土)
好的那么恭喜你成功理解了next数组是怎么来的了,
所以模式串中每一个字符你都应该可以推出他该字符所对应的next数组的数字了
但是别忘记了,前缀不包括最后一个字符,后缀不包括第一个字符
所以这也是一个难点,你记住,每一个算法都有他的边界,这也是算法难的原因,边界掌握到位了,你也能够出师了。
所以在next数组里面,next[0]所存的数字应该是-1,为什么呢,其实这个next[0] == -1 可以作为一个判断条件,待会你就知道为什么了。当一直处于匹配错误的时候,就拿他卡主模式串,当前后缀个数为0 的时候 j 就直接等于next[0],也就是j = -1,但是别怕,数组没有-1 的下标,但是还记得吗,刚刚才说了,这个next[0] 为 -1 可以作为一个判断条件 让主串不断前进,模式串回到0的下标位置匹配,别想太多,因为前后缀都没有一样的了,只能重新搞啦!懂我意思嘛!
求出next数组的代码
假设模式串是 :a a k f
这串代码我带你口述运行四遍,然后那你再看那个代码 (自己循环四遍就能看出规律)
首先模式串有两个哨兵 i, j
int i = 0, j = -1;
next[0] = -1;
// 这里的 i 是要完成他 ++ 的使命就OK, 接下去运行你就知道了。
if( j == -1 || temp[i] == temp[j] )
{
i++;
j++;
next[i] = j;
}
else
{
j = next[j];
}
运行第一遍 模式串是 :a a k f
进来先判断if
显然if在 j == -1 成立
所以进来后 i++ , j++(i=1,j = 0)
next[1] = 0
循环第二遍 模式串是 :a a k f
j 显然现在是0,所以判断第二个条件,
现在我们的模式串 0和 1下标字符相等,那么现在进入if语句,条件成立
所以继续 i++, j++, 现在 i = 2, j = 1
next[2] = 1;是不是很精髓,这语句就是先把下标自加了,
就是说第三个位置的前面有一个相等的前后缀,因为下标2就是第三个字符位置
所以先自加完后再给next数组就就很牛蛙,
循环第三遍 模式串是 :a a k f
现在帮你回忆下第二遍循环的变量情况:i = 2, j = 1
然后继续循环,回到if语句了,这惠很显然
temp[i]不等于temp[j]而且j 也不是-1,
因为第三个和第二个字符不一样,所以这次跳到else语句那里
j = next[j];//KMP算法精髓之处,现在next[j]是0因为先是j = 1,然后再被赋值为0
循环第四遍 模式串是 :a a k f
显然,这时候j 不是-1,然后继续匹配还是停留在i = 2, j = 1 的时候,显然这两个字符不相等,也没有进到if语句里面,
而是进去else语句里面,这时候j = 0, 所以next[j] = -1, j 被重新赋值为 -1,妙不妙就问你,前辈是多么的diao炸天,不仅控制了 i 不继续前进,而是先把 j 先回溯到原来的位置,然后这时候
第五遍的时候你应该自己可以推了,
这时候, j = = -1 了,这时候就可以进入 if语句了,重复上述操作,就先了解到这,如果你这能看懂,我觉得你可以继续推下去,更能理解深刻,我这只是帮你开开窍。
void Getnext(int *next, char *temp)
{
int i = 0, j = -1;
next[0] = -1;
while(i <= strlen(temp))
{
if(j == -1 || temp[i] == temp[j])
{
i++;
j++;
next[i] = j;
}
else
{
j = next[j];
}
}
}
开始实现KMP算法
铺垫这么多,下面就很容易了,因为KMP算法思想和求next数组思想差不多,甚至代码也长得很像,
记住一句话,前后缀有相同的时候,模式串的前缀不用匹配,因为主串中有与你的前缀相同的部分,直接移动到那一部分的位置就行,next数组就是为你的模式串干这事的。
需要注意的是
i < l_str && j < l_t
这个大于小于符号不能取等号,严格大于小于,没有等于
因为strlen计算长的的时候是直接给出字符串长度,但是我们的下标是从0开始的,
否则会造成下标越界,
(应该没有人特地去把strlen计算出来的长度减一然后条件就取等于吧,虽然这是可以,但是很傻)
还记得我让你记住的那一段话吗,你再回去读一遍理解一下,然后再看我这段代码,我就不信你妹有爱上那句话!!( 虚弱的说… )
↓ 下面的时间就留给你好好去欣赏一下前辈的算法艺术 ↓
void KMP(char *string, char *temp)
{
int next[10];
int i = 0, j = 0;
Getnext(next, temp);
//
// for(i = 0; i < strlen(temp); i++)
// {
// printf("%d\t",next[i]);
// }
// i = 0, j = 0;
int l_str = strlen(string), l_t = strlen(temp);
while(i < l_str && j < l_t)
{
if(j == -1 || string[i]==temp[j] )
{
i++;j++;
}
else
{
j = next[j];
//printf("**%d\t",j);
}
}
int c = strlen(temp);
//可能是编译器的问题,如果我不拿个c存放这个长度,直接反倒条件判断里面运行不出来
if( j >= c )
{
printf("\n%d位置:找到了。",i-strlen(temp) + 1);
return;
}
else
{
printf("\n没有找到匹配的部分。");
return;
}
}
结尾
虽然很垃圾的一篇博客,KMP算法确实难以理解,同样也难以解释
费时几天时间才把博客写出来
全部都是鄙人的习道,如果能帮到你,我将会很高兴,
如果没能把帮到你,恕我无能。
感谢你能看到这~
附上源代码(可以运行)
#include<stdio.h>
#include<string.h>
void Getnext(int *, char *);
void KMP(char *, char *);
int main()
{
char string[11] = "chinadaily";
char temp[11];
int next[10];
printf("\n请输入字符串进行匹配:");
scanf("%s", temp);
KMP(string, temp);
return 0;
}
void Getnext(int *next, char *temp)
{
int i = 0, j = -1;
next[0] = -1;
while(i <= strlen(temp))
{
if(j == -1 || temp[i] == temp[j])
{
i++;j++;
next[i] = j;
}
else
{
j = next[j];
}
}
}
void KMP(char *string, char *temp)
{
int next[10];
int i = 0, j = 0;
Getnext(next, temp);
//
// for(i = 0; i < strlen(temp); i++)
// {
// printf("%d\t",next[i]);
// }
// i = 0, j = 0;
int l_str = strlen(string), l_t = strlen(temp);
while(i < l_str && j < l_t)
{
if(j == -1 || string[i]==temp[j] )
{
i++;j++;
}
else
{
j = next[j];
//printf("**%d\t",j);
}
}
int c = strlen(temp);
//可能是编译器的问题,如果我不拿个c存放这个长度,直接反倒条件判断里面运行不出来
if( j >= c )
{
printf("\n%d位置:找到了。",i-strlen(temp) + 1);
return;
}
else
{
printf("\n没有找到匹配的部分。");
return;
}
}
C数据结构:KMP算法详解(呕心沥血)的更多相关文章
- 数据结构4.3_字符串模式匹配——KMP算法详解
next数组表示字符串前后缀匹配的最大长度.是KMP算法的精髓所在.可以起到决定模式字符串右移多少长度以达到跳跃式匹配的高效模式. 以下是对next数组的解释: 如何求next数组: 相关链接:按顺序 ...
- kmp算法详解
转自:http://blog.csdn.net/ddupd/article/details/19899263 KMP算法详解 KMP算法简介: KMP算法是一种高效的字符串匹配算法,关于字符串匹配最简 ...
- [转] KMP算法详解
转载自:http://www.matrix67.com/blog/archives/115 KMP算法详解 如果机房马上要关门了,或者你急着要和MM约会,请直接跳到第六个自然段. 我们这里说的K ...
- KMP算法详解(转自中学生OI写的。。ORZ!)
KMP算法详解 如果机房马上要关门了,或者你急着要和MM约会,请直接跳到第六个自然段. 我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法.KMP算法是拿来处理字符串匹配的.换句 ...
- 算法进阶面试题01——KMP算法详解、输出含两次原子串的最短串、判断T1是否包含T2子树、Manacher算法详解、使字符串成为最短回文串
1.KMP算法详解与应用 子序列:可以连续可以不连续. 子数组/串:要连续 暴力方法:逐个位置比对. KMP:让前面的,指导后面. 概念建设: d的最长前缀与最长后缀的匹配长度为3.(前缀不能到最后一 ...
- KMP算法详解&&P3375 【模板】KMP字符串匹配题解
KMP算法详解: KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt(雾)提出的. 对于字符串匹配问题(such as 问你在abababb中有多少个 ...
- 字符串匹配KMP算法详解
1. 引言 以前看过很多次KMP算法,一直觉得很有用,但都没有搞明白,一方面是网上很少有比较详细的通俗易懂的讲解,另一方面也怪自己没有沉下心来研究.最近在leetcode上又遇见字符串匹配的题目,以此 ...
- KMP算法详解-彻底清楚了(转载+部分原创)
引言 KMP算法指的是字符串模式匹配算法,问题是:在主串T中找到第一次出现完整子串P时的起始位置.该算法是三位大牛:D.E.Knuth.J.H.Morris和V.R.Pratt同时发现的,以其名字首字 ...
- KMP算法详解 --- 彻头彻尾理解KMP算法
前言 之前对kmp算法虽然了解它的原理,即求出P0···Pi的最大相同前后缀长度k. 但是问题在于如何求出这个最大前后缀长度呢? 我觉得网上很多帖子都说的不是很清楚,总感觉没有把那层纸戳破, 后来翻看 ...
- 算法-最通俗易懂的KMP算法详解
有些算法,适合从它产生的动机,如何设计与解决问题这样正向地去介绍.但KMP算法真的不适合这样去学.最好的办法是先搞清楚它所用的数据结构是什么,再搞清楚怎么用,最后为什么的问题就会有恍然大悟的感觉.我试 ...
随机推荐
- keycloak~在认证的action中自定义重定向地址
场景与实现逻辑 我的登录接口,在输入账号密码成功后进行中间页 中间页可以通过添加Authenticator的实现类来写逻辑 authenticate方法是渲染页面的,action方法是提交表单后的逻辑 ...
- #01背包#洛谷 2340 [USACO03FALL]Cow Exhibition G
题目 有\(n\)个物品,对于第\(i\)个物品, 有两种属性,第一种属性为\(x_i\),第二种属性为\(y_i\) 问选择若干个物品使得\(\sum{x_j}\geq 0\)且\(\sum{y_j ...
- #树状数组,离散#C 波动序列
分析 设\(dp[i][j][0/1/2/3]\)表示前\(i\)个位置当前选的数为\(j\), 且选择的是第一行/第二行/第三行不下降/第三行不上升, 状态转移方程显然,用线段树或者树状数组维护一下 ...
- OpenAtom OpenHarmony分论坛,今天14:00见!附大事记精彩发布
2022开放原子全球开源峰会 OpenAtom OpenHarmony分论坛 万物互联,使能千行百业 整装待发!精彩今日揭晓与您相约7月27日 14:00
- Java 日期和时间 API:实用技巧与示例 - 轻松处理日期和时间
Java 用户输入(Scanner) 简介 Scanner 类用于获取用户输入,它位于 java.util 包中. 使用 Scanner 类 要使用 Scanner 类,请执行以下步骤: 导入 jav ...
- 详解Java Chassis 3与Spring Cloud的互操作
本文分享自华为云社区<Java Chassis 3技术解密:与Spring Cloud的互操作>,作者: liubao68. Java Chassis 3一个很重要的设计原则:利用架构的韧 ...
- MogDB 操作系统优化指南
MogDB 操作系统优化指南 本文出处:https://www.modb.pro/db/413280 在性能调优过程中,可以根据实际业务情况修改关键操作系统(OS)配置参数,以提升 MogDB 数据库 ...
- 最后一站qsnctfwp
题目附件 图片一: 图片二: 根据图片一判断出位置为南昌市,地铁线路为4号线 根据题目名判断出搜索范围为白马山站或鱼尾洲站 通过百度地图全景地图查看两站环境,发现白马山站以工业区为主,鱼尾洲站以住宅区 ...
- 使用Elasticsearch做手机号和身份证号的模糊检索
使用Elasticsearch做手机号和身份证号的模糊检索 背景 客户想通过人名 四位数值 来检索人的信息 例如 张三 3421,例如需要检索包含张三和且手机号或者身份证里包含3421的数据 过程 e ...
- A7-100T的图像处理开发板
深圳市飞录科技有限公司 一:概述 开发板主控采用Xilinx Artix-7系列FPGA,型号为XC7A100T-2FGG676C,具有100K LUTs, 240个DSP, 芯片集成了LVDS.DD ...