目录
一、串的思维导图
二、串数据类型的定义
1、串的定义
串是由零个或者多个字符组成的有限序列。串中字符的个数称为串的长度,含有零个元素的串叫空串。在C语言中,可以用以下语句定义一个名为 str 的串:
char str[] = "abcdef";
说明:串通常用一个字符数组来表示。从这个角度来讲,数组 str 内存储的字符为'a'、'b'、'c'、'd'、'e'、'\0',其中 '\0' 作为编译器识别串结束的标记。而串内字符为'a'、'b'、'c'、'd'、'e'、'f'。因此数组str的长度为7,而串str的长度为6。
串中任意连续的字符组成的子序列称为该串的子串,包含子串的串称为主串,某个字符在串中的序号称为这个字符的位置。通常用子串第一个字符的位置作为子串在主串中的位置。要注意的是,空格也是串字符集合的一个元素,由一个或者多个空格组成的串称为空格串(注意,空格串不是空串)。串的逻辑结构和线性表类似,串是限定了元素为字符的线性表。从操作集上讲,串和线性表有很大的区别,线性表的操作主要针对表内的某一个元素,而串操作主要针对串内的一个子串。
求串的子串个数:一个串有n个元素(每个元素都不相同),则其子串的个数为:1+2+...+n+1(空串)= n(n+1)/2 + 1
2、串的存储结构
(1)定长顺序存储表示
定义并初始化一个串一般不采用字符数组的方式,如:
char str[] = "abcdef"; //数组 str 内存储的字符为'a'、'b'、'c'、'd'、'e'、'\0'
原因是串的结束标志是 '\0' ,如果想要求串的长度,则需要扫描整个串,时间复杂度为O(n),这样就不如额外定义一个变量来存储串的长度,这样求串的长度就变成了时间复杂度为O(1)的操作。
定长顺序存储表示表示的结构体定义如下:
typedef struct {
char str[maxSize + 1]; //maxSize为已经定义的变量,表示串的最大长度;
//str数组的长度定义为maxSize+1,是因为串的结束标记'\0'
int length; //存储当前串的长度
}Str;
(2)变长分配存储表示
变长分配存储表示(动态分配存储表示)方法的特点是,再程序执行过程中根据需要动态分配,其结构体如下:
typedef struct {
char *ch; //指向动态分配存储区首地址的字符指针
int length; //存储当前串的长度
}Str;
这种存储方式在使用时,需要用函数mallo()来分配一个长度为length、类型为char至的连续存储空间,分配的空间可以用函数free0释放掉。用函数malloc()分配存储空间如果成功,则返回一个指向起始地址的指针,作为串的基地址,这个地址由 ch 指针来指向:如果分配失败,则返回NULL。
通过对比以上两种存储结构的分配方法可以看出,变长分配存储表示方法有顺序存储结构的特点、操作中串长可根据需要来设定,更加灵活,因此在串处理的应用程序中更为常用。
3、串的基本操作
(1)赋值操作
与普通变量赋值操作不同,串的赋值操作不能直接用 “=” 来实现。因为串是一个数组,如果将一个数组的值赋值给另一个数组,则直接用 “=” 是不行的,必须对数组中的每个元素进行逐一赋值操作,串赋值操作函数strassign()具体实现如下,其功能是将一个常量字符串赋值给 str,赋值成功返回 1,否则返回 0;
int strassign(Str &str,char *ch){
if(str.ch)
free(str.ch); //释放原串空间
int len = 0;
char *c = ch;
while(*c){ //求ch串的长度
++ len;
++ c;
}
if(len == 0){ //如果ch串是空串
str.ch = NULL;
str.length = 0;
return 1;
}
else{
str.ch = (char*)malloc(sizeof(char)*(len + 1)); //len + 1是为了多加上'\0'
if(str.ch == NULL) //内存开辟失败
return 0;
else{
c = ch;
for(int i=0;i<=len;++i,++c) //i<=len是为了将'\0'复制到新串中
str.ch[i] = *c; //*c代表取值
str.length = len;
return 1;
}
}
}
函数strassign()的使用格式如下:
strassign(str,"hello world");
此句执行后,str.ch 的值为 "hello world",str.len 的值为 11;
(2)取串长度操作:变长分配存储的情况下:
int strlength(Str str){
return str.length;
}
(3)串的比较:规则如下:设两串A和B中的待比较字符分别为a和b,如果a的ASCⅡ码小于b的ASCIⅡ码,则返回A小于B标记;如果a的ASCⅡ码大于b的ASCⅡ码,则返回A大于B标记;如果a的ASCⅡ码等于b的ASCⅡ码,则按照之前的规则继续比较两串中的下一对字符,经过上述步骤,在没有比较出A和B大小的情况下,先结束的串为较小串,两串同时结束则返回两串相等标记。串比较代码如下:
int strcompare(Str s1,Str s2){
for(int i=0;i<s1.length && i<s2.length;i++)
if(s1.ch[i]!=s2.ch.ch[i]) //如果两个串中对应的位置字符不同
return s1.ch[i] - s2.ch[i];
return s1.length - s2.length;
}
(4)串的连接操作:将两个串首尾连接,合并成一个字符串的操作称为串的连接操作。代码如下:
int concat(Str &str,Str str1,Str str2){//将str1和str2合并成一个str
if(str.ch){ //释放str串的空间
free(str.ch)
str.ch = NULL;
}
str.ch = (char*)malloc(sizeof(char)*(str1.length + str2.length + 1))
if(str.ch == NULL) //内存开辟失败
return 0;
int i = 0;
while(i<str1.length){ //先将str1赋值进str
str.ch[i] = str1.ch[j];
++i;
}
int j = 0;
while(j<=str2.length){ //再将str2赋值进str 这里用<=是为了最后的'\0'
str.ch[i+j] = str2.ch[j];
++j;
}
str.length = str1.length + str2.length;
return 1;
}
(5)求子串操作:求从给定串中某一个位置开始到某一个位置结束的串的操作称为求子串操作(规定开始位置总是再结束位置的前面),如下面的代码中实现了求 str 串中从pos位置开始,长度为len的子串,子串有 substr 返回给用户。
int substring(Str &substr,Str str,int pos,int len){
if(pos<0||pos>=str.length||len<0||len>str.length-pos) //子串的选取位置
return 0;
if(substr.ch){ //释放substr的空间
free(substr.ch);
substr.ch = NULL;
}
if(len == 0){ //如果选取的串为空串
substr.ch = NULL;
substr.length = 0;
return 1;
}else{
substr.ch = (char*)malloc(sizeof(char)*(len+1)); //给substr分配一个len+1的内存空间
int i = pos;
int j = 0;
while(i<pos+len){ //取子串操作
substr.ch[j] = str.ch[i];
++i;
++j;
}
substr.ch[j] = '\0'; //结束字符'\0'
substr.length = len;
return 1;
}
}
(6)串清空操作:
int clearstring(Str &str){
if(str.ch){
free(str.ch); //释放原串空间
str.ch = NULL;
}
str.leng = 0;
return 1;
}
三、串的模式匹配算法
1、简单模式匹配算法
对一个串中某子串的定位操作称为串的模式匹配,其中定位的子串称模式串。算法的基本思想:从主串的第一个位置起和模式串的第一个字符开始比较,如果相等,则继续逐一比较后续字符;否则从主串的第二个字符开始,再重新用上一步的方法与模式串中的字符做比较,以此类推,直到比较完模式串中的所有字符。若匹配成功,则返回模式串在主串中的位置;若匹配不成功,则返回一个可区别于主串所有位置的标记,如“0”。算法实现代码如下:
int index(Str str,Str substr){
int i=1,j=1,k=i;
while(i<=str.length && j<=substr.length){
if(str.ch[i] = substr.ch[j]){
++i;
++j;
}else{
j = 1;
i = ++k; //匹配失败,i从主串的下一个位置开始,k记录了上一次的起始位置
}
}
if(j>substr.length) //匹配成功
return k; //返回子串位置
else
return 0;
}
2、KMP算法
KMP算法是对简单的模式匹配算法的一个改进,i 不需要回溯,这就意味着对于较大规模的外存中字符串的匹配操作可以分段进行,先读入内存一部分进行匹配,完成之后即可写回外存,确保在发生不匹配时不需要将之前写回外存的部分再次读入,减少了I/O操作,提高了效率。KMP算法的具体实现代码如下:
(1)KMP算法:
int index(Str str,Str substr){
int i=1,j=1,k=i;
while(i<=str.length && j<=substr.length){
if(str.ch[i] = subsstr.ch[j]){
++i;
++j;
}else{ //匹配失败,i不变,j赋值为其next函数的值
j = next[j];
}
}
if(j>substr.length) //匹配成功
return i-substr.length; //返回i子串的位置
else
return 0;
}
(2)求next数组的算法:KMP算法利用已经部分匹配的结果而加快模式串的滑动速度,这是用到了next函数:
void getnext(Str substr,int &next[]){
int i=1,j=0;
next[1] = 0;
while(i<substr.length){
if(j==0||substr.ch[i]==substr.ch[j]){
++i;
++j;
next[i] = j;
}else
j = next[j];
}
}
3、KMP算法的改进
有一种特殊情况,假如模式串中前几个元素都相同,则用KMP算法进行匹配时会出现一些问题。这个时候就需要对next函数进行改进,将其对应的next值修正为nextval。总结求nextval的一般步骤如下:
(1) 当 j = i 时,nextval[j]赋值为0,作为特殊标记。
(2) 当 Pj != Pk 时(k = next[j]),nextval[j] 赋值为k。
(3) 当 Pj = Pk 时(k = next[j]),nextval[j] 赋值为nextval[k]。
将getnext函数进行改进,最终得到的nextval的代码如下:
void getnext(Str substr,int &nextval[]){
int i=1,j=0;
nextval[1] = 0;
while(i<substr.length){
if(j==0||substr.ch[i]==substr.ch[j]){
++i;
++j;
if(substr.ch[i]!=substr.ch[j])
nextval[i] = j;
else
nextval[i] = nextval[j];
}else
j = nextval[j];
}
}