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

01数据结构-串和KMP算法

01数据结构-串和KMP算法

  • 1.串的定义
    • 1.1定长顺序存储和变长分配存储
    • 1.2串的初始化
  • 2.串的匹配
    • 2.1暴力匹配
  • 3.KMP
    • 3.1next数组的计算
    • 3.2KMP算法代码实现

1.串的定义

串是由0个或多个字符组成的有序序列。串中字符的个数称为串的长度,含有0个元素的串叫空串。在C语言中,可以用如下语句定义一个名为str的串。

C语言定义了’\0’作为字符串的技术标志,但在描述串的长度时,需要通过扫描整个串才获得,时间复杂度为O(N),不如额外定义一个变量专门来存储串的长度,这样求串的长度的时间复杂度为O©的操作了。

不同的编程语言,是否用’\0’作为串的结束标语,是没有定论的,可以通过length来约束空间的长度也会更通用。例如C语言定义了一个“123”,它会认为’1’ ‘2’ ‘3’ ‘\0’,C++中的string如下图,用一个指针指向一片连续的空间,用长度来约束访问,长度为多长就代表我们的字符个数有多少个,且大部分情况我们不是从第0号开始存放,而是从1号开始存,0号可以存放一个标志位。
在这里插入图片描述

1.1定长顺序存储和变长分配存储

typedef struct{char str[maxsize+1];	// 从0号索引存数据,+1是为了存储\0int length;
}typedef struct{char *str;	// 动态分配内存;int length;
}

1.2串的初始化

与普通变量赋值操作不同,串的赋值操作不能直接用=来实现,通过定义初始化函数来实现空间拷贝。

初始化:int strAssign(StrType *str, const char *ch);

typedef struct {char *str;int length;
} StrType;
int strAssign(StrType *str,const char *ch);int strAssign(StrType *str, const char *ch) {if (str->str) {// str已经指向了数据,释放后重新再通过ch来赋值free(str->str);}// 求ch的长度int len=0;while (ch[len]) {++len;}if (len==0) {str->str=NULL;str->length=0;}else {str->str=malloc(sizeof(char)*(len+2));  // 把0号位置空出来,把\0也放入空间for (int i = 0; i <= len; ++i) {    // 包含了ch指向空间的\0位置str->str[i+1]=ch[i];}str->length=len;}return 1;
}

✅ (a) 避免空指针解引用(历史安全原因)
在C语言中,空指针NULL的地址是0。

如果字符串从索引0开始存储,那么str->str[0]可能访问地址0(空指针),在某些系统会触发错误。

从索引1开始:str->str永远不为NULL(即使字符串为空,也可能指向最小分配),str->str[0]可安全用作标志位,而str->str[1]是首字符。

✅ (b) 标志位存储元数据
索引0(str->str[0])可存放:

长度信息(尤其短字符串时,节省存储)

引用计数(写时复制COW,但现代少用)

分配类型标志(如区分堆分配/栈内分配)

2.串的匹配

字符串模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置。

例如下面长的s串我们称作为主串,下面的p串我们称为模式串。我们在s串中找p串的位置。
在这里插入图片描述

2.1暴力匹配

我们把s串中的每一个点都假设为我们p串中的起始点(蓝色箭头),两者同时往后遍历(红色箭头和橙色箭头),当遇到s[i]!=p[j]时s串中的起始点往后移动一位,例如:假设s中从1开始匹配,当箭头指向4即D的时候,p中的4是A,说明p串在s中的起始点不是1,因此从s再从2开始依次匹配(蓝色箭头往后移动一位),直到s中某一个点往后遍历7个位置相应的点和对应的p串中相应的点都相同我们认为找到了。
在这里插入图片描述
我们也可以不要蓝色的指针,当发现不匹配时,红色指针直接指向s串从遍历的起始位置的下一个索引。

暴力匹配法的核心思想是:逐个比较主串和模式串的每个字符,如果不匹配就回溯,直到找到匹配或遍历完整个主串。我们设p串中的字符个数是m个,s串中的字符个数是n个,显然采取这种方法的时间复杂度是O(mn)。

暴力匹配代码实现:

int index_simple(const StrType *str, const StrType *subStr) {int i = 1;int j = 1;int k = 1;  //记录假设值while (i<=str->length&&j<=subStr->length) {if (str->str[i]==subStr->str[j]) {++i;++j;}else {++k;j = 1;i = k;}}// 因为是从1开始存储的数据,最后若找到j会比subStr->length大1if (j>=subStr->length) {return k;}else {return 0;}
}void test01(const StrType *str,const StrType *subStr) {int res=index_simple(str,subStr);printf("simple find index:%d\n",res);
}int main() {StrType str;StrType subStr;str.str=NULL;subStr.str=NULL;strAssign(&str,"ABCDABCABCABABCABCDA");strAssign(&subStr,"ABCABCD");test01(&str,&subStr);return 0;
}

最后循环结束的时候如果j大于了subStr->length说明已找到,否则说明找不到,我们返回0。

结果:

D:\work\DataStruct\cmake-build-debug\08_KMP\kmp.exe
simple find index:13进程已结束,退出代码为 0

3.KMP

当子串和模式串不匹配时,主串指针不回溯,通过改变模式串指针的值,来确定子串从失配处和模式串的哪个位置进行比较,因为模式串前面的信息在前面比较时已经是已知信息了。

若能够存储子串失配后从模式串的哪个位置上进行比较,就可以实现KMP算法,故引入next数组,专门存放这个值。

显然,next数组里的值,只跟模式串有关,因为模式串前面已经成功匹配的字符,就表示子串中已经包含了这些字符。

在这里插入图片描述
如图当我们主串匹配到6时,认为6号索引中可以存除了d的任何值,此时可以发现1-5个元素和子串中1-5号元素是匹配的,又发现由于1-2号是a和b,4-5号也是a和b,由于KMP算法是中的主串的移动指针i是不会往回走的,我们可以认为4-5号就是子串中的1-2号,子串中的移动指针从3号开始匹配,如图所示:
在这里插入图片描述
引入两个概念,前缀和后缀。
如图假设不匹配的点在图中的c

前缀有:a ab aba abaa abaab
后缀有:c ac abc aabc baabc

next数组就是前缀和后缀最大的公共子序列的长度。
在这里插入图片描述

3.1next数组的计算

next数组定义:当主串与模式串的某一个字符不匹配时,模式串要回退的位置。

next[j]:第j位字符前面j-1位字符组成的子串的前后缀重合字符数+1(+1的原因是从失效点开始重新匹配,若不加1则是失效点的前一位)。

当j=1时,规定next[1]=0;(第一个字符都不匹配)
当j=2时,j前面的子串a,next[2]=1;

在这里插入图片描述
如图前两个我们认为是固定的,当失效点在3的时候,前缀和后缀没有公共子序列,所以模式串要回退到从1开始,next数组填1。

在这里插入图片描述
失效点在4的时候,前缀和后缀有一个公共子序列,所以模式串要回退到从2开始,next数组填2。

在这里插入图片描述
失效点在6的时候,前缀和后缀有两个公共子序列,所以模式串要回退到从3开始,next数组填3。

在这里插入图片描述
最终next数组如上图。

如果我们要在代码方面实现next数组,比如下图我们要求17号索引的值,假设16号索引next数组中的值是8,那么证明1-7号索引代表的字符和9-15号索引代表的字符是工共子序列,此时若16号索引代表的字符与8号索引代表的字符相同,那么说明索引为17的next数组中的值应填17。

在这里插入图片描述
若16号索引代表的字符与8号索引代表的字符不相同,我们看8号索引对应的值,假设为4,说明1-3号索引代表的字符和5-7号索引代表的字符是相同的,又由于之前1-7号索引代表的字符和9-15号代表的字符是相同的,说明9-11号代表的字符和13-15号代表的字符也是相同的,如果16号索引代表的字符与4号索引代表的字符相同,那么说明索引为17的next数组中的值应填5。(若不等就往前面推)

在这里插入图片描述

3.2KMP算法代码实现

KMP算法:int index_KMP(const StrType *str, const StrType *subStr, const int *next);

int index_KMP(const StrType *str, const StrType *subStr, const int *next) {int i=1;int j=1;while (i<=str->length&&j<=subStr->length) {if (j==0||str->str[i]==subStr->str[j]) {++i;++j;}else {j=next[j];}}if (j>=subStr->length) {return i-subStr->length;}else {return 0;}
}

由于i是不会往回退的,所以最终返回的是i-subStr->length

如果没有 j==0 这个条件,算法在某些情况下可能会卡住。具体来说,当 jnext[j] 重置为 0 时,如果没有 j==0 这个条件,那么在下一次循环中,j 仍然是 0,而 str->str[i]subStr->str[j] 的比较就会变成 str->str[i]subStr->str[0] 的比较。

如果 str->str[i] 不等于 subStr->str[0],那么 j 会一直保持为 0,导致算法无法继续前进,从而进入无限循环。

因此,j==0 这个条件的作用是确保在 j 被重置为 0 时,能够正确地将 ij 都向前推进一位,继续进行比较。这样可以避免算法卡在 j=0 的情况。

next数组的计算:void getNext(const StrType *subStr,int next[]);

void getNext(const StrType *subStr,int next[]) {int i=1,j=0;        // i: 主指针, j: 前缀指针next[1]=0;while (i<subStr->length) {if (j==0||subStr->str[i]==subStr->str[j]) {++i;++j;next[i]=j;   // 这里记录的是前缀和后缀相等的长度}else {j=next[j];}}
}

🔍 i 的角色:后缀末尾指针
职责:指向当前正在考察的后缀的最后一个字符

移动规律:每次循环至少向前移动一位

最终目标:遍历模式串的每个位置,计算对应的next值

物理意义:i 指向的是我们要计算 next[i] 的位置

🔍 j 的角色:前缀匹配指针
职责:指向当前匹配的前缀的最后一个字符

移动规律:可能前进也可能回溯

核心功能:记录当前已匹配的前缀长度

物理意义:j 的值就是当前的最长相等前后缀长度

回溯 j = next[j]:当匹配失败时,找到更短的相同前后缀继续尝试

最后来测一下:

#include"findStr.h"
#include<stdio.h>
#include<stdlib.h>void test02(const StrType *str,const StrType *pattern) {int *next=malloc(sizeof(int)*(pattern->length+1));getNext(pattern,next);printf("%d\n",index_KMP(str,pattern,next));free(next);
}int main() {StrType str;StrType subStr;str.str=NULL;subStr.str=NULL;strAssign(&str,"ABCDABCABCABABCABCDA");strAssign(&subStr,"ABCABCD");test02(&str,&subStr);return 0;
}

结果:

D:\work\DataStruct\cmake-build-debug\08_KMP\kmp.exe
13进程已结束,退出代码为 0

这是数据结构的最后一篇博客,后续还会更新计算机组成原理,计算机网络,操作系统等的课程。感谢大家观看。

http://www.dtcms.com/a/391247.html

相关文章:

  • 前端性能优化实用方案(三):骨架屏提升30%用户感知速度
  • NVR设备ONVIF接入平台EasyCVR视频融合平台智慧小区视频监控一站式建设方案
  • window XP环境下配置VC6.0的Win32汇编语言开发环境
  • 【算法】0_算法工程师常见算法题
  • 免费插件分享 | BaseTool
  • sk04.【scikit-learn基础】--『监督学习』之 线性回归
  • Ubuntu终端切换WiFi
  • eBest渠道商管理软件:快消品渠道数字化的“破局利器”
  • 基于protues的电子时钟仿真与模拟
  • FastProperty 高效的属性读写设置
  • 【开题答辩全过程】以 基于Java的失物招领平台为例,包含答辩的问题和答案
  • 【7/20】前后端整合:Vue.js 调用 Express API,操作 MongoDB 数据,实现用户管理系统
  • 【8/20】用户认证基础:JWT 在 Express 中的实现,实现安全登录
  • 独立站有哪些建站工具
  • Linux 终端常用快捷键整理
  • 跨域的两种解决方法
  • 小程序中获取年月日时分的组件
  • Redis热升级秘籍:零停机迁移方案与工具链
  • 时序数据库选型指南深度解析IoTDB架构设计与性能对比
  • springboot超市管理系统的设计与实现(代码+数据库+LW)
  • 让Trae写一个AI的api中继服务
  • 跨国制造业SD-WAN:延迟下降78%,运维成本下降53%
  • MySQL服务启动不成功的可能解决方法
  • 硬解码出现画面回退分析
  • P1068 [NOIP 2009 普及组] 分数线划定-普及-
  • 用python语言如何排大小
  • pycharm连接GitHub,怎么配置 SSH 密钥并改用 SSH 连接
  • ​​[硬件电路-265]:电源系统要考虑的因素包括:不同的输出电压、隔离防干扰、防反、防浪涌、电压可调、电源开关、电池、可充电、低纹波、低噪声、防波动等
  • 【开题答辩全过程】以 基于Python的电影推荐系统为例,包含答辩的问题和答案
  • 格拉姆角场(Gramian Angular Field, GAF)详解