当前位置: 首页 > news >正文

字符串查找 - 模拟实现strstr 、BF算法 、 KMP算法

文章目录

前言

一、模拟实现库函数strstr

二、BF算法

三、KMP算法

总结


前言

路漫漫其修远兮,吾将上下而求索。


一、模拟实现库函数strstr

Tips:此处采用利用指针+字符串末尾'\0' 的判断,当然你可以利用数组的下标;

库函数strstr 的原型:

char * strstr ( const char *str1, const char * str2);

strstr 的工作原理分析:在主串中寻找字串;可以在str1 中找到str2 ,便返回str1 中找到str2 位置的首字符的地址;如若找不到,便会返回NULL;

显然,想要一个字符串(str1)中找到另一个字符串(str2),首先得在主串(str1)中找到其(str2)首字符,然后再将其”身体“部分一一比对;如果到了str2 ”尾部‘都相同,就说明能在str1 中找到字符串str2 ; 而如若,在比对“身体”部分的时候,发现存在不同得地方(字符),就说明在str1 中找的这个头不对 (前提是,这个“头”在主串str1 的范围中),那便换一个头往后,再继续比对“身体"部分……

显然,查找是一个循环的过程;

在上述代码中,我们需要一个指针在str1 中找"头" ,并且需要两个指针分别在 str1 ,str2 中比对”身体“部分;且在存在”头”找错的情况,所以要将用来比对身体部分的指针进行”刷新“;

代码如下:

#include<stdio.h>
#include<assert.h>char* my_strstr(const char* str1, const char* str2)
{assert(str1 && str2);const char* p = str1;//“头”const char* s1 = str1;//工具指针const char* s2 = str2;//工具指针//在str1 的范围内while (*p){//刷新工具指针s1 = p;s2 = str2;//当头相同时,比较“身体”;当然得在合理范围内while ((*s1 == *s2) && *s1 && *s2){s1++;s2++;//身体相同时,当到str2 到达尾部,说明能在str1 中找到str2if (*s2 == '\0'){return (char*)p;}}//出了“身体”相等的循环,说明身体不相等-->换“头”p++;}//等到了这里还没有返回值,说明在str1 中找不到str2 return NULL;
}
int main()
{char ch1[] = "accbcbbbcccdbbb";char ch2[] = "ccd";char* tmp = my_strstr(ch1, ch2);printf("%s\n", tmp);return 0;
}

代码运行情况如下:

二、BF算法

其实上面模拟实现库函数strstr 中就很“暴力”,BF算法也是如此,跟模拟实现strstr 的逻辑一样;但是有两种思考逻辑;

一是,利用指针+'\0' ;二是,利用数组的下标和字符串长度;

为了更好衔接KMP算法,此处利用数组的下标来实现BF算法;

分析:跟模拟实现strstr 的思想很像,只不过是将专门指向“头”的指针的功能让工具指针也具备罢了;

图解如下:

代码如下:(模拟strstr 的味道极重)


//BF算法
#include<stdio.h>
#include<assert.h>
#include<string.h>int BF(const char* str, const char* sub)
{assert(str && sub);//因为要对str1 srt2 进行解引用操作,所以要避免str1 str2 为空指针int len_str = strlen(str);int len_sub = strlen(sub);int i = 0;//负责str 的下标int j = 0;//负责 sub 的下标//在查找的范围内while (i < len_str && j < len_sub){j = 0;//每一次的刷新while (str[i] == sub[j] && i <len_str && j< len_sub){i++;j++;}if (j >= len_sub){return 1;}i = i - j + 1;}//出了循环就说明没有找到return 0;
}int main()
{char ch1[] = "accbcbbbcccdbbb";char ch2[] = "ccd";int ret = BF(ch1, ch2);if (ret)printf("能找到\n");elseprintf("找不到\m");return 0;
}

代码输出结果如下:

改进的BF代码:

如下:

//BF算法
#include<stdio.h>
#include<assert.h>
#include<string.h>int BF(const char* str, const char* sub)
{assert(str && sub);int len_str = strlen(str);int len_sub = strlen(sub);int i = 0;//负责str 的下标int j = 0;//负责 sub 的下标//在查找的范围内while (i < len_str && j < len_sub){if (str[i] == sub[j]){i++;j++;}else{//刷新//非常巧妙,当j = 0,而 str[i] != sub [j]--> i = i +1;相当于i 自增了;i = i - j + 1;j = 0;}}if (j >= len_sub){return 1;}return 0;
}int main()
{char ch1[] = "accbcbbbcccdbbb";char ch2[] = "ccd";int ret = BF(ch1, ch2);if (ret)printf("能找到\n");elseprintf("找不到\n");return 0;
}

代码运行结果如下:

三、KMP算法

在BF算法中,需要遍历str中的所有字符来找sub 中的首字符;这不太高效,显然KMP就是优化BF算法的一种算法;

KMP算法是一种字符串匹配算法,由D.E.Knuth.J.H.Morris和V.R.Pratt提出,因此被称为克努特-莫里斯-普拉特操作(简称KMP算法)。该算法的核心是利用匹配失败后的信息,尽量减少使用子串与主串的匹配次数,即在匹配成功得串中找与字串其前段字符匹配的串,以达到快速匹配的目的;具体实现是通过一个数组:next 数组,每次匹配失败后j 回退的下标的信息在next 数组中(此next 数组中会包含字串的局部匹配信息);想要得到数组需要通过写函数计算得到;

KMP算法的实现涉及到前缀和后缀相等的部分的最大长度的计算,以及如何利用这个信息来优化匹配过程。这个过程虽然看起来复杂,但通过逐步的学习和理解,可以逐渐掌握其精髓。

KMP算法的时间复杂度 为O(m+n)

KMP与BF 的不同所在:

1、BF算法中, i 会返回到上一次匹配开始的下一个坐标( i = i - j +1);而在KMP算法中,i 始终不会回退,只会前进

2、在BF算法中,j 在每一次匹配失败后会返回到其起始位置即下标为0 的位置上;但是在KMP算法中,j 返回到下标为几 的位置上(j 会返回到特殊位置上),next 数组说了算,即存在 next[ j ] = k ;

在上图中,你可以发现,str [ i ]  前面的 abca 与 sub [ j ] 前面的 abca 是相同的而在字串中,前面两个ab 等于后面两个ab ,所以可以得到主串中靠近 str[i] 的ab 与字串中第一个ab 相同;

见下图,主串中的 i 不变,而只需要比较str [ i ] 是否与sub[ j ](此 j 是移动之后的j ) 相等 ;如果相等,则 i 与 j 便继续向前(右)比对;如若不相等,则说明此 i 前面的部门字符串可以舍弃,而 j 便会回到 下标为0 的位置上;

相当于,主串中的 i 的位置不变,但是得在这部分字符(从i 与sub 比对得开头到有差异这段)中找找看有没有与字串相同字串的部分字符段(限制:一个必须为下标为0 的位置开始,另一个必须以 j-1 的下标结尾),有的话,j 便移动到特定位置上;没有的话,j 便回归到下标为0 的位置上;通俗的讲就是,在比较这个比对过程中其最左边的字符段有没有和最右边得字符串相同的 (本质上也是在找合适可以匹配的开头,不过相较于暴力的BF一个一个地找,KMP就更加灵活,为字串中每一个可能不会匹配成功地字符对应了相应地返回坐标);

在下图中,j 回退到了下标为2  的位置上,因为在字串中,找到以两个相等的真字串,其中一个以下标为0 开始,另外一个以下标为 j-1 结束;而此真字串的长度放在字串的开头,就可以被看作已经被比对过,自然 j = 2 ;此时:

之前我们说过,在KMP 算法中,j 回退到的位置由next 数组决定的;在以上描述中,我们视乎可以看到一些计算next 数组中元素的苗头;即,在已经匹配的字符串中找两个真字串,其中一个字串必须以下标为0的字符开头,另外一个字串必须以 j-1 对应位上的字符结尾(对位置的强制要求);若存在这两个真字串(不包含本身),那么j 回退的位置就为此真字串的长度,这样说的原因是因为下标为0的字段的后面的坐标就是此字段的长度;

再次回顾一下,next 数组是用来保存字串中某个位置匹配失败后所要回退的位置(下标);即存在next [ j ] = k; j 为字串当前匹配失败的位置,k 为 j 匹配失败后所要回退的下标;

接下来我们来计算一下以上示例中其字串的 next 数组是如何计算得到的 

规则:

1、找到匹配成功部分的两个相等的真字串(不包含其本身),一个以下标为0的字符作为开头,另一个以下标为 j - 1 的字符作为结尾

2、不管是什么数据 next [0] = -1; next[1] = 0; (在不同的资料中next数组中的第一个元素和第二个元素为1 和 0 , 其实影响也不是很大);

过程如下图所示:

原理是如此得到的,那么推广到代码让计算机通过代码根据不同的字串而得到对应的next 数组呢?

其实很简单,如上图;

假设我们知道当前下标为j 时对应下标数组中next 中的元素,而去求 next[j+1];next[j] = k; k 的意思是当子串在 j 下标时与主串匹配失败,字串便回退到下标为 k 的位置上去;而字串回退到该下标的原理是,在字串与主串匹配成功的部分字段中找此段(匹配成功的部分字)结尾部分有没有和前部分字符段相同的部分,已达到 i 不变避免在主串中错过合适的开头;如果找得到,j 便回退到找到找到的串的后面下标处;如果找不到,j 会继续回退,直到 j  回退为0,则说明在此匹配成功的字符段中没有与子串开头字符相匹配的字段,所以可以舍弃,i 在主串不动的位置开始重新与子串进行匹配,j即 回退到下标为0 的位置上进行“刷新”;

代码如下:

#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>void GetNext(const char* sub, int* next)
{//已经拍过空int lensub = strlen(sub);//规定next[0] = -1;next[1] = 0;int j = 2;//从下标为2 开始计算int k = 0; //因为 next[j-1]=0; 所以 k= 0,k 代表的是 j 前一个下标对应的next数组的值while (j < lensub)//数组next 所需要计算的元素个数{if ((sub[j - 1] == sub[k])||(k==-1)){next[j] = k + 1;j++;//调整k++;}else{//不相等便继续回退k = next[k];//但是如果 next[k]==-1;就代表着k 要回到下标为0 的位置上;}}
}int KMP(const char* str, const char* sub, int pos)
{assert(str && sub);//因为要对str sub 进行解引用操作,就要保证str sub 不为空指针int i = pos;//在主串下标为 pos 的位置开始查找子串int j = 0;int lenstr = strlen(str);int lensub = strlen(sub);if (lenstr == 0 || lensub == 0)return -1;if (pos < 0 || pos >= lenstr)return -1;int* next = (int*)malloc(lensub * sizeof(int));//next 数组在内存中开辟的空间assert(next);//排空//调用函数计算next 数组中元素的值GetNext(sub, next);//next数组是根据子串来计算得到的//得到next 数组,便可以实现查找while (i < lenstr && j < lensub)//在合理的范围之内{if ((str[i] == sub[j])|| (j==-1)){i++;//调整j++;}else//不相同的话,i 只会前进而不会回退,j 会根据 next 数组而回退到一定下标的位置上{j = next[j];//next 数组下标为0 的元素为-1,其本质是想让j 回退到下标为0的位置上;}}//记得释放空间free(next);//出了此范围有两种情况,一是 i >= lenstr ;二是 j >= lensub ;if(j >= lensub){return i - j;}//否则就是没有找到return -1;
}int main()
{//KMP算法char ch1[] = "abbcdabccdabcdabcaddd";char ch2[] = "abca";int ret = KMP(ch1, ch2, 0);if (ret == -1)printf("没有找到\n");elseprintf("在主串下标为%d处找到了子串\n", ret);return 0;
}

代码运行结果如下:


总结

BF算法其实和模拟实现strstr 的逻辑很像, 或者说就是一样的感觉~

但是KMP 算法中 在主串负责管理下标 的 i 只会前进而不会回退; 而 j会根据next 数组中的对应的值而回退到特定位置上,即存在 next[j]= k ; 

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【AI】算力底座的巨变
  • golang中的星号*通配符字符串模式匹配 和问号? 通配符字符串模式匹配的2种实现方法 和相关的单元测试用例
  • SQL Zoo 6.The JOIN operation
  • 【c++】类和对象 (中) (类的默认成员函数)
  • Springboot 实现 Modbus Rtu 协议接入物联网设备
  • matlab实现红绿灯识别
  • MySQL事务隔离级别、InnoDB使用MVCC+各种锁实现了RC和RR事务隔离级别、具体案例
  • cpio 命令
  • element-ui周选择器,如何获取年、周、起止日期?
  • C# Type 对象序列化与反序列化
  • 合并两个有序数组(LeetCode)
  • oracle创建dblink使得数据库A能够访问数据库B表LMEAS_MFG_FM的数据
  • sql获取过去的小时数
  • vue请求springboot接口下载zip文件
  • 【书生大模型实战营第三期 | 入门岛第3关-Git 基础知识】
  • 【comparator, comparable】小总结
  • Apache Pulsar 2.1 重磅发布
  • Js实现点击查看全文(类似今日头条、知乎日报效果)
  • JS题目及答案整理
  • Node 版本管理
  • npx命令介绍
  • Promise面试题2实现异步串行执行
  • vue+element后台管理系统,从后端获取路由表,并正常渲染
  • XML已死 ?
  • 从零搭建Koa2 Server
  • 基于OpenResty的Lua Web框架lor0.0.2预览版发布
  • 记一次删除Git记录中的大文件的过程
  • 七牛云 DV OV EV SSL 证书上线,限时折扣低至 6.75 折!
  • 七牛云假注销小指南
  • 前端面试之闭包
  • 浅谈web中前端模板引擎的使用
  • 掌握面试——弹出框的实现(一道题中包含布局/js设计模式)
  • - 转 Ext2.0 form使用实例
  • 通过调用文摘列表API获取文摘
  • 组复制官方翻译九、Group Replication Technical Details
  • ​渐进式Web应用PWA的未来
  • # 睡眠3秒_床上这样睡觉的人,睡眠质量多半不好
  • ### RabbitMQ五种工作模式:
  • #include到底该写在哪
  • #WEB前端(HTML属性)
  • #数学建模# 线性规划问题的Matlab求解
  • $nextTick的使用场景介绍
  • (04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
  • (09)Hive——CTE 公共表达式
  • (C语言)输入自定义个数的整数,打印出最大值和最小值
  • (day6) 319. 灯泡开关
  • (k8s)Kubernetes本地存储接入
  • (Qt) 默认QtWidget应用包含什么?
  • (附源码)springboot太原学院贫困生申请管理系统 毕业设计 101517
  • (附源码)计算机毕业设计SSM疫情居家隔离服务系统
  • (蓝桥杯每日一题)love
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战
  • (推荐)叮当——中文语音对话机器人
  • (转) Face-Resources