串
串的逻辑结构
串,即字符串(String),是由零个或多个字符组成的有限序列。一般记为 S = ′ a 1 a 2 a 3 . . . a n ′ ( n > = 0 ) S='a_1a_2a_3...a_n'(n>=0) S=′a1a2a3...an′(n>=0)
其中,S是串名,单引号括起来的字符序列是串的值;串中字符的个数n称为串的长度。n=0时的串称为空串
Eg:
S=“HelloWorld!”
T=‘iPhone 15 Pro Max?’
主串
包含字串的串。 Eg:T是字串’iPhone’的主串
字串
串中任意个连续的字符组成的子序列。 Eg:‘iPhone’,'Pro Max’是串T的子串。
字符在主串中的位置:字符在串中的序号。 Eg:'1’在T中的位置是8(第一次出现的位置)
字串在主串中的位置:子串的第一个字符在主串中的位置。 Eg:'15 Pro’在T中的位置为8
字符集
字符集:所有字符的集合
常见的字符集:ASCII字符集(英文字符)、Unicode字符集(中英文字符)
编码方式:将字符当作自变量x,将每个字符所对应的二进制编码作为因变量y,则y=f(x)中f即为编码方式,通过f映射将x编码为y。采用不同编码方式编码会导致每个字符在计算机中所占的空间大小不同。
Eg:ASCII码采用8位二进制数来表示字符,所以ASCII在计算机中所占空间大小为1b。
UniCode采用16位二进制数来表示,所以占空间大小位2B,即1word
乱码:由于编码方式的不同,若给出一段二进制码,不同的解码方式解出来的字符串也是不同的。
Eg:若在某个编辑器中的编码方式为:y=f(x)
则其对应的解码方式应为:x=f-1(y)
出现乱码的情况可能为你使用了与编码方式不匹配的解码方式,即x=g-1(y)
串的运算(基本操作)
假设有串T=‘’,S=‘iPhone 15 Pro Max?’,W=‘Pro’
- StrAssgin(&T,chars):赋值操作。把串T赋值为chars。
- StrCopy(&T,S):复制操作。由串S复制得到串T。
- StrEmpty(S):判空操作。
- StrLength(S):求串长。
- ClearString(&S):清空操作。将S清为空串。
- DestroyString(&S):销毁串。将串S销毁(回收存储空间)。
- Concat(&T,S1,S2):串连接。用T返回由S1和S2连接而成的新串。
- SubString(&Sub,S,pos,len):求子串。用Sub返回串S的第pos个字符长度len的子串。
- Index(S,T):定位操作。若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置;否则函数值为0。
存储结构
串是一种特殊的线性表,串的数据对象限定为字符。
串的基本操作,如增删改查是以子串为操作对象
定长顺序存储
存储方式
采用静态数组来存储
#define MAXLEN 256 //预定义最大串长为255
typedef struct SString{
char ch[MAXLEN];
int length
}SString;
基本操作的实现
赋值、复制、判空、求串长、清空、销毁、串连接
// 赋值操作
bool StrAssign(SString &T,char chars){
if(T.length>=MAXLEN-1) return false;
else{
T.ch[++T.length]=chars;
return true;
}
}
// 复制
void StrCopy(SString &T,SString S){
T.length=S.length;
for(int i=1;i<=S.length;i++){
T.ch[i]=S.ch[i];
}
}
// 判空操作
bool StrEmpty(SString S){
if(S.length==0) return true;
else return false;
}
//求串长
int StrLength(SString S){
return S.length;
}
//清空操作
void ClearString(SString &S){
S.length=0;
}
// 销毁串
void DestroyString(SString &S){
S.length=0;
free(S.ch);
}
//串联接
void Concat(SString &T,SString S1,SString S2){
T.length=S1.length+S2.length;
StrCopy(T,S1);
for(int i=S1.length+1;i<=S2.length;i++)
{
T.ch[i]=S2.ch[i];
}
}
⭐串的模式匹配
//求子串,用Sub返回串S的第pos个字符起长度为len的子串
bool SubString(SString &Sub,SString S,int pos,int len){
if(pos+len-1 > S.length) return false; //超过串的长度
Sub.length=0;
for(int i=pos;i<pos+len;i++){
StrAssign(Sub,S.ch[i]);// 插入操作
}
return true;
}
// 王道📕上该方法的实现
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;
}
// 串的比较操作
int StrCompare(S,T){
for(int i=1;i<=S.length&&i<=T.length;i++)
if(S.ch[i]!=T.ch[i]) //如果两个字符不相等,直接返回两字符串的差值,若返回值大于0,则S>T.
return S.ch[i]-T.ch[i];
//扫描过的所有字符都相等
return S.length-T.length;
}
// 定位操作,若主串S中存在与串T相同的子串,则返回它在主串中第一次出现的位置
int Index(S,T){
SString Sub;
for(int i=1;i<S.length-T.length+1;i++){
SubString(Sub,S,i,T.length);
if(StrCompare(T,Sub)==0)
return i;
return 0;
}
}
堆分配存储
采用动态数组来存储
typedef struct HString{
char *ch;
int length;
}HString;
HString S;
S.ch=(char*)malloc(MAXLEN*sizeof(char)); //分配一段连续的空间
S.length=0;
**attention!**此类方法需要分配空间,使用完后要手动free释放空间
块链存储
定长顺序存储和堆分配存储通常为高级程序设计语言所采用,块链存储不常用
存储方式
typedef struct StringNode{
char ch; // 每个节点存一个字符
struct StringNode *next;
}StringNode,*String;
KaTeX parse error: \hline valid only within array environment
这样存储的话,每个元素所占的空间大小为1b,在32位机中*next所占大小为4b,有效数据所占比例过小,存储密度太低!!!
所以我们可以采用另一种方式
typedef struct StringNode{
char ch[4]; //每个结点存4个字符,这样数据存储占4b,*next占4b,存储密度就增大了
struct StringNode *next;
}StringNode, *String;
⭐模式匹配算法
暴力匹配算法(朴素模式匹配算法)
int Index(SString S,SString T){
int i=1,j=1;
while(i<=S.length&&j<=T.length){
if(S.ch[i]==T.ch[i]){
i=i+1; //如果两字符相同,则i和j同时后移
j=j+1;
}
else{
i=i-j+2; // i一共和j走了j步,遇到不匹配的返回这j步之前,再加2到当前匹配串的下一个串头
j=1; //j从1开始重新匹配
}
}
if(j>T.length)
return i-T.length; //现在i在匹配好的后一个字符上,减去要匹配的字符长度,就是匹配完成的字符的位置。
else return 0; //没有匹配到
}
⭐时间复杂度:O(nm)
分析:最坏的情况就是每一次的匹配都要对比m个字符串,共n-m+1个子串,所以复杂度应为O(m(n-m+1))=O(nm)(在实际应用中主串n的长度要远远大于子串m的长度)
⭐⭐⭐KMP算法
详情请看书本P121和纸质手稿!!
算法代码实现:
int Index(SString S,SString T,int next[]){
int i=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;
}
next数组的求法代码:
void get_next(SString T,int next[]){ //在这段代码中其实就是模式串的自身匹配,而j就代表了i-1个长度的字符串中前后缀相等的子串的最大长度+1
int i=1,j=0;
while(i<=T.length){
if(j==0||T.ch[i]==T.ch[j]){
++i;
++j;
next[i]=j;
}
else{
j=next[j];
}
}
}
时间复杂度为O(m+n)
KMP算法的进一步改进———nextval数组
优化基本思想(代码体现)
nextval[1]=0;
for(int i=2;i<=T.length;i++){
if(T.ch[i]==T.ch[next[i]]){
nextval[i]=nextval[next[i]];
}
else{
nextval[i]=next[i];
}
}
源代码实现
void get_nextval(SString T,int nextval[]){
int i=1;j=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]=nextval[j]; //⭐如果T.ch[i]=T.ch[j] 若到i处失配,那么到j处一定也是失配的,所以直接到j的nextval处就行!
else nextval[i]=j;
}
else{
j=nextval[j];
}
}
}