🚀前言
😊各位小伙伴久等了,本专栏新文章出炉了!!!
计算机上的非数值处理的对象基本上是字符串数据,字符串一般简称为串 ,是计算机中一种常见且重要的数据结构。
在不同数据类型的应用中,所处理的字符串具有不同的特点,要有效的实现字符串的处理,就必须根据具体情况使用合适的存储结构。
🚀串类型的定义
串(String)(或字符串)是由零个或多个字符组成的有限序列 ,一般记为
S
=
′
a
1
,
a
2
,
a
3
,
.
.
.
,
a
n
′
(
n
>
=
0
)
S='a1, a2, a3,...,an'(n>=0)
S=′a1,a2,a3,...,an′(n>=0),其中,s是串的名,用单引号括起来的字符序列是串的值;
a
i
(
1
<
=
i
<
=
n
)
ai(1<=i<=n)
ai(1<=i<=n)可以是字母,数字或其他字符;串中字符的数目n称为串的长度,零个字符的串称为空串(null String),表示为
t
=
“”
t=“ ”
t=“”,它的长度为零,串的逻辑结构属于线性结构。
📌例如:S=“holleworld!”
由一个或多个空格字符组成的串,称为空格串(blank string,请注意,此处不是空串),它的长度为串中空格字符的个数
串中任意个连续的字符组成的子序列称为该串的子串 ,包含子串的串相应地称为主串 ,空串是任意非空字符串的子串,通常称字符在序列中的序号为该字符在串中的位置,子串在主串中的位置则以子串的第一个字符在主串中的位置来表示
想要称两个串是相等的,只有当且仅当这两个串的值相等,那么这两个串才是相等的,也就是说,只有当两个串的长度相等,并且各个对应位置的字符都相等时才相等
值得一提的是,串值必须用一对单引号(双引号)括起来,但单引号本身不属于串,它的作用是为了避免与变量名或数的常量混淆而已
串的逻辑结构和线性表极为相似,区别仅在于串的数据对象约束为字符集,然而,串的基本操作和线性表由很大差别。在线性表的基本操作中,大多以”单个元素“作为操作对象,例如在线性表中查找某个元素,求取某个元素,在某个位置上插入一个元素和删除一个元素等,而在串的基本操作中,通常以”串的整体“作为操作对象,例如在串中查找某一个子串,求取一个子串,在串中的某个位置上插入一个子串以及删除一个子串
串与线性表的区别:
- 📌串是限定了元素为字符的线性表
- 📌线性表的操作主要针对表内的某一个元素,而串操作主要针对串内的一个子串(整体)
- 📌在串里面,一个数据元素是由一个字符组成
🚀串的存储结构以及实现
串有3中机内表示方法,分别介绍如下:
🚢定长顺序存储表示
类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列,在串的顺序存储结构中,按照预定义的大小,为每个定义的串分量分配一个固定长度的存储区,可用于定长数组描述
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
串的实际长度可在这预定义长度的范围内随意,超过预定义长度的串值则被舍去,称之为“截断” 。对串长有两种方法表示:一种是如上定义描述的那样,以下标为0的数组分量存放串的实际长度,另一种是在串值后面加一个不计入串长的结束标记字符,此时的串长为隐含值,显然不便于进行某些串的操作。
🌈串联接Concat(&T, S1, S2)
假设
S
1
,
S
2
S1,S2
S1,S2和
T
T
T都是String型的串变量,且串T是由串S1联杰结串S2得到的,即串T的值的前一段和串S1的值相等,串T的值的后一段和串S2的值相等,则只要进行相应的“串值复制” 操作即可,只是需前述约定,对超长部分实施“截断”操作
🌈求子串SubString(& Sub, S, pos, len)
求子串的过程即为复制字符序列的过程,将串S中从第pos个字符开始长度为len的字符序列赋值到串Sub中。显然,本操作不会有需截断的情况,但有可能产生用户给出的参数不符合操作的初始条件,返回false
bool SubString(SString &Sub, SString S, int pos, int len){
if(pos+len-1 > S.length){
return false;
}
for(int i = pos; i<pos+len; i++){
Sub.ch[i-pos+1] = S.ch[i];
}
Sub.length = len;
return true;
}
在顺序存储结构中,实现串操作的原操作为“字符序列的复制”,操作的时间复杂度基于复制的字符序列的长度,另一操作的特点是:如果在操作中出现串值序列的长度超过上界MAXSTRLEN 时,约定用截尾法 处理。这种情况不仅在求联串时可能发生,在串的其他操作中,如插入、置换等也可能发生。客服这个弊病唯有不限定串长的最大长度,即动态分配串值的存储空间。
🌈比较操作
若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
int StrCompare(SString S, SString T){
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;
}
🌈定位操作
若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0。
int Index(SString S, SString T){
int i = 1, n = StrLength(s), m = StrLength(T);
SString sub;
while(i<=n-m+1){
SubStriing(sub, S, i, m);
if(StrCompare(sub, T) != 0){
i++;
}else{
return i;
}
}
return 0;
}
?
🚢堆分配存储表示
这种存储表示的特点是,仍以一组地址连续的存储单元存放串值字符序列,但他们的存储空间是在程序执行过程中动态分配而得。利用函数malloc() 为每个新产生的串分配一块实际串长所需要的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基址,同时,为了以后处理方便,约定串长作为存储结构的一部分
#define MAXLEN 255
typedef struct{
char *ch;
int length;
}HString;
HString S;
S.ch = (char *)malloc(MAXLEN * sizeof(char));
S.length = 0;
这种存储结构表示时的串操作仍是基于“字符序列的复制”进行的。
由于堆分配存储结构的串既有顺序存储结构的特点,处理方便,操作中对串长又没有任何限制,更显灵活,因此在串处理的应用程序中也常被选用
🚢链式存储表示
和线性表的链式存储结构相类似,可采用链表方式存储串值,由于串结构的特殊性——结构中的每个数据是一个字符 ,则用链表存储串值时,存在一个“结点大小”的问题,即每个结点可以存放一个字符,也可以存放多个字符。当结点大小大于1时,由于串长不一定是结点大小的整数倍,则链表中的最后一个结点不一定全被串值占满,此时通常补上“#”或者其他非串值字符(通常“#”不属于串的字符集,是一个特殊的符号
第一种表示:
typedef struct StringNode{
char ch;
struct StringNode * next;
}StringNode, * String;
为了便于进行串的操作,当以链表存储串值时,除头指针外还可附设一个尾指针表示链表中的最后一个结点,并给出当前串的长度。称如此定义的串存储结构为块链结构
第二种表示:
typedef struct StringNode{
char ch[4];
struct StringNode * next;
}StringNode, * String;
由于在一般情况下,对串进行操作时,只需要从头向尾扫描即可,则对串值不必建立双向链表 。设尾指针的目的是为了便于进行联结操作,但应注意联结时需要处理第一个串尾的无效字符
在链式存储方式中,结点大小的选择和顺序存储方式的格式选择一样都很重要,它直接影响着串处理的效率
存储密度可定义为:串值所占的存储位/实际分配的存储位
存储密度小(如结点大小为1时),运算处理方便,然而,存储占用最大,如果在串处理过程中需要进行内、外存交换的话,则会因为内外存交换操作过多而影响处理的总效率。一般地,字符集小,则字符的机内编码就短,这也影响串值的存储方式的选取。
串值的链式存储结构对某些串操作,如联接操作等有一定方便之处,但总的来说不如另两种存储结构灵活,它占用存储量大且操作复杂。
🚀拓展
🚢朴素模式匹配算法
串的模式匹配:在主串中找到与匹配模式相同的子串,并返回其所在位置。
朴素模式匹配算法: 将主串中与模式串长度相同的子串提出来,一个一个与模式串进行比较,当子串与模式串某个对应字符不匹配时,就立即放弃当前子串,转而检索下一个子串。
int Index(SString S, SString T){
int k=1;
int i=k, j=1;
while(i<=S.length && j<=T.length){
if(S.ch[i] == T.ch[i]){
++i;
++j;
}else{
k++;
i=k;
j=1;
}
}
if(j>T.length){
return k;
}else{
return 0;
}
}
若模式串长度为m,主串长度为n,则匹配成功的最好时间复杂度为:O(m) ,匹配失败的最好时间复杂度为:O(n)
若模式串长度为m,主串长度为n,则直到匹配成功/匹配失败最多需要
(
n
?
m
+
1
)
?
m
(n-m+1)*m
(n?m+1)?m次比较。最坏时间复杂度就可表示为:O(nm)
🚢KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt 提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法 )。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。
朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描指针i会经常回溯,导致时间开销增加。KMP算法的出现就是为了解决主串指针回溯问题。
int Index_KMP(SString T, int next[]){
int i=1, j=1;
while(i<=S.length && j<=T.length){
if(j==0 || S.ch[i] == T.ch[j]){
++i;
++j;
}else{
j=next[j];
}
}
if(j>T.length){
return i-T.length;
}else{
return 0;
}
}
这里只是对KMP算法的 一个简单介绍,并没有过多深入,但是KMP算法是串这一章节中比较重要的一个算法,后面还会涉及到next数组的推算等等,所以将会在以后单独出一篇文章讲解KMP算法。
💻总结
在经过将近一个多月的停更后,本专栏的文章再次开始更新,之前因为个人原因停更,感谢各位小伙伴们的支持与等待,同样,在新文章更新的同时,我也会不断提高自己的创作水平,争取自己的文章能给小伙伴们带来更多的帮助。
本篇文章比较全面的讲解了数据结构中串这一节的详细内容,因其与线性表类似的结构,所以有些点并没有重复提起,但是关于串的知识点,小伙伴们要多花时间去细读文章,串是编程中处理字符串比较重要的数据结构。一如既往希望我的文章能给各位小伙伴们带来帮助,数据结构与算法专栏也在持续更细中!!!
🎨觉得不错的话记得点赞收藏呀!!🎨
😀别忘了给我关注~~😀
|