数据结构_串

news2025/4/22 11:41:36

目录

1. 串的定义和实现

1.1 串的定义

1.2 串的存储结构

1.2.1 定长顺序存储表示

1.2.2 堆分配存储表示

1.2.3 块链存储表示

1.3 串的基本操作

2. 串的模式匹配

2.1 简单的模式匹配算法

2.2 串的模式匹配算法——KMP算法

2.2.1 字符串的前缀、后缀和部分匹配值

2.2.2 KMP算法的原理是什么?

2.2.3 KMP算法的进一步优化

2.3 相关练习


1. 串的定义和实现

        字符串简称串,计算机上非数值处理的对象基本都是字符串数据。我们常见的信息检索系统(搜索引擎)、文本编辑程序(Word)、问答系统、自然语言编译系统等,都是以字符串数据作为处理对象的。

1.1 串的定义

        串(String)是由零个或多个字符组成的有限序列。一般记为

S="a1a2a3a4……an"

其中,S是串名,单引号括起来的字符序列是串的值;a_{i} 可以是字母、数字或其他字符;串中字符的个数 n 称为串的 长度。n =0 时的串 称为空串

        中任意多个连续的字符组成的子序列称为该串的子串,包含子串的串称为主串某个字符在串中的序号称为该字符在串中的位置子串在主串中的位置以子串的第一个字符在主串中的位置来表示。当两个串的长度相等且每个对应位置的字符都相等时,称这两个串是相等的

例如,有串A='China Beijing',B='Beijing',C='China',则它们的长度分别为13,7和5。B和C是A的子串,B在A中的位置是7,C在A中的位置是1。

        需要注意的是,由一个或多个空格(空格是特殊字符)组成的串称为空格串(注意,空格串不是空串),其长度为串中空格字符的个数。

        串的逻辑结构和线性表极为相似,区别仅在于串的数据对象限定为字符集。在基本操作上,串和线性表有很大差别。线性表的基本操作主要以单个元素作为操作对象,如查找、插入或删除某个元素等; 而串的基本操作通常以子串作为操作对象,如查找、插入或删除一个子串等。

1.2 串的存储结构

1.2.1 定长顺序存储表示

        类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。在串的定义顺序存储结构中,为每个串变量分配一个固定长度的存储区,称为定长数组

#define MAXLEN 255  //预定义最大串长为255
typedef struct
{
    char ch[MAXLEN];  //每个分量存储一个字符
    int length;       //串的实际长度
}SString;

        串的实际长度只能小于等于MAXLEN,超过预定义长度的串值会被舍去,称为截断。串长有两种表示方法:一是如上述定义描述的那样,用一个额外的变量 length 来存放串的长度;二是在串值后面加一个不计入串长的结束标记字符 “\0” ,此时的串长为隐含值。

        在一些串的操作(如插入、联接等)中,若串值序列的长度超过上界 MAXLEN,约定用 “截断” 法处理,要克服这种弊端,只能不限定串长的最大长度,即采用动态分配的方式。

1.2.2 堆分配存储表示

        堆分配存储表示仍然以一组地址连续的存储单元存放串值的字符序列,但是区别于定长顺序存储的地方是堆分配存储的存储空间是在程序执行过程中动态分配得到的。

typedef struct
{
    char *ch;   //按串长分配存储区,ch 指向串的基地址
    int length;  //串的长度
}HString;

        在C语言中,存在一个称之为 “堆” 的自由存储区,并用 malloc() 和 free() 函数来完成动态存储管理。利用 malloc() 为每个新产生的串分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基地址,这个串由ch指针来指示;若分配失败,则返回NULL。已分配的空间可用 free ()释放掉。

1.2.3 块链存储表示

        类似于线性表的链式存储结构,也可采用链表方式存储串值。由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点既可以存放一个字符,也可以存放多个字符。每个结点

称为,整个链表称为块链结构

        如下图 a,每个结点大小为4,每个结点存放四个字符,最后一个结点占不满时通常用 “#” 补上; 图 b,每个结点大小为1;

1.3 串的基本操作

  • StrAssign(&T,chars):赋值操作。把串T赋值为chars;
  • StrCopy(&T,S):复制操作。由串S复制得到串T;
  • StrEmpty(S):判空操作。若S为空串,则返回TRUE,否则返回FALSE;
  • StrCompare(S,T):比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0;
  • StrLength(S):求串长。返回串S的元素个数。
  • SubString(&Sub,S,pos,len):求子串。用 Sub 返回串 S 的第 pos 个字符起长度为 len 的子串。
  • Concat(&T,S1,S2):串联接。用 T 返回由 S1 和 S2 联接而为的新串。
  • Index(S,T):定位操作。若主串 S 中存在与串 T 值相同的子串,则返回它在主串 S 中第一次出现的位置;否则函数值为 0。
  • ClearString(&S):清空操作。将 S 清为空串。
  • DestroyString(&S):销毁串。将串 S 销毁。
//求子串
 
#define MAXSIZE 255     //预定义最大串长为255

typedef struct
{
    char ch[MAXSIZE];   //每个分量存储一个字符
    int length;     //串的实际长度
}SString;

bool SubString(SString &Sub,SString S,int Pos,int len)
{
    //判断子串范围是否越界 
    if(pos+len-1 > S.length)    // Sub 用于返回,S是所要找的主串,找子串就是从主串 S 中的 pos 位置开始找到长度为 len 的子串
    {
        return false;
    }
    for(int i=pos;i<pos+len;i++) // for 循环起始位置是 pos ,范围是[pos,pos+len],将这个范围内的每一个字符都赋值给Sub 用于返回;
    {    
        Sub.ch[i-pos+1] = S.ch[i]; 
    }
    Sub.length = len;  // Sub 的长度是所要定位的子串的长度
    return true;
}
//比较操作

int StrCompare(SString S,SString T)
{
    //比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0
    for(int i=1;i<=S.length && i<=T.length;i++)
    {
        if(S.ch[i]!=T.ch[i])
            return S.ch[i]-T.ch[i];                
    }
    //扫描过的所有字符都相同,则长度长的串更大
    return S.length-T.length;
}
//定位操作

int Index(SString S,SString T)
{
    int i=1,n=StrLength(S),m=StrLength(T); // n 表示主串 S 的长度; m 表示子串 T 的长度
    SString sub;  // 用于暂存子串
    while(i<n-m+1) //主串中最多可以找到 n-m+1 个子串,其中n表示主串的长度,m 表示子串的长度
    {
        SubString(sub,S,i,m); // 从主串 S 的第 i 个位置开始找到长度为len的子串
        if(StrCompare(sub,T)!=0) //从首元素开始比较,如果对应的元素不相等,则从第 (++i)个位置开始找到长度为 len 的子串
            ++i;
        else
            return i;  // 如果对比子串全部相等,则返回子串在主串中的位置
    }    
    return 0; // S 中不存在与 T 相等的子串
}

2. 串的模式匹配

2.1 简单的模式匹配算法

        子串的定位操作通常称为串的模式匹配,它求的是子串 (常称为模式串) 在主串中的位置。

//首先先需要知道:主串长度为 n ,模式串长度为 m ;
主串中最多有 n-m+1 个子串

这个其实很好理解:
    首先子串的长度为 m ,那么要找一定是从主串中找长度为 m 的子串,一定是从第一个元素开始依次遍历
n-m表示的意思是要给最后一个子串留有充足的空间,防止溢出;n-m+1 中的 加一 的意思是锁定最后一个子串的首元素
//这里采用定长顺序存储结构,一种不依赖于其他串操作的暴力匹配算法

//算法的思想是:
            //从主串S的第一个字符起,与子串T的第一个字符比较,若相等,则继续逐个比较后续字符;否则
//从主串的下一个字符起,重新和模式串的字符比较;以此类推,直至子串 T 中的每个字符依次与主串 S 中的一
//个连续的字符序列相等,则匹配成功;函数值为与子串 T 中第一个字符相等的字符在主串 S 中的序号(也就
//是返回子串首元素在主串中对应的序号),否则称匹配不成功,函数值为零。

int Index(SString S,SString T)
{
    int i=1,j=1;
    while(i<S.length && j<=T.length)
    {
        if(S.ch[i]==T.ch[j])
        {
            ++i;
            ++j;  //继续比较后继字符
        }
        else  //如果两个指针指向的元素不相等
        {
            i=i-j+2; // i-j+2 的意思是:通过执行上述程序,i 和 j 会同时指向不相等的元素,i-j的意思是把 i 指向这个子串的前一个元素,记住是子串的前一个元素,不是元素的前一个元素;
//i-j+2 现在指向的是原本不相等子串的第二个元素,思想就是从主串中一个元素一个元素的依次遍历,上一个子串已经不符合要求了,现在需要去找下一个子串,上一个子串的第二个元素就是下一个子串的首元素
            j=1;   //指针后退重新开始匹配
        }
        if(j>T.length)
            return i-T.length;
        else
            return 0;
    }
}

//简单模式匹配算法的最坏时间复杂度为O(nm),其中n和m分别为主串和子串的长度。

2.2 串的模式匹配算法——KMP算法

        在暴力匹配中,每趟匹配失败都是模式后移一位再从头开始比较。而某趟已匹配相等的字符序列是模式的某个前缀,这种频繁的重复比较相当于模式串在不断地进行自我比较,这也就是其低效率的根源;

2.2.1 字符串的前缀、后缀和部分匹配值

        前缀、后缀部分匹配值子串的结构联系紧密前缀指除最后一个字符以外,字符串的所有头部子串;后缀指除第一个字符外,字符串的所有尾部子串;部分匹配值则为字符串的前缀和后缀的最长相等前后缀长度。

例如:

       ‘ababa’

‘a’的前缀和后缀都是空集,最长相等前后缀长度为0;

‘ab’的前缀是a,后缀是b,a交b=空集,最长相等前后缀长度为0;

‘aba’的前缀是a、ab,后缀是a、ba,交集是a,最长相等前后缀长度为1;

‘abab’的前缀是a、ab、aba,后缀是b、ab、bab,交集是ab,最长相等前后缀长度为2;

‘ababa’的前缀是a、ab、aba、abab,后缀是a、ba、aba、baba,交集是a、aba,最长相等前后缀长度为3;

故字符串‘ababa’的部分匹配值是 00123;

那么部分匹配值到底有什么作用呢?

        回到我们暴力匹配的最初问题中,主串为 ababcabcacbab,子串为 abcac;通过上述的学习我们可以计算出 ‘abcac’ 的部分匹配值为 00010 ,将该部分匹配值写成数组的形式,就得到了部分匹配表(Partial Match,PM)的表

下面用 PM 表进行字符串匹配:

第一趟匹配过程:

        发现 c 与 a 不匹配,前面的 2 个字符 ‘ab’ 是匹配的,查PM表可以知道:最后一个匹配的字符 b 对应的部分匹配值为 0 ,按照下面的公式算出子串需要向后移动的位数:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

因此 移动位数 = 2 - 0 = 2;所以子串向后移动两位;

第二趟匹配过程:

        第二趟匹配过程在第一趟匹配过程的基础上向后移动了两位;发现 c 与 b 不匹配,前面四个字符 “abca” 是匹配的,最后一个匹配字符 a 对应的部分匹配值为 1 ,4 - 1 = 3 ;所以将子串向后移动 3 位;

第三趟匹配过程:

        第三趟子串全部比较完成,匹配成功。整个匹配过程中,主串始终没有回退,所以 KMP算法 可以在 O(n+m) 的时间数量级上完成串的模式匹配操作,大大提高了匹配效率

2.2.2 KMP算法的原理是什么?

        刚刚我们只是学习了怎么计算字符串的部分匹配值怎样利用子串的部分匹配值快速地进行字符串匹配操作这里着重来学习公式 “移动位数 = 已匹配的字符数 - 对应的部分匹配值” 的意义

        如图 4.3 所示,当 c 与 b 不匹配时,已匹配 ‘abca’ 的前缀 a 和后缀 a 为最长公共元素。已知前缀 a 和 b c 均不同,与后缀 a 相同,所以不需要进行比较,直接将子串移动 “已匹配的字符数 - 对应的部分匹配值” 用子串前缀后面的元素与主串匹配失败的元素开始比较即可

对算法的改进方法:

        已知:右移位数 = 已匹配的字符数 - 对应的部分匹配值

        写成:Move = ( j - 1 ) - PM [ j - 1 ]

        使用 部分匹配值 时,每当匹配失败,就去找它前一个元素的部分匹配值,这样使用起来是不方便的,因此将 PM 表右移一位,这样哪个元素匹配失败,直接看它自己的部分匹配值即可。

        将上个例子字符串 ‘abcac’ 的 PM 表右移一位,就得到了 next 数组:

我们注意到:

        1. 第一个元素右移以后空缺的用 -1 来填充,因为若是第一个元素匹配失败,则需要将子串向右移动一位,不需要计算子串移动的位数。

        2. 最后一个元素在右移的过程中溢出,因为原来的子串中,最后一个元素的部分匹配值是其下一个元素使用的,显然最后一个元素已经没有下一个元素了,故可以舍去。

这样,上式可以改写成

        Move = ( j - 1 ) - next [ j ] 

 相当于将子串的比较指针 j 回退到

        j = j - Move = j - (( j - 1 )- next [ j ]) = next [ j ] + 1

也可以将 next 指针整体 +1

        在实际的匹配过程中,子串在内存中是不会移动的,而是指针在变化next [ j ] 的含义是:在子串的第 j 个字符与主串发生失配时,则跳到子串的 next [ j ] 位置重新与主串当前位置进行比较。

//写 next 值程序
void get_next(String T,int next[])
{
    int i=1,j=0;
    next[1]=0;
    while(i<T.length)
    {
        if(j==0 || T.ch[i]==T.ch[j])
        {
            ++i;
            ++j;
            next[i]=j; 
            //若pi=pj,则 next[j+1] = next[j] + 1
        }
        else
        {    
            j=next[j];  //否则令 j=next[j],循环继续
        }
    }
}

2.2.3 KMP算法的进一步优化

        这里直接给出 KMP 算法的优化代码具体的 KMP 算法代码如何实现,参照下述相关练习题具体说明;

void get_nextval(String T,int nextval[])
{
    int i=1,j=0;
    nextval[1]=0;
    while(i<T.length)
    {
        if(j==0 || T.ch[i]==T.ch[j])
        {
            ++i;
            ++j;
            if(T.ch[i]!=T.ch[j])
                nextval[i]=j;
            else
                nextval[i]=nextval[j];
        }
        else
            j=nextval[j];
    }
}

2.3 相关练习

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

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

相关文章

你不知道的裁员攻略:真正的离职赔偿是2N起步,不是N+1!计算赔偿金时,年终奖要计入总收入!...

最近裁员现象猖獗&#xff0c;许多人都不知道如何维护自己的合法权益&#xff0c;在和公司撕扯时屡屡被坑。一位曾经和字节撕过且成功的网友&#xff0c;友情给大家写了一份离职攻略。但要注意&#xff0c;这份攻略只针对那些守规矩的大公司&#xff0c;如果是不守规矩的小公司…

得ChatGPT者,得智能客服天下?

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 在现代社会&#xff0c;高效、专业的客服服务已成为企业、组织机构竞争力的关键要素。智能客服系统应运而生&#xff0c;智能客服系统对客服的赋能作用和价值主要表现在提高效率、降低成本、优化用户体验、深度挖掘用户需求…

【综述】结构化剪枝

目录 摘要 分类 1、依赖权重 2、基于激活函数 3、正则化 3.1 BN参数正则化 3.2 额外参数正则化 3.3 滤波器正则化 4、优化工具 5、动态剪枝 6、神经架构搜索 性能比较 摘要 深度卷积神经网络&#xff08;CNNs&#xff09;的显著性能通常归因于其更深层次和更广泛的架…

QML转换(Transformation)

目录 一 QML介绍 二 QML的使用场合 三 实例演示 一 QML介绍 QML是Qt Quick的缩写&#xff0c;它是一种新型的、面向对象的、跨平台的脚本语言&#xff0c;可以用来描述用户界面或应用程序的交互逻辑。QML可以在Qt应用程序中使用&#xff0c;也可以在其他JavaScript应用程序中…

Python入门(九)字典(二)

字典&#xff08;二&#xff09; 1.遍历字典1.1 遍历所有键值对1.2 遍历字典中的所有键1.3 按特定顺序遍历字典中的所有键 2.嵌套2.1 字典列表2.2 在字典中存储列表2.3 在字典中存储字典 作者&#xff1a;xiou 1.遍历字典 一个Python字典可能只包含几个键值对&#xff0c;也可…

【源码解析】SpringBoot自定义条件及@Conditional源码解析

自定义注解 自定义条件类 public class MessageCondition extends SpringBootCondition {Overridepublic ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {MultiValueMap<String, Object> attrs metadata.getAllAnnota…

成年人的崩溃只在一瞬间,程序员凌晨三点写的代码竟被女友删了...

对于恋爱中的情侣来说&#xff0c;吵架是很正常的事情&#xff0c;就算是再怎么亲密&#xff0c;也难免会出现意见不合的时候。 吵架不可怕&#xff0c;可怕的是&#xff0c;受吵架情绪的影响&#xff0c;做出一些比较“极端”的事情。 之前某社交平台上一位女生吐槽自己的男…

Ubuntu 本地部署 Stable Diffusion web UI

Ubuntu 本地部署 Stable Diffusion web UI 0. 什么是 Stable Diffusion1. 什么是 Stable Diffusion web UI2. Github 地址3. 安装 Miniconda34. 创建虚拟环境5. 安装 Stable Diffusion web UI6. 启动 Stable Diffusion web UI7. 访问 Stable Diffusion web UI8. 其他 0. 什么是…

我这里取出来的数据(最后边的excel)有点问题,我没有要取性别的数据,但是表里有...

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 上穷碧落下黄泉&#xff0c;两处茫茫皆不见。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python钻石群【不争】问了一个Python自动化办公的问题&…

PyCharm安装教程,图文教程(超详细)

「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 PyCharm 一、PyCharm下载安装二、Python下载安装三、创建项目四、安装模块1、pip安装2、P…

商城系统商品属性的数据库设计思路

京东商城的数据库是如何搭建的&#xff0c;那么多商品&#xff0c;每种商品的参数各不相同&#xff0c;是怎样设计数据库的&#xff1f; 在提及这种设计思路前&#xff0c;首先得了解数据表可以分为两种结构: 1\横表,也就是我们经常用到的表结构&#xff0c; 2\纵表,这种结构…

ASEMI代理LT8609AJDDM#WTRPBF原装ADI车规级芯片

编辑&#xff1a;ll ASEMI代理LT8609AJDDM#WTRPBF原装ADI车规级芯片 型号&#xff1a;LT8609AJDDM#WTRPBF 品牌&#xff1a;ADI /亚德诺 封装&#xff1a;DFN-10 批号&#xff1a;2023 安装类型&#xff1a;表面贴装型 引脚数量&#xff1a;10 工作温度:-40C~125C 类型…

MySQL学习---13、存储过程与存储函数

1、存储过程概述 MySQL从5.0版本开始支持存储过程和函数。存储过程和函数能够将负杂的SQL逻辑封装在一起&#xff0c;应用程序无序关注存储过程和函数内部复杂的SQL逻辑&#xff0c;而只需要简单的调用存储过程和函数就可以。 1.1 理解 含义&#xff1a;存储过程的英文是Sto…

ASEMI代理LTC2954CTS8-2#TRMPBF原装ADI车规级芯片

编辑&#xff1a;ll ASEMI代理LTC2954CTS8-2#TRMPBF原装ADI车规级芯片 型号&#xff1a;LTC2954CTS8-2#TRMPBF 品牌&#xff1a;ADI/亚德诺 封装&#xff1a;TSOT-23-8 批号&#xff1a;2023 引脚数量&#xff1a;23 工作温度&#xff1a;-40C~85C 安装类型&#xff1a…

若依框架(RuoYI)项目打包(jar)方法,部署到 Linux 服务器

序言 在若依框架的 bin 目录下&#xff0c;存在着三个 bat 文件&#xff0c;一个是清除之前的依赖的自动化 bat 脚本&#xff08;clean.bat&#xff09;&#xff0c;一个是自动化项目打包的 bat 脚本&#xff08;package.bat&#xff09;&#xff0c;一个是运行若依项目的脚本…

云原生CAx软件:技术约束

Pivotal公司的Matt Stine于2013年首次提出Cloud Native(云原生)的概念&#xff0c;从概念提出到技术落地&#xff0c;云原生还处于不断发展过程中&#xff0c;关于云原生的定义也不断在完善。 云原生为使用开源软件堆栈来创建容器化、动态编排和面向微服务的应用程序。 Heroku创…

YOLOv8 深度解析!一文看懂,快速上手实操(附实践代码)

关注并星标 从此不迷路 计算机视觉研究院 公众号ID&#xff5c;ComputerVisionGzq 学习群&#xff5c;扫码在主页获取加入方式 开源地址&#xff1a;https://github.com/ultralytics/ultralytics 计算机视觉研究院专栏 作者&#xff1a;Edison_G YOLOv8 是 ultralytics 公司在 …

消息队列测试场景和redis测试场景

一、消息队列测试场景 1、什么是消息队列。你是怎么测试的&#xff1f; 解题思路&#xff1a; 什么是消息队列。 消息队列应用场景。 消息队列测试点列举。 2、什么是消息队列 Broker&#xff1a;消息服务器&#xff0c;提供消息核心服务 Producer&#xff1a;消息生产者&a…

Python入门(八)字典(一)

字典&#xff08;一&#xff09; 1.字典概述2.一个简单的字典3.使用字典3.1 访问字典中的值3.2添加键值对3.3 先创建一个空字典3.4 修改字典中的值3.5 删除键值对 作者&#xff1a;xiou 1.字典概述 在本章中&#xff0c;你将学习能够将相关信息关联起来的Python字典&#xff…

二叉排序树查找成功和不成功的平均查找长度

理解二叉树的特性: 1)结点:包含一个数据元素及若干指向子树分支的信息。 2)结点的度:一个结点拥有子树的数据成为结点的度。 3)叶子结点:也称为终端结点,没有子树的结点或者度为零的结点。 4)分支结点:也称为非终端结点,度不为零的结点成为非终端结点。 5)结点…