前言

KMP算法可以说说许多学习算法的同学的第一道坎,要么是领会不到KMP算法的思想,要么是知道思想写不出代码,网上各种查找。关于算法的书籍上也都有KMP算法的实现,可为啥自己写不出来呢?博主看得大话数据结构上的分析,书上的代码都比较精简,但是不易理解 ,跟着代码思路走结果也是对的。那么我们为啥我们不可以多写几行代码 更加容易理解呢。博主今天就用普通程序员的思路 去写KMP算法 采用C语言实现,虽然代码可能会多那么几行,如果你能看懂,那我也就很高兴了,如果看不懂 请看大话数据结构中KMP的实现领略其思想 然后自己实现代码没有必要和书上一模一样博主写的KMP算法是结合之前写的字符串:BF算法。程序代码可以循环运行进行测试

KMP算法介绍

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。

KMP算法思路

举个简单先说明KMP思想

主串abcdefgab 子串abcdex,我们采用BF算法 当比较到字符x和d时候发现相等,这是主串回溯到字符b 又和 子串abcdex进行比较 然后c...e 和子串abcdex进行比较。我们明显知道,主串的abcde已经和子串的abcde相等了,而且abcde字符之间互不相等!那么主串中b c 有必要 再次和子串abcd进行比较么,没有必要,因为 主串abcde和子串abcde匹配 而且他们 之间又是互不相等 所以没有 b...e和子串进行比较了。只需要步骤①和步骤⑥ 请看下图(大话数据结构上的图)

如果在不相等的字符前面有重复的字符串的情况怎样呢?主串abcabcabc和子串abcabx 是不是应该和下图一样呢?仔细想一想是不是呢,领悟...,子串回溯到不相等字符前 的 重复字符串的后面。

所以我们回溯的重点是子串而不是主串,尤其是当我们子串有大量不重复字符且长度越长,节省的比较次数越多

KMP算法和BF算法和核心区别就是遇到不相等的字符串 主串和子串的回溯问题,BF算法遇到不相等字符主串回溯到之前开头比较的第一位字符的下一位 子串回溯到第一位 会进行大量没有必要的比较,而KMP算法会根据子串中字符情况进行比较。

next 数组推导

下面我们就进行子串中每个字符回溯位置的推导,将子串每个字符回溯的位置放在一个next数组中,字符串格式采用书上推荐的格式sub_str[0]存放子串长度,sub_str[1]开始放字符,那么我们的next数组同样也是从[1]开始放字符对应的回溯位置。比如说我们匹配的子串是ababaaaba,每个字符的next值就是他前缀表达式和后缀表达式相等元素个数+1。

子串下标123456789 123456789

子串ababaaaba aaaaaaaab

next值     011234223 012345678

注意 next[1] = 0 next[2] = 1 这是不变的然后从第3位字符开始,我们就要进行前缀后缀字符重复的计算了,重复1位next[]值是2重复2位next[]值是3 依次类推,当比较到不等字符时 最后一位和第一位重新比较如果还是不等那next值就是1,否则就是2。总感觉描述代码实现不清楚,下面还是看代码吧。

/*
获取子串的next数组
没有优化的的next数组
*/
int* NextKMP1(uchar* sub_str)
{int subLen = sub_str[0];int* next = (int*)malloc(sizeof(int)*(subLen + 1));next[1] = 0;//特殊情况 子串只有一个字符if (subLen == 1){return next;}next[2] = 1;int start = 1;int end = 2;int count = 1;for (size_t i = 3; i <subLen + 1; i++){//i 当前字符的下标,计算它的next值if (sub_str[start] == sub_str[end]){next[i] = ++count;start++;end++;}else{//遇到不相等,那就只能从头开始比较咯,start = 1;count = 1;if (sub_str[start] == sub_str[end]){next[i] = ++count;//相等前缀往后走start++;}else{next[i] = 1;//不相等 start停留在第一个位置}//后缀一直往后走end++;}}return next;
}

next 数组优化

后来前辈们发现KMP还是有缺陷的,比如我们的主串aaaabcde和子串aaaaax,按照KMP算法next值分别为012345,按照KMP算法比较如下:

其中二、三、四、五是多余的判断,因为其位置上的字符都和首字符'a'相等,那么可以用首位next[1]的值进行取代当前next[]的值。下面请看代码,就加了2行语句。

/*
获取子串的next数组
优化后的next数组
*/
int* NextKMP2(uchar* sub_str)
{int subLen = sub_str[0];int* next = (int*)malloc(sizeof(int)*(subLen + 1));next[1] = 0;//特殊情况 子串只有一个字符if (subLen == 1){return next;}next[2] = 1;int start = 1;int end = 2;int count = 1;for (size_t i = 3; i <subLen +1; i++){//i 当前字符的下标,计算它的next值if (sub_str[start] == sub_str[end]){next[i] = ++count;start++;end++;}else{//遇到不相等,那就只能从头开始比较咯,start = 1;count = 1;if (sub_str[start] == sub_str[end]){next[i] = ++count;//相等前缀往后走start++;}else{next[i] = 1;//不相等 start停留在第一个位置}//后缀一直往后走end++;}//优化 如果start指向的字符和当前字符相等,那么就取前缀相同字符的next值if (sub_str[start] == sub_str[end]){next[i] = next[start];}}return next;
}

完整代码

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef unsigned char uchar;
/*
获取子串的next数组
优化后的next数组
*/
int* NextKMP2(uchar* sub_str)
{int subLen = sub_str[0];int* next = (int*)malloc(sizeof(int)*(subLen + 1));next[1] = 0;//特殊情况 子串只有一个字符if (subLen == 1){return next;}next[2] = 1;int start = 1;int end = 2;int count = 1;for (size_t i = 3; i <subLen +1; i++){//i 当前字符的下标,计算它的next值if (sub_str[start] == sub_str[end]){next[i] = ++count;start++;end++;}else{//遇到不相等,那就只能从头开始比较咯,start = 1;count = 1;if (sub_str[start] == sub_str[end]){next[i] = ++count;//相等前缀往后走start++;}else{next[i] = 1;//不相等 start停留在第一个位置}//后缀一直往后走end++;}//优化 如果start指向的字符和当前字符相等,那么就取前缀相同字符的next值if (sub_str[start] == sub_str[end]){next[i] = next[start];}}return next;
}/*
获取子串的next数组
没有优化的的next数组
*/
int* NextKMP1(uchar* sub_str)
{int subLen = sub_str[0];int* next = (int*)malloc(sizeof(int)*(subLen + 1));next[1] = 0;//特殊情况 子串只有一个字符if (subLen == 1){return next;}next[2] = 1;int start = 1;int end = 2;int count = 1;for (size_t i = 3; i <subLen + 1; i++){//i 当前字符的下标,计算它的next值if (sub_str[start] == sub_str[end]){next[i] = ++count;start++;end++;}else{//遇到不相等,那就只能从头开始比较咯,start = 1;count = 1;if (sub_str[start] == sub_str[end]){next[i] = ++count;//相等前缀往后走start++;}else{next[i] = 1;//不相等 start停留在第一个位置}//后缀一直往后走end++;}}return next;
}/*
用KMP算法查询子串在主串中的位置
dest_str:目标字符串
sub_str:子串
next:子串的next数组
begin:开始查询的位置
return 返回子串在主串中的index
*/
int IndexKMP(uchar* dest_str,uchar* sub_str,int* next,int begin)
{int subLen = sub_str[0];uchar* dest = dest_str + 1 + begin;uchar* sub = sub_str + 1;int count = 0;//和暴风算法一样while (*dest != 0){count++;//判断第一个字符是否相等,不相等 主串往后移if (*dest != *sub){dest++;continue;}//碰到相等字符,记录比较起始位置uchar* temp = dest;//走到这里主串和子串第一个字符相等,继续往下进行比较sub++;dest++;while (*sub !=0){ count++;//相等继续比较后面的字符if (*dest == *sub){dest++;sub++;}else{//遇到不相等的字符 就回溯//BF算法就是从头再来了,主串回溯到标记的下一位继续和子串的第一位开始进行比较//dest = temp + 1;//sub = sub_str + 1;//KMP 算法,就是比较字符不等时,next[]有值,主串不进行回溯,把子串进行回溯!这是KMP的核心思想。if (next[sub - sub_str] == 0)//next[]值为0主串比较下一位{dest = temp + 1;sub = sub_str + 1;}else{//sub = sub_str + 1 + next[sub - (sub_str + 1) + 1] - 1;sub = sub_str + next[sub - sub_str];//别写错了哟,这里是关键。}break;}}//子串遍历完毕,说明子串在主串中匹配完毕if (*sub == 0){printf("KMP算法字符比较的次数:%d\n", count);return dest - (dest_str + 1) - subLen;}}printf("没有找到\n");return -1;
}
/*
将普通字符串转换为KMP需要的字符串格式
char* src = "abc";--> char* dest = {3,'a','b','c','\0'};
*/
uchar* StrConvert(char* src)
{int len = strlen(src);if (NULL == src || len == 0 ){return NULL;}int newLen = len + 2;//\0 占一个位置,字符数量占一个位置uchar* str = malloc(sizeof(char)*newLen);memset(str, 0, newLen);str[0] = len;//为了是主串可以更长使用unsigned char ,所以主串最长不要超过255个字符strncpy(str + 1, src, len);return str;
}
/*
用BF算法查询子串在主串中的位置
dest:目标字符串
sub:查询子串
begin:开始查找的下标
return 返回子串在主串中的index
*/
int StrIndexBF(char* dest_str, char* sub_str, int begin)
{if (begin < 0){begin = 0;}char* dest = dest_str + begin;char* sub = sub_str;int count = 0;//记录比较次数//通过字符一个一个进行比较while (*dest != 0){count++;//和子串第一个字符不相等if (*dest != *sub){dest++;continue;}char* temp = dest;//走到这里主串和子串第一个字符相等,继续往下进行比较sub++;dest++;//遇到不相等的字符就回溯,主串回溯到标记的下一位继续和子串的第一位开始进行比较while (*sub != 0){count++;if (*sub == *dest){sub++;dest++;}else{sub = sub_str;dest = temp + 1;break;}}//判断子串是否遍历完毕,返回位置if (*sub == 0){printf("BF算法字符比较的次数:%d\n", count);return dest - dest_str - strlen(sub_str);}}printf("没有找到\n");return -1;
}/*
打印next数组的数据
*/
PrintNext(int* arr,int length)
{//next[0]为空闲空间for (size_t i = 1; i < length; i++){printf("%d  ", arr[i]);}printf("\n");return 0;
}/*
KMP算法查找子串
dest 目标字符串
sub 查询子串
begin 开始查找的下标
*/
int StrIndexKMP(char* dest,char* sub,int begin)
{if (NULL == dest || NULL == sub || begin < 0){printf("传入参数有误...\n");return -1;}uchar* dest_ = StrConvert(dest);uchar* sub_ = StrConvert(sub);int* nextArr1 = NextKMP1(sub_);int* nextArr2 = NextKMP2(sub_);printf("KMP的 next   :");PrintNext(nextArr1, sub_[0]+1);printf("KMP的 nextval:");PrintNext(nextArr2, sub_[0] + 1);return IndexKMP(dest_,sub_, nextArr2,begin);
}int main(int argc, char *argv[])
{char dest[256] = { 0 }, sub[256] = { 0 }, num[5] = { 0 };int begin = 0;while (1){memset(dest, 0, 256);memset(sub, 0, 256);memset(num, 0, 5);printf("请输入目标字符串(#退出):");fgets(dest, 256, stdin);dest[strlen(dest) - 1] = 0;//去掉换行符if (strcmp(dest, "#") == 0){break;}printf("请输入查询起始位置(不输入从0开始):");fgets(num, 5, stdin);if (strlen(num) != 1){num[strlen(num) - 1] = 0;//去掉换行符sscanf(num, "%d", &begin);}printf("请输入查询子串:");fgets(sub, 256, stdin);sub[strlen(sub) - 1] = 0;//去掉换行符int index = StrIndexBF(dest, sub, begin);printf("BF:dest=%s,sub=%s,begin=%d,index=%d\n", dest, sub, begin, index);index = StrIndexKMP(dest, sub, begin);printf("KMP:dest=%s,sub=%s,begin=%d,index=%d\n", dest, sub, begin, index);}return 0;
}

运行结果检测

我们主要看next数组值和优化后的nextval数组值是否推导正确,这是KMP算法的关键,如果你能写BF算法然后能将next数组用代码推算出来,那么你的KMP算法就ok了,然后可能就是一些细节的上的完善了。

字符串:你看的懂的KMP算法(带验证)相关推荐

  1. No.5终于搞懂了kmp算法(精髓为next数组的求解过程,此文next数组未经过优化)

    背景 KMP 是经典的字符串匹配算法,在大学课本里面都是讲到过的,不过我感觉课本都讲的太生硬了,很容易忘记.大多数博客也是讲的云里雾里的,一开始就来个公式+部分匹配表,反正我感觉对于没有计算机基础的童 ...

  2. 数据结构之字符串匹配算法(BF算法和KMP算法)

    字符串匹配算法: 就是给定两个串,主串(s)和子串(sub), 查找子串是否在主串里面,如果找到便返回子串在主串中第一个元素的位置下标,否贼返回-1,. 在这里我 们讨论的时候主要用字符串来举例实现. ...

  3. 【算法篇-字符串匹配算法】BF算法和KMP算法

    目录 前言 1. BF算法 1.1 画图分析 1.3 BF 算法的时间复杂度 2. KMP 算法 2.1 KMP 算法和 BF 算法 的区别 2.1.1 为什么主串不回退? 2. 2 next 数组 ...

  4. 【附源码】一看就懂的感知机算法PLA

    AI有道 一个有情怀的公众号 本文所有的源代码均放在了我的GitHub上,需要的点击文末「阅读原文」获取.如果对你有用的话,别忘了Fork和Star哦! 什么是感知机「Perceptron」 PLA全 ...

  5. 你也能看得懂的python算法书pdf_你也能看得懂的Python算法书最新章节_王硕著_掌阅小说网...

    1.2 三大结构 Python语言中有三大结构:循序.分支和循环.这三种结构分别适用于不同的情况,一个复杂的程序中常常同时包含这三种结构. 1.2.1 循序结构 说到"循序"这个词 ...

  6. 一看就懂的感知机算法PLA

    个人网站:红色石头的机器学习之路 CSDN博客:红色石头的专栏 知乎:红色石头 微博:RedstoneWill的微博 GitHub:RedstoneWill的GitHub 微信公众号:AI有道(ID: ...

  7. 《你也能看得懂的Python算法书》学习笔记(四)

    在学习完哈希算法之后,我们开始学习深度优先遍历算法.深度优先遍历算法是经典的图论算法,从某个节点v出发开始搜索,不断搜索直到该节点的所有边都被遍历完.当节点v的所有边都被遍历以后,深度优先遍历算法需要 ...

  8. 【一看就懂的图解算法】简单选择排序

    简单选择排序 冒泡排序是将最大的元素往后面排,简单选择排序是将小的元素往前面排 算法思想: 1.将第一个元素和其余元素进行对比,如果第一个元素和其他元素相比,第一个元素大,则交换,一轮下来,最小的元素 ...

  9. 字符串模式匹配——最长公共子序列与子串 KMP 算法

    最长公共子序列 最长公共子序列的问题很简单,就是在两个字符串中找到最长的子序列,这里明确两个含义: 子串:表示连续的一串字符 . 子序列:表示不连续的一串字符. 所以这里要查找的是不连续的最长子序列, ...

最新文章

  1. ubuntu ssh 客户端查看服务器端图形界面
  2. nginx + gunicorn + django的简单部署
  3. java用重载实现获取元素的数据类型
  4. mysql远程服务器访问数据库
  5. 进程上下文与中断上下文
  6. vue路由守卫判断用户是否登录,如果没登陆就跳转到登录
  7. linux下qt生成可安装的程序,linux – 如何为Qt应用程序创建“安装”包?
  8. IDEA编译通过能运行但是出现红色下划线
  9. Python第十六课(模块3)
  10. 锐起无盘服务器优化,(锐起无盘系统制作系统优化教程.doc
  11. php仿大众点评,Android高仿大众点评(带服务端)
  12. Vmware15安装win7专业版以及vmtools
  13. 树莓派+USB免驱摄像头远程监控
  14. Java ques:Client does not support authentication protocol requested by server;Access denied user‘roo
  15. 水星如何设置虚拟机服务器,水星mercury路由器电脑怎么设置?
  16. 中国高校计算机大赛英语cccc,通知-CCCC中国高校计算机大赛.PDF
  17. 使用python爬取新浪微博的内容
  18. 没有寻线仪怎么找网线_乱七八糟的网线怎么找?寻线仪来帮你
  19. 【Python 常用英文单词】——总结Python常用的英文单词 最全版
  20. Vivo(IQOO)无法输出调试日志解决办法

热门文章

  1. 《数学与生活》的3本书籍
  2. php我赢职场季枫_我赢职场 - 主页
  3. 博图能打开s7200吗_域名掉备案了,还能打开吗?域名掉备案了怎么办?
  4. Linux基础学习八:mysql主从复制原理以及详细搭建步骤
  5. [1] SDK Tools安装
  6. PHPJavaScript笔记-后端利用Refresh头带错误信息给前端(野路子操作)
  7. C++设计模式-外观模式
  8. Qt工作笔记-自定义模型【继承QAbstractTableModel】
  9. 5.3稀疏矩阵的十字链表存储
  10. java 8 java demo_Java 8 中的 Streams API Demo