数据结构(7)—串、广义表
(mi6236)
一、串
1、串的基本概念
串(String)是字符串的简称,是由零个或多个字符组成的有限序列,记为S=“a 1a 2a 3…an”。
空串(Null string),零个字符的串,它的长度为零。
子串,串中任意个连续的字符组成的子序列称为该串的子串。
主串,包含子串的串。
串的长度,串中字符的数目n。
空格串 (blank string),由一个或多个空格组成的串''.
两个串相等当且仅当两串值相等,即长度、位置都相等。
从字符串的定义来看,字符串属于成分类型为字符的一种线性表。因此,对线性表的一切运算都能对字符串进行,从字符串的表示来看,他是字符的紧密排列,是一个有机整体,能够用他描述事物的基本属性。因此对简单类型数据所做的一切运算也都能对字符串进行,另外还可以把字符串看作为具有固定长度的字符型数组,对数组的所有运算也都适应于字符串。字符串具有简单数据、数组和线性表这三个方面的特点,同时字符串是数据处理领域重要的数据类型之一。
2、串的存储方式
定长顺序存储串的思想:
用一个字符数组存储一个串,c 语言中以‘/0'作为串的结束标志。
#define MAXSTRLEN 255 //可由用户定义
unsigned char SString[MAXSTRLEN+1];
动态分配顺序存储串的思想:
串的存储空间由malloc()函数来分配。
类型定义:
typedef struct{
char *ch;
int length;
}Hstring;
用链表存储串的思想:
例如:
类型定义:
#define SIZE 80 //可由用户定义大小
typedef struct Node {
char ch[SIZE];
struct Node *next;
}LString;
3、串的基本操作
WINXP+VC6.0环境测试
(1)、赋值:assign(S,T):把S值赋给T。
#include <stdio.h>
#define MAXSTRLEN 25
void assign(char S[],char T[])
{
for(int i=0;S[i]!='/0';i++)
T[i]=S[i];
T[i]='/0';
}
int main()
{
char string[MAXSTRLEN]="hello world";
char temp[MAXSTRLEN];
assign(string,temp);
printf("%s/n",temp);
return 0;
}
(2)、串赋值strassign(S,chars):把一个字符串常量赋给串S,即生成一个其值等于chars的串S。
#include <stdio.h>
#define MAXSTRLEN 25
void assign(char S[],char T[])
{
for(int i=0;S[i]!='/0';i++)
T[i]=S[i];
T[i]='/0';
}
int main()
{
char temp[MAXSTRLEN];
assign("hello world",temp);
printf("%s/n",temp);
return 0;
}
3、求长:求串中字符的个数。
#include <stdio.h>
#define MAXSTRLEN 25
int length(char S[])
{
for(int i=0;S[i]!='/0';i++);
return i;
}
int main()
{
int i=0;
char string[MAXSTRLEN]="hello world";
i=length(string);
printf("%d/n",i);
return 0;
}
(4)、联接concat(T, S1, S2):用T返回由S1和S2联接而成的新串。
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>
typedef struct{
char *ch;
int length;
}HString;
void concat(HString *T,HString S1,HString S2)
{
//用T返回由S1和S2联接而成的新串。
int i,j;
for(i=0;i<S1.length;i++)
*(T->ch+i)=*(S1.ch+i);
for(j=0;j<S2.length;j++)
*(T->ch+i+j)=*(S2.ch+j);
*(T->ch+i+j)='/0';
T->length=i+j;
}
int main()
{
HString S1,S2,T;
S1.ch="hello1 ";
S1.length=strlen(S1.ch);
S2.ch="hello2";
S2.length=strlen(S2.ch);
T.ch=(char*)malloc((S1.length+S2.length+1)*sizeof(char));
concat(&T,S1,S2);
printf("联接后的字符串为:%s/n",T.ch);
printf("联接后的字符串长度为:%d/n",T.length);
free(T.ch);
return 0;
}
(5)、求子串substr(S,start,length):求S从start位置开始,长度为length的子串。
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>
typedef struct{
char *ch;
int length;
}HString;
void substr(HString S,int start,int length)
{
HString temp;
temp.ch=(char *)malloc((length+1)*sizeof(char));
if(start>S.length)
{
printf("您输入的子串起始位置大于字符串的长度/n");
return;
}
if(start+length-1>S.length)
{
printf("您输入的子串长度大于字符串的长度/n");
return;
}
for(int i=0;i<length;i++)
{
temp.ch[i]=S.ch[start+i-1];
}
temp.ch[length]='/0';
temp.length=length;
printf("你所要的子串是:%s/n",temp.ch);
printf("子串的长度是:%d/n",temp.length);
free(temp.ch);
}
int main()
{
HString string;
int i,j;
printf("请输入字符串的长度:");
scanf("%d",&string.length);
string.ch=(char *)malloc((string.length+1)*sizeof(char));//注意+1否则释放空间报错。
printf("请输入字符串:");
scanf("%s",string.ch);
printf("您所输入字符串的长度为:%d/n",strlen(string.ch));
printf("你输入的字符串为:%s/n",string.ch);
printf("请输入子串的起始位置:");
scanf("%d",&i);
printf("请输入子串的长度:");
scanf("%d",&j);
substr(string,i,j);
free(string.ch);//释放空间出现错误,有时是因为使用的空间小于使用的空间.
return 0;
}
(6)、替换replace(S,t,v):以v替换所有在S中出现的和T相等的的串
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
void REPLACE(char s[],char t[],char v[])
{
unsigned int i=1; //i作为扫描主串的指针符
unsigned int j=1; //j作为扫描子串的指针符
unsigned int k=1; //k作为替换子串的指针符
unsigned int m=1; //m作为替换后主串的临时串
unsigned int count=1; //count作为主串中与子串相等的个数
char *temp=(char *)malloc((strlen(s)-strlen(t)+strlen(v))*sizeof(char));
while(i<=strlen(s))
{
if(j<=strlen(t))
{
if (*(s+i-1)==*(t+j-1)) //继续使指针分别指向下一个字符
{
*(temp+m-1)=*(s+i-1);
i++;
j++;
m++;
}
else //使指针i回溯,指针j重新指向模式的第一个字符
{
*(temp+m-1)=*(s+i-1);
m=m-j+2;
i=i-j+2;
j=1;
}
}
else //替换
{
temp=(char *)realloc(temp,(strlen(s)-strlen(t)*count+strlen(v)*count)*sizeof(char));//增加分配的空间
m=m-j+1;
//i=i-j+1;
for(k=1;k<=strlen(v);k++,m++)
{
*(temp+m-1)=v[k-1];
}
j=1;
count++;
}
}
printf("%s/n",temp);
free(temp);
}
int main()
{
char S1[]="hello world1 hello world2";
char S2[]="l";
char S3[]="XXXXXX";
REPLACE(S1,S2,S3);
return 0;
}
(7)、比较StrCompare(S,T):S,T相等返回1,否则返回0。
(8)、插入StrInsert(S,pos,T):在串S的第pos个字符之前插入串T。
(9)、删除StrDelete(S,pos,len):在串S中删除pos个字符起长度为len的子串。
上述三个串操作比较简单,不在一一列举。
4、模式匹配
串的运算是串的重点和难点,特别是顺序串上子串定位的运算。
子串定位运算又称串的“模式匹配”或“串匹配”,即在主串中查找出子串出现的位置,实际应用中非常广泛,如文本编辑中的“查找和替换”用到的是子串定位运算的算法。
在串匹配中,将主串称为目标(串),子串称为模式(串),子串始同一个模板(样本),用其在目标上从头往后比较查找,若找到和子串一样的一个连续子序列,则称匹配成功,并返回其相应的起始位置。
经典的模式匹配算法----Brute-Force的思想是:从目标串=“s0s1…sn -1” 的第一个字符开始和模式串t=“t0t1…tm -1 ” 中的第一个字符比较,若相等,则继续逐个比较后继字符;否则从目标串s的第二个字符开始重新与模式串t的第一个字符比较,依次类推。若存在模式串的每个字符依次和目标串中的一个连续字符序列相等,则匹配成功,函数返回模式串t中第一个字符在主串s中的位置;否则匹配失败,函数返回-1。
#include <stdio.h>
#include <string.h>
int index(char *S,char *T,int pos)
{
unsigned int i=pos; //i作为扫描主串的指针符
unsigned int j=1; //j作为扫描子串的指针符
while(i<=strlen(S)&&j<=strlen(T))
{
if (*(S+i-1)==*(T+j-1)) //继续使指针分别指向下一个字符
{
i++;
j++;
}
else //使指针i回溯,指针j重新指向模式的第一个字符
{
i=i-j+2;
j=1;
}
}
if(j>strlen(T))
return i-strlen(T); //S中存在T,返回起始位置
else
return -1; //S中不存在T,返回-1
}
int main()
{
char *S1="hello world";
char *S2="lle";
int pos=1;
printf("%d/n",index(S1,S2,pos));
return 0;
}
Brute-Force算法在进行模式匹配过程中,指向主串的指针经常回溯,因而在某些情况下时间复杂度高,为此提出了KMP算法。
KMP算法是由D.E.Knuth,J.H.Morris和V.R.Pratt共同提出的,所以称为Knuth-Morris-Pratt(克努特——莫里斯——普拉特)算法,简称KMP算法。该算法较Brute-Force算法有较大改进,主要是消除子主串指针的回溯,从而使算法效率有一定程度的提高。
#include <stdio.h>
#include <string.h>
#define OUTPUT( X ) printf( "position: %d/n", X+1)
#define XSIZE 20
void preKmp(char *x, int m, int kmpNext[]) {
int i, j;
i = 0;
j = kmpNext[0] = -1; /* kmpNext数组的初值 */
while (i < m) {
while (j > -1 && x[i] != x[j])
j = kmpNext[j];
i++;
j++;
if (x[i] == x[j])
kmpNext[i] = kmpNext[j];
else
kmpNext[i] = j;
}
}
void KMP(char *x, int m, char *y, int n)/* x是模式串,m是其长度;y是主串,n是其长度*/
{
int i, j, kmpNext[XSIZE];
/* Preprocessing */
preKmp(x, m, kmpNext); /* 计算模式串x中各个字符的kmpNext值*/
/* Searching */
i = j = 0; /* i和j分别是模式串和主串的下标指针 */
while (j < n) {
while (i > -1 && x[i] != y[j])
i = kmpNext[i];
/* 当比较发生“失配”时令指针i回溯到模式串x的第kmpNext[i]个字符处以便下一次比较。由于回溯到的位置仍然可能出现失配,所以要不停地回溯,直到某次出现相配或者是不能再回溯(即i=-1)为止 */
i++;
j++;
/* i和j都自增1,表示比较下一个字符。如果自增前i等于-1,那么就表示主串y中的当前字符y[j]与模式串的第一个字符x[0]不相同,所以上面的两个 自增是有必要的;如果自增前x[i]d等于y[j],那么无疑这两个字符已经不需要再比较了,所以也要令i和j自增以比较下一对字符。综上,i和j总是要 自增一个1 */
if (i >= m) {
OUTPUT(j - i);
/* 当模式串和主串中的某一部分匹配后,则输出主串位置 */
i = kmpNext[i];
/* i指针的再次回溯可以找到主串中所有剩余的模式串 */
}
}
}
int main( void )
{
char * x = "ababababc" ;
char * y = "abababababababababababababababc" ;
int m, n ;
m = strlen( x ) ;
n = strlen( y ) ;
KMP( x, m, y, n ) ;
return( 0 ) ;
}
二、广义表
1、概念:
广义表是线性表的推广,广义表的定义是一个递归定义,一般记作LS=(α1,α2,……,αn),其中αi可以是单个元素,也可以是广义表。在线性表中,αi只能是单个元素。广义表中元素个数n称为广义表的长度。
当广义表非空时,第一个元素α1称为表头(Head),其余元素称为表尾(Tail)。
在广义表的讨论中,为了把单元素同表元素区别开来,一般用小写字母表示单元素,用大写字母表示表,如:A=():A是一个空表,其长度为0;
B=(e):是一个只含有单元素e的表,其长度为1;
C=(a,(b,c,d)):C中有两个元素,一个是单元素a,另一个是表元素(b,c,d),C的长度为2;
D=(A,B,C)=((),(e),(a,(b,c,d))):D有三个元素,其中每个元素又都是一个表,D的长度为3;
E=((a,(a,b),((a,b),c))):E中只含有一个元素,该元素是一个表,该表中包含三个元素,其中后两个元素又都是表。
广义表的图形表示象倒着画的一棵树,树根结点代表整个广义表,各层树枝结点代表相应的子表,树叶结点代表单元素或空表。
一个表的深度是指该表中括号嵌套的最大次数,在图形表示中,则是指从树根结点到每个树枝结点所经过的结点个数的最大值。
2、广义表的存贮结构
广义表是一种递归的数据结构,因此很难为每个广义表分配固定大小的存贮空间,所以其贮结构只好采用动态链接结构。
第一种表结点结构:
,原子结点结构:
其中hp,tp为指针,hp指向表头,tp指向表尾。
其形式定义如下:
//---广义表的头尾链表存储表示---
typedef enum{ATOM,LIST} ElemTag; //ATOM==0:原子,LIST==1:子表
typedef struct GLNode{
ElemTag tag; //公共部分,用于区分原子结点和表结点
union{ //原子结点和表结点的联合部分
AtomType atom; //atom是原子结点的值域,AtomType由用户定义
struct {struct GLNode *hp,*tp;} ptr; //ptr是表结点的指针域,ptr.hp和ptr.tp分别指向表头和表尾
};
}*GList; //广义表类型
示例:
①A=()
②B=(e)
③C=(a,(b,c,d))
④D=(A,B,C)
⑤E=(a,E)
在一个广义表中,其数据元素有单元素和子表之分,所以在对应的存贮结构中,其存贮结点也有单元素结点和子表结点之分。对于单元素结点,应包括值域和指向其后继结点的指针域;对于子表结点,应包括指向子表中第一个结点的表头指针域和指向其后继结点的指针域。为了把广义表中的单元素结点和子表结点区别开来,还必须在每个结点中增设一个标志域,让标志域取两种不同的值,从而代表两种不同的结点。
若把整个广义表也同样用一个表结点来表示的话,则应在每个广义表的表头结点(即表中第一个结点)之前增加一个表结点(称此表结点为附加表头结点),此表结点的表头指针域指向表头结点,后继结点指针域为空,表头指针则指向这个表结点。这种带附加表头结点的广义表表示,将给广义表的某些运算带来方便。
第二种:表结点结构:
原子结点结构:
其形式如下:
//---广义表的扩展线性链表存储表示---
typedef enum{ATOM,LIST} ElemTag; //ATOM==0:原子,LIST==1:子表
typedef struct GLNode{
ElemTag tag; //公共部分,用于区分原子结点和表结点
union{ //原子结点和表结点的联合部分
AtomType atom; //原子结点的值域
struct GLNode *hp; //表结点的表头指针
};
struct GLNode *tp; //相当于线性链表的next,指向下一个元素结点
}*GList; //广义表类型GList是一种扩展的线性链表
示例:
3、广义表的运算
广义表的运算主要有求广义表的长度和深度,向广义表插入元素和从广义表中查找或删除元素,建立广义表的存贮结构,打印广义表等。由于广义表是一种递归的数据结构,所以对广义表的运算一般采用递归的算法。
(1)求广义表的深度
广义表的深度定义为广义表中括弧的重数,是广义表的一种量度。
例如:多元多项式广义表的深度为多项式中变元的个数。
设非空广义表为LS=(α1,α2,……,αn), 其中αi(i=1,2,……,n)或为原子或为LS的子表,则求LS的深度可分解为n个子问题,每个子问题为求αi的深度,若αi是原子,则由定义其深度为零,若αi是广义表,则和上述一样处理,而LS的深度为各αi(i=1,2,……,n) 的深度中最大值加1。空表也是广义表,并由定义可知空表的深度为1。
由此可见,求广义表的深度的递归算法有两个终结状态:空表和原子,且只要求得αi(i=1,2,……,n)的深度,广义表的深度就容易求得了。显然,它应比子表深度的最大值多1。
广义表LS=(α1,α2,……,αn)的深度DEPTH(LS)的递归定义为
基本项: DEPTH(LS)=1 当LS为空表时
DEPTH(LS)=0 当LS为原子时
归纳项: DEPTH(LS)=1+ Max {DEPTH(αi)} n≥1
1≤i≤n
由此定义容易写出求深度的递归函数。假设L是GList型的变量,则L=NULL表明广义表为空表,L->tag=0表明是原子。反之,L指向表结点,该结点中的hp指针指向表头,即为L的第一个子表,而结点中的tp指针所指表尾结点中的hp指针指向L的第二个子表。在第一层中由tp相连的所有尾结点中的hp指针均指向L的子表。
求广义表深度的递归函数如下面算法所示:
int GListDepth(GList L){
//采用头尾链表存储结构,求广义表L的深度。
if(!L) return 1; //空表深度为1
if(L->tag==ATOM) return 0; //原子深度为0
for(max=0,pp=L; pp; pp=pp->ptr.tp){
dep=GListDepth(pp->ptr.hp); //求以pp->ptr.hp为头指针的子表深度
if(dep>max) max=dep;
}
return max+1; //非空表的深度是各元素的深度的最大值加1
}//GListDepth
上述算法的执行过程实质上是遍历广义表的过程,在遍历中首先求得各子表的深度,然后综合得到广义表的深度。
(2) 任何一个非空广义表均可分解成表头和表尾,反之,一对确定的表头和表尾可唯一确定一个广义表。由此,复制一个广义表只要分别复制其表头和表尾,然后合成即可。假设LS是原表,NEWLS是复制表,则复制操作的递归定义为:
基本项:InitGList(NEWLS){置空表},当LS为空表时。
归纳项:COPY(GetHead(LS)->GetHead(NEWLS)) {复制表头}
COPY(GetTail(LS)->GetTail(NEWLS)) {复制表尾}
复制表的操作便是建立相应的链表。只要建立和原表中的结点一一对应的新结点,便可得到复制表的新链表。
复制广义表的递归算法如下:
Status CopyGList(GList &T,GList L){
//采用头尾链表存储结构,由广义表L复制得到广义表T。
if(!L) T=NULL; //复制空表
else{
if(!(T=(GList)malloc(sizeof(GLNode)))) exit(OVERFLOW); //建表结点
T->tag=L->tag;
if (L->tag==ATOM) T->atom=L->atom; //复制单原子
else {CopyGList(T->ptr.hp,L->ptr.hp);
//复制广义表L->ptr.hp的一个副本T->ptr.hp
CopyGList(T->ptr.tp,L->ptr.tp);
//复制广义表L->ptr.tp的一个副本T->ptr.tp
}//else
}//else
return OK;
}//CopyGList
注意,这里使用了变参,使得这个递归函数简单明了,直截了当的反映出广义表的复制过程。