考纲要求 💕
知识点(考纲要求)
- (1)串的基本概念、串的基本操作和存储结构。
- (2)串的模式匹配算法和改进的KMP算法
考核要求
1.掌握串的特性,串和线性表之间的关系
2.掌握串的各种存储结构,比较它们的优缺点。
3.理解串的各种基本操作。
▶️ 串 ✨
串的定义 ✨
串,即字符串(String)是由零个或多个字符组成的有限序列。
一般记为 S = ‘a1a2······an' (n ≥0) 其中,S是串名,单引号括起来的字符序列是串的值;ai可以是字母、数字或其他字符;i指的是该字符在串中的位置,串中字符的个数n称为串的长度。n = 0时的串称为空串(用∅或者""表示)
例子:
S="hello world";
T='iphone 11 Pro'; //注意有的地方用双引号(java,c)有的单引号(python)
相关术语 ✨
-
空格串:是只包含空格的串,注意与空串的区别 -
子串与主串:串中任意个数的连续字符组成的子序列称之为该串的子串,相应地,包含该子串的串称为主串 -
- 比如"
over“就是”lover"的字串
- 比如"
-
字符在串中序号:注意区别数组,下标从1开始
串的基本操作 ✨
串需要实现的基本操作如下
StrAssign(&T,chars):赋值,把串T赋值为charsStrCopy(&T,S):复制,由串S复制得到TStrEmpty(S):判空StrLength(S):求串长度ClearString(&S):清空DestoryString(&S):销毁(释放空间)Concat(&T,S1,S2):连接,用T返回S1和S2连接而成的新串SubString(&Sub,S,pos,len):求子串,用Sub返回串S的第pos个字符串起长度为len1的子串Index(S,T):定位,若主串S中存在与串T相同的子串,则返回它在主串S中第一次出现的位置,否则返回0StrCompare(S,T):比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,返回值<0
Eg:串T=“”,S=”iPhone 11 Pro Max?”,W=“Pro”
- 执行基本操作 Concat(&T, S, W) 后,T=“iPhone 11 Pro Max?Pro”
- 执行基本操作 SubString(&T ,S, 4, 6)后,T=“one 11”
- 执行基本操作 Index(S, W)后,返回值为 11
串的比较操作 StrCompare ✨
串的比较操作位
StrCompare(S,T),其中
- 若S>T:则返回值大于0
- 若S=T:则返回值等于0
- 若S<T:则返回值小于0
对于给定的两个串:s="a1a2...an" 和 t="b1b2...bm",当下列条件满足时有s<t
-
n<m ,且ai=bi(1,2,3,...n),即前n个字符相同,但长度较短
-
存在某个k<=min(m,n),使得ai=bi(i=1,2,3,....,k-1) ,即ak<bk;
例如:
- “
abandon”<"aboard" - “
abstract”<"abstraction" - “
academic”>"abuse" - “
academic”="academic"
总结下,也就是这样比较的,比如上面“abandon”<"aboard" ,
从第一个字符开始向后比较,先出现更大字符的串更大。
然后下一个
“abstract”<"abstraction" ,串的前缀相同,更长的串更大
“academic”="academic",只有二个串完全相同时候才相等
字符集编码
y = f(x)
字符集:函数定义域
编码:函数映射规则 f
y:对应的二进制数
任何数据存到计算机中 一定是二进制数。 需要确定一个字符和二 进制数的对应规则 这就是“编码”
“字符集”: 英文字符——ASCII字符集 中英文——Unicode字符集 比如:基于同一个字符集, 可以有多种编码方案, 如:UTF-8,UTF-16
注:采用不同的编码方 式,每个字符所占空间 不同,考研中只需默认 每个字符占1B即可
就是字符编码类型(转换为二进制数表示),比如ASCII编码,
这部分内容比较简单,如果有兴趣的可以在计组中深入了解
下面可正式来讲串的存储结构,如何实现串:
1. ❗ 串的存储结构
❗ 1.1 串的顺序表示 ✨
串的顺序存储结构:是用一组地址连续的存储单元来存储串中的字符序列,一般使用定长数组实现。规定在串值后面加上一个不计入串长度的结束标记字符,比如"\0"来表示串的结束
注意:'\0'不算入长度但需要占用空间
如下图:
因此其结构定义如下:
❗ 1.1.1 固定长度
#defined MaxSize 255 //预定义最大串长为255
typedef struct SString
{
char ch[MaxSize];
int len;
}SString;
这个是固定长度的,一般用不固定长度的
❗ 1.1.2 动态数组
typedef struct HString
{
char *ch; // 指向动态分配存储区首地址的字符指针
int len;
}HString;
HString S;
S.ch=(char*)malloc(MaxSize*sizeof(char)); //用完需要手动free释放内存
//这样就申请了MaxSize大小的数组空间
s.len=0; //初始时候长度为0
❗ 1.1.3 顺序存储的一些注意事项
这里说明下串顺序存储的存储结构跟数组不同,如下图所示:
一般教材用第四种,这里不用char[0],直接从1开始,变量length
❗ 1.2 串的链式表示 ✨
串的链式存储结构:和线性表的链式存储结构类似
因此其结构体定义如下:
typedef struct StringNode
{
char ch;
struct StringNode* next; //next结点指针
}StringNode;
就跟链表一样。
但是,需要注意,这种方式存储密度较低,每个结点空间利用不充分,所以可以考虑将每个结点搞成数组
就是数据域扩大,以前是一个,现在搞成数组
因此其结构体定义如下:
typedef struct StringNode
{
char ch[4];
struct StringNode* next;
}StringNode;
这样就算完成了最基本的串的结构定义,下面实现它的基本操作
2. ❗ 串的基本操作实现
这里用固定长度结构说明
#defined MaxSize 255 //预定义最大串长为255
typedef struct SString
{
char ch[MaxSize];
int len;
}SString;
SubString:求子串
//求子串
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]; //把原来串S的pos位置开始往后len依次放入Sub数组的1,2,3...位置
Sub.length=len;
}
return true;
}
StrCompare(S,T): 比较操作
下面举个例子:比如:
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;
}
这里需要知道下字符相减加也可以转换为ASCII码中数字相加减
字符指的是:与课本上ASCII表相对应,例如0 1 2 3 a b c A B C等;字符相减知道是:对应到ASCII码相减得到整数值,例如 'c'-'a' 的就是:2
所以上面可以直接字符相减比较大小
index(S,T): 定位操作
比如T如下,找它在上面S中的位置
int index(SString S,SString T){
int i=1,n=StrLength(S),m=StrLength(T);
SString sub; //用于暂存子串
while(i<=n-m+1)
SubString(sub,S,i,m); //用Sub返回串S的第i个字符起长度为m的子串。
if(StrCompare(sub,T)!=0) //返回的字串跟T不相同
++i;
else
return i;//相同,找到了,i是T第一个字符位置,后面i+m是字串=T。返回字串位置
}
return 0; //S中不存在与T相等的子串
}
上面基本操作差不多完成了,我记得基本操作有些可以直接调用使用的。。
下面看串的一些匹配算法
3. ❗ ✨ 串的模式匹配算法
✨ 什么是模式匹配?
字符串模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置
比如我们搜索百度搜索东西时候搜索栏输入的内容,找到与输入的字串类型相同的内容就叫字符串的模式匹配。
注意一下:
模式串跟字串可不一样,字串是主串的一部分能找到,而模式串不一定能在主串中找到。
比如下面举个例子:
模式串是 '笑出喵叫','笑出猪叫'的话,与它类型相同的模式串是'笑出猪叫',但是模式串的另一个'笑出喵叫' 不是主串的一部分。
下面讲解下这二个算法
3.1 ✨ 朴素的模式匹配算法
3.1.1 ✨ 定义
字符串模式匹配:是一种纯暴力算法。将主串中所有长度为m的子串依次与模式串比较,直到找到一个完全匹配的子串,或所有子串都不匹配为止
3.1.2 ✨ 举例说明
具体描述:以下面的主串S和模式串T为例。
如果当前子串匹配失败:主串指针指向下一个子串的第一个位置,模式串指针回溯至模式串的第一个位置。
举例:
- 主串S:"goodgoogle"
- 模式串T:"google"
- 主串S从第一位开始,S和T前三位字母都匹配成功,但是S第四个字母是d而T的则是 g,所以第一位匹配失败
2:主串S从第二位开始,主串S首字母是 o,但是需要匹配的T的首字母是 g,第二位匹配失败
3:主串从第三位开始,主串S首字母是o,但是需要匹配的T的首字母是g,第三位匹配失败
- 主串从第四位开始,主串S首字母是d,但是需要匹配的T的首字母是g,第四位匹配失败
- 主串从第五位开始,6个字母全部匹配成功
3.1.3 ✨ 代码实现
突然我们发现上面写的定位操作就是要求的
int index(SString S,SString T){
int i=1,n=StrLength(S),m=StrLength(T);
SString sub; //用于暂存子串
while(i<=n-m+1)
SubString(sub,S,i,m); //用Sub返回串S的第i个字符起长度为m的子串。
if(StrCompare(sub,T)!=0) //返回的字串跟T不相同
++i;
else
return i;//相同,找到了,i是T第一个字符位置,后面i+m是字串=T。返回字串位置
}
return 0; //S中不存在与T相等的子串
}
下面不使用操作函数,直接通过下标实现:
int index(SString S,SString T){
int i=1;//i指向主串S
int j=1;//j指向模式串T
int k=1;//记录不匹配时候S重新开始位置,1,2,3,4,5....
while(i<=S.length && j<= T.length){
if(S.ch[i]==T.ch[j]){
++i;++j;//继续比较后缀字符
}
else{ //不匹配
k++;
i=k; //主串从k位置重新开始匹配
j=1;//模式串从第一个位置重新开始匹配
}
}
if(j>T.len) //如果模式串扫描完了 ++j; j这时候=T.length+1
//说明匹配成功了,此时k就是模式串匹配的起始位置
return k;
else
return 0; //匹配失败。没有
}
效率分析
- 最好情况:时间复杂度为 O ( m )
- 平均情况:平均查找次数为 (m+n)/2,时间复杂度为 O ( n + m )
- 最坏情况:每次不成功的匹配都发生在T的最后一个字符,O((n−m+1)∗m),最坏时间复杂度为O(mn)
- 参考 : 朴素算法
3.2 ❗ KMP算法
3.2.1 ❗ 定义
前面介绍的朴素模式匹配算法,如果匹配中间有元素不同的话,咋办?
下面举个例子说明为啥:
按照朴素算法的话,你肯定这样回溯
因为主串的第2,3位都不是A,在第一位不匹配的情况下,回溯到2,3位也自然是无济于事的,既然这样还不如直接从第4位开始比较
所以KMP算法的核心就是:不要回溯到无效的地方,让其回溯到有效的位置
下面自己写的可以不看(按索引求)
3.2.2 ❗ 最长相同前缀和后缀
要想知道什么是最长相同前缀和后缀,首先得明白什么是字符串的前缀和后缀,看完下面这个图相信你就不难理解了
从前从后查
最长相同前缀和后缀你也应该能找出来了吧
- 你可能会奇怪不应该是最下面的那个吗?
其实这里就要说明一点:最长相同前后缀不能是字符串本身。因为如果你认为最长相同前后缀可以是字符串本身的话,那么这就是一个恒成立的命题了,也就不具有任何讨论的意义了。
3.2.3 ❗ 如何回溯
前面讲过,KMP算法的核心问题就是处理如何有效回溯的问题。
继续观察,你有没有发现它回溯的位置有些特点?没错!
就是最长公共前后缀重合的地方,也就是说让最长相同前缀对齐至后缀。
比如下面这个例子:
是在主串第5个位置发生不匹配,然后第二次匹配时候咱们要回溯到主串第4位A位置上,此时目标串的2号位置C与主串的不匹配位置第5个位置对齐了。
发现此时这个2恰好是目标串发生不匹配位置前的字符串的最长前缀和最长后缀的长度。
3.2.4 ❗ next数组
经过前面的叙述,我们可以意识到,这种回溯是有规律的,并不是凭空臆想的,我们再举几个例子
索引从0开始, 如下目标串的索引为3号的位置发生不匹配
其3号索引前的字符串的最长相同前后缀的长度是1,所以就应该让模式串索引为1的位置与该不匹配处“对齐”
再来,如下目标串的索引为5号的位置发生不匹配
其5号位置前的字符串的最长相同前后缀的长度是2,所以就应该让目标串索引为2的位置与该不匹配处“对齐”
其实从本质上来说匹配是根本“不需要”主串,因为每个位置发生不匹配时,总有一个值能确定其应该回溯的位置。这个值就是不匹配位置前面的字符串的最长相同前后缀的长度
我们把目标串每个位置发生不匹配时,目标串应该回溯的位置给记录下来,形成一个数组,这个数组就是next数组,next[i]=j所表示的意思是索引为i的位置发生不匹配,就让其回溯到j的位置继续匹配。(i是不匹配的索引位置,j是最大相同前后缀长度)
注意这里我们使用的是索引,目标串从索引0开始,但是有些使用的是目标串123456... ,直接第一个元素,第二个元素,next[1]=0; next[1]是第一个元素;
而且j的值都是最大相同前后缀+1,j指向模式串,也是第1.2.3.4.5.6元素
3.2.5 ❗ ✨ 求解next数组
上面学习了next数组的定义,下面学习下如何计算next数组
如下为一个目标串的next数组,需要注意的是由于第一个位置也就是next[0]前面没有串,因此要记为-1
比如:
目标串是这个,那么i指的是不匹配的索引位置,从0,1,2,3......这样开始
- next[0]=-1; //索引为0,指向A,A模式串前面没有串可以计算最大相同长度
- next[1]=0; //索引为1,指向B,此时前面是串A,最大相同长度为0
- next[2]=0; //索引为2,指向C,此时前面是串AB,最大相同长度为0
- next[3]=0; //索引为3,指向A,此时前面是串ABC,最大相同长度为0
- next[4]=1; //索引为4,指向B,此时前面是串ABCA,最大相同长度为1
- next[5]=2; //索引为5,指向C,此时前面是串ABCAB,最大相同长度为2
- next[6]=3; //索引为6,指向M,此时前面是串ABCABC,最大相同长度为3
- next[7]=0; //索引为7,指向N,此时前面是串ABCABCM,最大相同长度为0
表格如下:
| A | B | C | A | B | C | M | N |
|---|---|---|---|---|---|---|---|
| next[0] | next[1] | next[2] | next[3] | next[4] | next[5] | next[6] | next[7] |
| -1 | 0 | 0 | 0 | 1 | 2 | 3 | 0 |
注意这里我们使用的是索引,目标串从索引0开始,但是有些使用的是目标串123456... ,直接第一个元素,第二个元素,next[1]=0; next[1]是第一个元素;
而且j的值都是最大相同前后缀+1,j指向模式串,也是第1.2.3.4.5.6元素
上面自己写的可以不看(按索引求)
上面是看别人讲解的按照索引求next数组
下面我说下王道的按照元素位置匹配求next数组
3.2.6 ❗❗ ✨ 王道求解next数组
next[1]都⽆脑写 0next[2]都⽆脑写 1其他 next:在不匹配的位置前,划⼀根美丽的分界线,模式串⼀步⼀步往后退,直到分界线之前“能对上”,或模式串完全跨过分界线为⽌。此时 j 指向哪⼉,next数组值就是多少
举例:
- 首先next[1]=0 ;next[2]=1;
- next[3]
此时不匹配的位置是第3个元素,在3前面划一道分界线
然后 模式串一步一步后退,直到分界线之前的能对上,或者模式串完全跨过分界线
这时候完全跨过分界线,此时j指向哪里,next数组值就是多少
这时候j指向第一个元素,next[3]=1;
- next[4]
对上了,这时候ij
j指向模式串的第二个元素,所以next[4]=2;
- next[5]
- next[6]
此时j指向第四个元素。
3.2.7 ❗❗ ✨举例说明回溯过程和求next数组
关于这个特殊位置:说明下next[1]=0; 也就是说第一个位置不匹配,然后二个串都往后移动一下:
举例说明过程和求解:
例子1
- 开始时候
——————i=j=1;指向第一个元素
- 第一次匹配:到第6个元素发现匹配失败
此时求得 next[6]=3; 所以j=3; 从模式串的第3元素开始匹配,把模式串的第3个元素移到主串S的i位置,也就是刚才不匹配的位置重新匹配
- 第二次匹配:发现匹配成功
例子2
- 开始时候
——————i=j=1;指向第一个元素
- 第一次匹配:第五个元素匹配失败
此时求得 next[5]=2; 所以j=2; 从模式串的第2元素开始匹配,把模式串的第2个元素移到主串S的i位置,也就是刚才不匹配的位置重新匹配
- 第二次匹配:第2个元素匹配失败
此时求得 next[2]=1; 所以j=1; 从模式串的第1元素开始匹配,把模式串的第1个元素移到主串S的i位置,也就是刚才不匹配的位置重新匹配
- 第三次匹配:第1个元素匹配失败
第一个元素就匹配失败,这种情况下 ,让next[1]=0; j=0; i++;j++;
也就是把主串S和模式串T都往后移1
- 第四次匹配:第2个元素不匹配
此时求得 next[2]=1; 所以j=1; 从模式串的第1元素开始匹配,把模式串的第1个元素移到主串S的i位置,也就是刚才不匹配的位置重新匹配
- 第五次匹配:第三个元素不匹配
此时求得 next[3]=1; 所以j=1; 从模式串的第1元素开始匹配,把模式串的第1个元素移到主串S的i位置,也就是刚才不匹配的位置重新匹配
- 第六次匹配:第一个元素不匹配
第一个元素就匹配失败,这种情况下 ,让next[1]=0; j=0; i++;j++;
也就是把主串S和模式串T都往后移1
- 第七次匹配:匹配成功
此时i++,j++,i超过了它自身S串的长度。j也是,超过它自身模式串T的范围。
好了,下面试试代码实现。。。。
3.2.8 ❗ ✨ 代码实现
整个KMP算法的整个根据是这样的:
第一个模块next数组的计算可以手算出来,下面重点看看第二个模块回溯
还是以这个为例子:
if (S[i] !=T[j])
j=next[j];
if (j==0)
{ i++; j++ }
首先应该比较主串S[i]是否等于目标串[j]
如果不相等就要 j=next[j]; //j=next数组 ,下一个元素位置
如果j==0,i++,j++; 即第一个元素匹配失败,next[1]=0,下一个元素匹配
int Index_KMP(SString S,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]){//第一个位置匹配失败j=0或者匹配成功
++i; ++j; // 继续比较后继元素
}
else //匹配失败
j=next[j]; //模式串向右移动到j位置对应着不匹配的主串i的位置
}
if(j>T.length)
return i-T.length; //匹配成功,返回此时模式串第一个元素对应在主串中的位置
//就是返回匹配成功时候模式串T在主串S中的位置。
else
return 0;
}
效率比较
其中,求next数组的最坏时间复杂度最多是O(m)
模式匹配过程中最坏时间复杂度为O(n)
整个过程最坏时间复杂度为O(m+n)
3.2.9 ❗ ✨ 求nextval数组
举例:
第一次匹配失败位置是3,说明3位置不等于a,这时候next【3】=1,模式串第一个元素移动到i=3的位置,此时模式串T的值是a,肯定不匹配。
所以可以优化这步,这时候next[3]=1;肯定不匹配,然后又是j=1;从第一个元素匹配, next[1]肯定等于0。
所以可以直接让next[3]也等于0
这样如果第三个元素不匹配时候直接让next[3]=0,直接i++,j++
假设:
此时匹配到5时候,匹配失败,next[5]=2
还是跟上面一样,2的位置也是b,肯定匹配不成功
所以这个直接跳过,让next[5]的值直接等关于next[2]的值 1
假设 :
第六个位置匹配失败,next[6]=3,从第3个位置开始匹配。
因为第三个元素值是a,这步是没法跳过的,所以next[6]=3
总结下就是:
模式串中后面第几个元素==前面第几个元素的值 ,可以直接使用前面的next[]值
那么它对算法代码有影响吗?
用nextval数组来替代next数组,代码没变
练习: 求nextval数组
优化成nextval
-
nextval[1]=0
-
nextval[2] :
当前j所值的字符和next[j]所指的字符看相同不
不相等,这时候不变
- nextval[3]: j=3时候的字符a对应next[3]=1所指的 j=1时候字符a相同
- nextval[4]: j=4时候的字符b对应next[4]=2所指的 j=2时候字符b相同
- nextval[5]: j=5时候的字符a对应next[5]=3所指的 j=3时候字符a相同
- nextval[6]: j=6时候的字符a对应next[6]=4所指的 j=4时候字符b不相同
代码表示
nextval[1]=0;
for(int j=2;j<T.length;j++){
if(T.ch[next[j]]==T.ch[j]) //如果j对应的字符跟next[j]序号对应的字符相同
nextval[j]=nextval[next[j]]; //直接让j序号的nextval等于 next[j]序号的nextval
else
nextval[j]=next[j];
}
这也是一种优化,大家要学会
3.2.10 ❗ ✨ 参考
-
王道视频 :KMP算法