引言
计算机处理的信息中,很大一部分包括字符串变量的处理。字符串变量有时候也简称为串。与链表、栈、队列结构的学习过程类似,在学习串的过程中,需要先了解其基本结构,然后是基本操作函数(例如插入删除修改等)。不同的是,由于串这个名词含义的复杂性,导致其本身就有着多种的表示方式。所以,在学习串时,有必要掌握它的三种表示方式:定长顺序存储表示、堆分配存储表示以及块链顺序存储表示。在表示方式后,需要掌握的是不同表示方式下的操作。
正文
一、串的定长顺序存储表示
与顺序表的定义类似,使用定长存储串只需要分配一片连续的内存,即定义一个字符串类型的数组来存储字符即可完成在连续内存区域中存储的目的。在设置长度的时候,需要多留一位(字符串首位)以存储字符串的长度。该类表示方式的代码如下:
#include <iostream>
#define max 100
using namespace std;
typedef char S[max+1];
在定长存储表示下,可以直接通过访问数组中的下标来访问字符串数组中的任意字符。这种访问方式有两种最直观的应用:串连接以及求子串。
(1)串连接操作
串连接操作的整体思路是,将两个定长为L1,L2的字符串数组S1,S2通过串值复制操作复制到新建的定长为L3的字符串数组S3中,当L1+L2>L3时,需要将串S2的一部分截断,若L1>L3时,需要将串S1的一部分截断。示意图如下所示:
字符串连接代码如下所示:
void Contact(char s1[101],char s2[101],char s3[101],int l1,int l2)
{
if(l1+l2<=max)
{
for(int i=1;i<=l1;i++)
{
s3[i]=s1[i];
}
for(int j=1;j<=l2;j++)
{
s3[l1+j]=s2[j];
}
s3[0]=(char)(l1+l2);
}
else if(l1<max)
{
for(int i=1;i<=l1;i++)
{
s3[i]=s1[i];
}
for(int k=l1+1;k<=max;k++)
{
s3[k]=s2[k-l1];
}
s3[0]=(char)max;
}
else{
for(int j=0;j<max;j++)
{
s3[j]=s1[j];
}
s3[0]=(char)max;
}
}
补充两个函数:1)初始化函数Init,初始化函数主要有两个步骤,包括输入字符串的字符数目以及输入字符,先定义新字符数组s,输入字符数目l。再通过for循环将字符打入字符数组内,字符数组的首位赋值为字符数目l。代码如下:
void Init(char s[101],int l)
{
s[0]=(char)l;
for(int i=1;i<=l;i++)
{
cin>>s[i];
}
s[l+1]='\0';
}
2)显示函数show,显示函数意为先通过strlen()函数求出字符串数组的长度,再由下标为1开始的for循环将字符挨个输出,代码如下:
void show(char s[101])
{
int l=strlen(s);
for(int i=1;i<=l;i++)
{
cout<<s[i];
}
cout<<endl;
cout<<l<<endl;
}
这两个函数的主要目的是初始化字符数组以及检验字符的算法成果,在之后字符串的应用中也会多次使用到。
(2)求子串操作
对比串连接操作而言,求子串操作其实相对简单,主要的过程就是从母串下标为pos开始,复制长度为len的字符数组进入新的字串sub中,完成的仅仅只是一个字符串的复制过程。具体代码如下:
void Sub(char s0[101],char s1[101],int pos,int len)
{
if(pos<1||pos>s1[0]||len<0||len>s1[0]-pos+1)
{
cout<<"Wrong"<<endl;
}
else
{
for(int i=1;i<=len;i++)
{
s0[i]=s1[pos+i-1];
}
s0[0]=len;
s0[pos+len]='\0';
}
}
二、堆分配存储表示
由串定长分配存储的结构可以看出,在定长分配计算的过程中,如果出现字符串长度超过设定的母串最大长度时,必须要进行截取超出长度的处理。这就意味着,操作的复杂度会不断增加。为了改进这种情况,于是就出现了堆分配存储这一种串的存储方式。堆分配存储的直接概念是:以一段连续分配的地址作为存储字符串的地址,与定长不同的是,要求使用malloc()与free()函数进行内存的动态添加与删除。如果分配地址成功,要求返回一个指向起始地址的指针ch,方便进行后续内存的访问。
串的堆分配存储代码如下:主要新建一个名为Hstring的结构体,结构体内部定义一个返回类型为char型的指针以及int型变量length。
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
using namespace std;
typedef struct
{
char *ch;
int length;
}Hstring;
同之前的学习经验类似,在了解了串的堆分配结构后,开始了解其基本操作。首先是串的初始化函数。特别注意的是,串的初始化函数其实等同于生成一个值等于某个串常量的新串。这个操作本质上分为两步,首先是内存的分配,其次便是字符的复制。内存的分配首先要求出串的长度l,接着利用for循环通过控制下标赋值将单个字符按顺序复制,此处不同于串的定长存储,首位不需要放置长度。串的初始化代码如下:
void Init(Hstring *h,char *chars)
{
int i=0;
int len=0;
if(h->ch)
{
free(h->ch);
}
for(i=0;chars[i]!='\0';i++);
len=i;
if(!i)
{
h->ch='\0';
h->length=0;
}
else
{
h->ch=(char*)malloc(len*sizeof(char));
for(i=0;i<len;i++)
{
h->ch[i]=chars[i];
}
h->length=len;
}
h->ch[i]=chars[i];
h->length=i;
}
类似地,在定义了串的初始化操作后,需要完成的是串的显示操作,首先明确定义的显示操作对象为一个Hstring类型的变量记作h,可以访问其内部的指针ch(ch的返回类型为字符型),由指针的知识易得,通过for循环语句控制下标可以访问指针所指内存的下一段内存。通过赋值语句(将指针所指的内存的返回量赋值给字符)使得字符被输出。
void show(Hstring *h)
{
for(int i=0;i<h->length;i++)
{
cout<<h->ch[i];
}
cout<<endl;
}
在完成了初始化操作以及显示操作后,开始定义串的字典序比较,求子串,连接函数。首先是串的字典序比较,字典序比较的主要特点是:首先定义两个Hstring类型的串,记作s与h。通过for循环控制两个串的下标i,通过i的增减来访问每一个字符。返回值用两者第一个不同的字符的字典序之差来表示。代码如下:
int compare(Hstring*s,Hstring *h)
{
for(int i=0;i<s->length&&i<h->length;i++)
{
if(s->ch[i]!=h->ch[i])
{
return s->ch[i]-h->ch[i];
break;
}
}
return s->length-h->length;
}
接下来是串的求子串操作,与定长存储分配时的算法相同,先进行条件的判断,判断完成后进行一个字符串的求子串操作过程,本质上就是通过for循环控制下标,将特定顺序的字符复制到新串中去。并用一个新的返回类型为字符的指针去返回。代码如下:
void Substring(Hstring *sub,Hstring *s,int pos,int len)
{
if(sub->ch)
{
free(sub->ch);
}
if(pos<0||pos>s->length||len<=0||len>s->length-pos+1)
{
cout<<"Wrong"<<endl;
}
if(!len)
{
sub->ch=NULL;
sub->length=0;
}
else
{
sub->ch=(char*)malloc(len*sizeof(char));
for(int i=0;i<=len-1;i++)
{
sub->ch[i]=s->ch[pos+i-1];
}
sub->length=len;
sub->ch[len]='\0';
}
}
串连接操作与定长存储时基本相同,只是不需要考虑固定长度时对字符串截取的影响,只需要给其分配正确长度的空间即可。主要操作就是通过for循环控制下标对字符串进行复制。具体代码如下:
void contact(Hstring *h,Hstring *s1,Hstring *s2)
{
if(h->ch)
{
free(h->ch);
}
h->ch=(char*)malloc((s1->length+s2->length)*sizeof(char));
for(int i=0;i<s1->length;i++)
{
h->ch[i]=s1->ch[i];
}
for(int j=0;j<s2->length;j++)
{
h->ch[s1->length+j]=s2->ch[j];
}
h->length=s1->length+s2->length;
}
三、串的块链存储表示
串的块链存储表示,直白地说,就是用链表来表示串,存储串的信息。在串的定长存储与堆分配存储中,串的一个个字符都是作为单个的单元或者整体连续的内存开始存储的。在串的第三种存储方式,即块链存储表示中由于链表的结构因素,使得每一个节点会由不同的字符所组成,显而易见地,节点中的字符数目也不一定相同。由于串的长度不一定是节点大小的整倍数,则链表的最后一个节点不一定全部都是字符,相对地,可以由‘#’符号进行补齐。 需要注意的是,以链表存储串值时,需要同时设置一个尾指针来指示链表中的最后一个节点。基本结构代码以及示意图如下,(定义的CHUNKSIZE变量为单个节点中的字符数目,同时用变量blank代替字符‘#’):
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
using namespace std;
#define CHUNKSIZE 5
char blank ='#';
typedef struct Chunk
{
char ch[CHUNKSIZE];
struct Chunk *next;
}chunk;
typedef struct
{
Chunk *head,*tail;
int curlen;
}LString;
串的块链存储的基本操作如下:首先是初始化操作Init(LString *t),将指针t返回的对象的head指针与tail指针均初始化为NULL,同时将t所指结点的长度设为0,代码如下:
void Init(LString *t)
{
(*t).head=(*t).tail=NULL;
(*t).curlen=0;
}
与串的堆分配存储学习过程类似,在了解了串的初始化操作后,需要了解的是串的生成相同子串操作。整体操作的过程为新建一个返回LString为类型的指针t,并将字符串赋值给该指针所指的内存(相当于生成一个值等于串常量chars的串t)。在串的块链存储过程中,基本思路如下:1)进行条件的判断,如果字符串chars的长度l等于0或者字符串中出现了‘#’符号,则输出错误指示 2)求出节点的数量j,如果字符串长度(字符数目)是单个节点的大小的整数倍时,则节点的数量j等于l/size,若不是整数倍,则j的数量需要加1 3)在求出节点的数目j后,定义块链类型的指针p,q,通过for循环为指针p分配空间,初始化的p与q指针均指向头结点,每新建一个指针所指向的节点p,便将q结点的后继记作p节点,再将q节点后移一位。 4)通过for循环填补每个节点中的字符 5)如果此时chars指针的返回值为空(即已经没有字符可以输入节点了),则将结点中剩余的位置填补上‘#’字符 6)以上的步骤均是在j次for循环中每一步所实现的,等同于j个节点每一次都需要实现一次“新分配结点,后继结点的设立,填补字符”的步骤。 代码如下:
void StrAssign(LString *t,char *chars)
{
Chunk*p,*q;
int j,k,m;
int l=strlen(chars);
if(!l||strchr(chars,blank))
{
cout<<"Wrong"<<endl;
}
(*t).curlen=l;
j=l/CHUNKSIZE;
if(l%CHUNKSIZE)
{
j++;
}
for(k=0;k<j;k++)
{
p=(Chunk*)malloc(sizeof(Chunk));
if(!p)
{
cout<<"Wrong"<<endl;
}
if(k==0)
{
q=p;
(*t).head=q;
}
else
{
q->next=p;
q=p;
}
for(m=0;m<CHUNKSIZE&&*chars;m++)
{
*(q->ch+m)=*chars++;
}
if(!*chars)
{
(*t).tail=q;
q->next=NULL;
for(;m<CHUNKSIZE;m++)
{
*(q->ch+m)=blank;
}
}
}
}
串的截取子串操作是极其重要的一项操作,截取子串的基本过程就是在定义了初始的字符位序以及字符串长度后,将原串s中的字符复制到新串t中。基本思路如下: 1)先进行条件的判断,如果输入的pos值或Len值不符合要求,则输出为错误 2)确定结点的数目为n 3)新建头结点p以及n-1个剩余的结点q,将q按顺序连接,p不停赋值等于q,最后使结点p指向NULL,并使串t的尾指针指向p 4)最后一个结点剩余空位均用字符‘#’来填补 5)定义变量m与i,m用来表示在串t中的字符的位序,与字符处在的结点的位序i需要分开考虑。i的意义在于,表示了一个字符在每一个结点内的位序,而这个位序是不能大于CHUNKSIZE的 6)定义一个flag变量,当flag为1时,可以直接进行字符的复制,而不需要通过for循环来控制数目为n的结点访问.访问下一个结点的要求为i=CHUNKSIZE,字符串赋值的语句为*(q->ch+i)=*(p->ch+k); 代码如下:
void Substring(LString*t,LString s,int pos,int len)
{
Chunk *p,*q;
int n,i,flag=1;
if(pos<1||pos>s.curlen||len<0||len>s.curlen-pos+1)
{
cout<<"Wrong"<<endl;
}
n=len/CHUNKSIZE;
if(len%CHUNKSIZE)
{
n++;
}
p=(Chunk*)malloc(sizeof(Chunk));
(*t).head=p;
for(i=1;i<n;i++)
{
q=(Chunk*)malloc(sizeof(Chunk));
p->next=q;
p=q;
}
p->next=NULL;
(*t).tail=p;
(*t).curlen=len;
for(i=len%CHUNKSIZE;i<len;i++)
{
*(p->ch+i)=blank;
}
q=(*t).head;
i=0;
int m=0;
p=s.head;
while(flag)
{
for(int k=0;k<CHUNKSIZE;k++)
{
if(*(p->ch+k)!=blank)
{
m++;
if(n>=pos&&n<=pos+len-1)
{
if(i==CHUNKSIZE)
{
q=q->next;
i=0;
}
*(q->ch+i)=*(p->ch+k);
i++;
if(m==pos+len-1)
{
flag=0;
break;
}
}
}
}
p=p->next;
}
}
在掌握了求子串以及子串复制生成操作后,需要掌握其最后一个操作,即子串的连接。其主要的思路就是将LString s 与LString t两个子串进行串的复制,完成后进行串的连接。在字符串连接操作之前,首先需要完善字符串的复制函数Strcopy()。基本操作思路如下:1)要实现的过程为将串s的内容复制到串t之中,先定义串指针h,将其初始化为s的头指针,定义返回类型为Chunk的指针p,q,同时赋值上对应的长度(将s的长度赋给t) 2)将p作为每一个新建过程中新建的结点,每次循环与q指针交互赋值(将新的p指针所指结点连接至q指针后,再将q指针后继指为p) 3)通过while循环,当指示串s的指针h不为空时,将h指针的返回字符复制给p指针所指的内存 4)在循环完成后,将p指针所指结点的后继结点赋值为NULL,同时将p指针的结点定义为串t的尾指针指向的结点。 基本代码如下:
void Strcopy(LString *t,LString s)
{
Chunk *h=s.head;
Chunk *p,*q;
(*t).curlen=s.curlen;
if(h!=NULL)
{
(*t).head=(Chunk*)malloc(sizeof(Chunk));
p=(*t).head;
*p=*h;
h=h->next;
while(h)
{
q=p;
p=(Chunk*)malloc(sizeof(Chunk));
q->next=p;
*p=*h;
h=h->next;
}
p->next=NULL;
(*t).tail=p;
}
else
{
cout<<"Wrong"<<endl;
}
}
定义完成串的复制操作后最终串的连接操作也相对可以完成,基本思路如下:1)明确条件下基本操作为将串s1,s2复制并且连接为新串t 2)定义串a1,a2,并将s1,s2复制给串a1,a2 3)新串t的长度为a1a2的长度之和 4)新串t的头指针与a1的头指针相同,尾指针与a2的尾指针相同 5)定义a1的尾指针的后继为a2的头指针 6)借此可以直接通过访问指针t来达到访问s1,s2连接的结果。
void StrContact(LString *t,LString s1,LString s2)
{
LString a1,a2;
Init(&a1);
Init(&a2);
Strcopy(&a1,s1);
Strcopy(&a2,s2);
(*t).curlen=a1.curlen+a2.curlen;
(*t).head=a1.head;
a1.tail->next=a2.head;
(*t).tail=a2.tail;
}