前面讲过,数组即可以存储不可再分的数据元素(如数字 5、字符 'a'),也可以继续存储数组(即 n 维数组)。
但需要注意的是,以上两种数据存储形式绝不会出现在同一个数组中。例如,我们可以创建一个整形数组去存储 {1,2,3},我们也可以创建一个二维整形数组去存储 {{1,2,3},{4,5,6}},但数组不适合用来存储类似 {1,{1,2,3}} 这样的数据。
有人可能会说,创建一个二维数组来存储{1,{1,2,3}}。在存储上确实可以实现,但无疑会造成存储空间的浪费。
对于存储 {1,{1,2,3}} 这样的数据,更适合用广义表结构来存储。
广义表,又称列表,也是一种线性存储结构。同数组类似,广义表中既可以存储不可再分的元素,也可以存储广义表,记作:
LS = (a1,a2,…,an)
其中,LS 代表广义表的名称,an 表示广义表存储的数据。广义表中每个 ai 既可以代表单个元素,也可以代表另一个广义表。
广义表的原子和子表
通常,广义表中存储的单个元素称为 "原子",而存储的广义表称为 "子表"。
例如创建一个广义表 LS = {1,{1,2,3}},我们可以这样解释此广义表的构成:广义表 LS 存储了一个原子 1 和子表 {1,2,3}。
以下是广义表存储数据的一些常用形式:
- A = ():A 表示一个广义表,只不过表是空的。
- B = (e):广义表 B 中只有一个原子 e。
- C = (a,(b,c,d)) :广义表 C 中有两个元素,原子 a 和子表 (b,c,d)。
- D = (A,B,C):广义表 D 中存有 3 个子表,分别是A、B和C。这种表示方式等同于 D = ((),(e),(b,c,d)) 。
- E = (a,E):广义表 E 中有两个元素,原子 a 和它本身。这是一个递归广义表,等同于:E = (a,(a,(a,…)))。
注意,A = () 和 A = (()) 是不一样的。前者是空表,而后者是包含一个子表的广义表,只不过这个子表是空表。
广义表的表头和表尾
当广义表不是空表时,称第一个数据(原子或子表)为"表头",剩下的数据构成的新广义表为"表尾"。
强调一下,除非广义表为空表,否则广义表一定具有表头和表尾,且广义表的表尾一定是一个广义表。
例如在广义表中 LS={1,{1,2,3},5} 中,表头为原子 1,表尾为子表 {1,2,3} 和原子 5 构成的广义表,即 {{1,2,3},5}。
再比如,在广义表 LS = {1} 中,表头为原子 1 ,但由于广义表中无表尾元素,因此该表的表尾是一个空表,用 {} 表示。
广义表存储结构
由于广义表中既可存储原子(不可再分的数据元素),也可以存储子表,因此很难使用顺序存储表示,通常情况下广义表结构采用链表实现。
使用顺序表实现广义表结构,不仅需要操作 n 维数组(例如 {1,{2,{3,4}}} 就需要使用三维数组存储),还会造成存储空间的浪费。
使用链表存储广义表,首先需要确定链表中节点的结构。由于广义表中可同时存储原子和子表两种形式的数据,因此链表节点的结构也有两种,如图 1 所示:
图 1 广义表节点的两种类型
如图 1 所示,表示原子的节点由两部分构成,分别是 tag 标记位和原子的值,表示子表的节点由三部分构成,分别是 tag 标记位、hp 指针和 tp 指针。
tag 标记位用于区分此节点是原子还是子表,通常原子的 tag 值为 0,子表的 tag 值为 1。子表节点中的 hp 指针用于连接本子表中存储的原子或子表,tp 指针用于连接广义表中下一个原子或子表。
因此,广义表中两种节点的 C 语言表示代码为:
typedef struct GLNode{
int tag;//标志域
union{
char atom;//原子结点的值域
struct{
struct GLNode * hp,*tp;
}ptr;//子表结点的指针域,hp指向表头;tp指向表尾
}subNode;
}*Glist;
这里用到了 union 共用体,因为同一时间此节点不是原子节点就是子表节点,当表示原子节点时,就使用 atom 变量;反之则使用 ptr 结构体。
例如,广义表 {a,{b,c,d}} 是由一个原子 a 和子表 {b,c,d} 构成,而子表 {b,c,d} 又是由原子 b、c 和 d 构成,用链表存储该广义表如图 2 所示:
图 2 广义表 {a,{b,c,d}} 的结构示意图
图 2 可以看到,存储原子 a、b、c、d 时都是用子表包裹着表示的,因为原子 a 和子表 {b,c,d} 在广义表中同属一级,而原子 b、c、d 也同属一级。
广义表的另一种存储结构
一般使用这种存储方式,更好理解一些
如果你觉得图 2 这种存储广义表的方式不合理,可以使用另一套表示广义表中原子和子表结构的节点,如图 3 所示:
图 3 广义表的另一套节点结构
如图 3 所示,表示原子的节点构成由 tag 标记位、原子值和 tp 指针构成,表示子表的节点还是由 tag 标记位、hp 指针和 tp 指针构成。
图 3 的节点结构用 C 语言代码表示为:
typedef struct GNode {
int tag;//标志域
union {
int atom;//原子结点的值域
struct GNode* hp;//子表结点的指针域,hp指向表头
}subNode;
struct GNode* tp;//这里的tp相当于链表的next指针,用于指向下一个数据元素
}GLNode, *Glist;
采用图 3 中的节点结构存储广义表 {a,{b,c,d}} 的示意图如图 4 所示:
图 4 广义表 {a,{b,c,d}} 的存储结构示意图
C语言代码实现
GenList.h
#pragma once
#include<stdio.h>
#include<string.h>
#include<malloc.h>
#include<assert.h>
#include<stdlib.h>#define AtomType int //原子类型
/*
广义表的结点标记
HEAD:表示表头结点
ATOM:表示原子结点
CHILDLIST:表示子表结点
*/
typedef enum{HEAD,ATOM,CHILDLIST}ElemTag;//广义表的结点
typedef struct GLNode
{
ElemTag tag; //结点标记
union //联合体
{
AtomType atom; //原子数据(如果该结点是原子结点,则使用该项)
struct GLNode *hp; //表结点的表头指针(如果该结点是子表结点,则使用该项),指向子表的表头结点
};
struct GLNode *tp; //表尾指针(因为广义表的结点只有表头和表尾,所以这里的表尾指针相当于线性链表的next,指向下一个元素结点)
}GLNode;typedef GLNode* GenList; //广义表
void InitGenList(GenList &gl);
void CreateGenList(GenList &gl, char *str);
bool sever(char *sub, char *hsub);///
char* GetGenListStr(GenList gl,int type);
char* GetHead(GenList gl);
char* GetTail(GenList gl);
char* GetLast(GenList gl);
void ShowGenList(GenList gl);
bool GenListEmpty(GenList gl);
int GenListLength(GenList gl);
int GenListDepth(GenList gl);
void CopyGenList(GenList gl,GenList &T);
void InsertFirstGenList(GenList &gl, char *str);
void DeleteFirstGenList(GenList &gl,char* &str);
void ClearGenList(GenList &gl);
void DestroyGenList(GenList &gl);
GenList.cpp
//广义表
#include"GenList.h"
//广义表的初始化
void InitGenList(GenList &gl)
{
gl = NULL;
}//char *ga = "(1,2,3)";
//char *gb = "(1,(2,3))";
//char *gc = "(1,(2),3)";
//char *gd = "((1,2),3)";
//char *gd = "((1,2,3))";
//char *gd = "()";
//char *ge = "(1,(2,(3,4)),5)";//创建广义表:通过字符串str来创建广义表gl
void CreateGenList(GenList &gl, char *str)
{
int n = strlen(str);//求解字符串长度
//存储表内元素
char *sub = (char *)malloc(sizeof(char) * (n-2));
//存储表头
char *hsub = (char *)malloc(sizeof(char) * (n-2));
assert(sub!=NULL && hsub!=NULL);//"1,2,3"
//去掉sub左右两边的括号
strncpy(sub,str+1,n-2);
sub[n-2] = '\0';//加上结束符//判断广义表是否为空
if(gl == NULL)
{//为空
//创建头结点(广义表的第一个结点为头结点,其余都为尾结点)
gl = (GLNode*)malloc(sizeof(GLNode));
assert(gl != NULL);
gl->tag = HEAD; //结点标记为头结点
gl->hp = gl->tp = NULL; //把子表指针和尾指针都指向空
}GLNode *p = gl; //为了不丢失gl的指向,定义一个指针来操作
//求子串长度,当sub长度不为零时,说明广义表还未创建完成
while(strlen(sub) != 0)
{
//采用尾插法,在后面插入结点
/*
1、创建一个结点
2、让p所指结点的尾指针指向新建的结点
3、让p指向新建结点
*/
p = p->tp = (GLNode*)malloc(sizeof(GLNode));
assert(p != NULL);
p->hp = p->tp = NULL;//将新建结点的子表指针和尾指针都赋空//"1,2,3" ==> "1" hsub="1", sub="2,3";
//"(1,2),3,4" ==> hsub="(1,2)" sub="3,4"
if(sever(sub,hsub))//sever函数分离表头,并将表头存入hsub中
{//分离成功//对分离出来的表头进行判断,是否包含括号
if(hsub[0] == '(')
{//是
//说明要创建的结点是子表类型
p->tag = CHILDLIST;//设置子表标记
CreateGenList(p->hp,hsub);//在p的表头结点处,创建hsub子表
}
else
{//否
//说明要创建的结点是原子类型
p->tag = ATOM;//设置原子标记
p->atom = atoi(hsub);//将表头字符串转换成整型数据,赋值给原子数据
}
}
}
}//"1,2,3" ==> hsub="1", sub="2,3";
//"(1,2),3,4" ==> hsub="(1,2)" sub="3,4"//"" "()"
//"(1,2)" ==> hsub ="(1,2)" sub = ""
//广义表表头分割函数,将sub中的表头分割到hsub中
bool sever(char *sub, char *hsub)
{
//判断sub是否为空
if(*sub=='\0' || strcmp(sub,"()")==0)
{//是 情况:"" 或 "()"
hsub[0] = '\0';//将hsub赋空
return true;
}
int n = strlen(sub);//求sub的长度
int i=0;
char ch = sub[0];//获取第一个字符
int k = 0;//表示括号的信息
/*当sub还没遍历完成且还未检测到括号外的逗号,继续遍历
注:当检测到括号外的逗号时,说明找到表头分割点,如:"(1,2),3,4" 表头即为(1,2)
*/
while(i<n && (ch!=','|| k!=0))
{
//判断是否运到左括号
if(ch == '(')
k++;//k++表示进入一层括号
else if(ch == ')')//判断是否遇到右括号
k--; //k--表示退出一层括号//获取下一个元素
i++;
ch = sub[i];
}//判断是否是因为检测到括号外的逗号而结束的
if(i < n)
{//是
//在i位置截断,前面部分就是表头
sub[i] = '\0';
//将取得的表头放入hsub中
strcpy(hsub,sub);
//更新sub的值:此时的sub应该去掉表头hsub
strcpy(sub,sub+i+1);
}
else if(k != 0) //判断是否是因为内部括号不匹配
return false;//是,分割失败
else//判断是否是因为i>=n而结束
{//是,情况 "(1,2)" ==> hsub ="(1,2)" sub = ""
//说明sub整个就是表头
strcpy(hsub,sub);//把sub整个赋值给hsub
sub[0] = '\0';//sub赋给hsub后,此时sub为空
}return true;
}//将整数转换成字符串
void NumToStr(int num, char* str,int& i)
{
char temp[25];
itoa(num, temp, 10);
for(unsigned j=0;j<strlen(temp);++j)
str[i++]=temp[j];
}
/*
将广义表转换成字符串形式:如(1,(2,3))
但是由于递归获取,该函数转换后在结尾会多一个逗号,
所以使用GetGenListStr对其进行包装消去逗号
*/void GetGenList(GenList gl,char* str,int& i)
{
//获取从首结点到最后一个结点的元素
GLNode *p = gl->tp;
//当还未遍历到最后一个结点,一直进行遍历
while(p != NULL)
{
//对结点类型进行判断
if(p->tag == ATOM)
{//结点类型为原子类型
// char temp[25];
// itoa(p->atom, temp, 10);
// for(unsigned j=0;j<strlen(temp);++j)
// str[i++]=temp[j];
NumToStr(p->atom, str,i);//将数字转换成字符串存入str中
if(p->tp != NULL)//判断该结点后面是否还有结点
str[i++]=',';
p = p->tp;//下移
}
else if(p->tag == CHILDLIST)
{//结点类型为子表
str[i++]='(';
GetGenList(p->hp,str,i);//从子表的表头指针开始对子表进行遍历
str[i++]=')';
if(p->tp != NULL)//判断该结点后面是否还有结点
str[i++]=',';
p = p->tp;//子表遍历完成后下移
}
}
}//将广义表转换成字符串形式:如(1,(2,3)) type:0 输出原子结点 type:1 输出表
char* GetGenListStr(GenList gl,int type)
{
int i=0;
char* str=(char*)malloc(sizeof(char)*1000);
//str[i++]='(';//左括号
if(type==0)
{
NumToStr(gl->atom, str,i); //将数字转换成字符串
}
else
{
GetGenList(gl,str,i);//会多一个逗号
}
//str[i++]=')';//右括号
str[i]='\0';
return str;
}//取首元素
char* GetHead(GenList gl)
{
//判断首元素的结点类型
if(gl->tp->tag==ATOM) //原子类型则传入该原子结点地址
return GetGenListStr(gl->tp,0);
else //子表类型则传入子节点的首地址
return GetGenListStr(gl->hp,1);
}//取尾元素
char* GetTail(GenList gl)
{
GLNode *p=gl->tp;
if(p->tp!=NULL)//判断是否存在尾元素
{//存在
//取尾元素(取后面的一串值)
return GetGenListStr(p,1);
}
return NULL;
}//取最后一个元素
char* GetLast(GenList gl)
{
GLNode *p=gl->tp;
while(p->tp!=NULL)
p=p->tp;
//判断首元素的结点类型
if(p->tag==ATOM)//原子类型则传入该原子结点地址
return GetGenListStr(p,0);
else //子表类型则传入子节点的首地址
return GetGenListStr(p->hp,1);}
//遍历广义表
/*
void ShowGenList(GenList gl)
{
//由头结点指向下一个结点(头结点不存放数据)
GLNode *p = gl->tp;
printf("(");
//当还未遍历到最后一个结点,一直进行遍历
while(p != NULL)
{
//对结点类型进行判断
if(p->tag == ATOM)
{//结点类型为原子类型
printf("%d",p->atom);//打印原子数据
if(p->tp != NULL)//判断该结点后面是否还有结点
printf(",");//有,打印","
p = p->tp;//下移
}
else if(p->tag == CHILDLIST)
{//结点类型为子表
ShowGenList(p->hp);//从子表的表头指针开始对子表进行遍历
p = p->tp;//子表遍历完成后下移
}
}
printf("),");//遍历结束补上括号
}*/
//打印广义表
void ShowGenList(GenList gl)
{
//调用GetGenListStr函数将广义表转换成字符串
printf("(%s)",GetGenListStr(gl,1));
}
//判断广义表是否为空
bool GenListEmpty(GenList gl)
{
return gl->tp==NULL; //判断表头结点尾指针的指向是否为空
}//求解广义表的长度
int GenListLength(GenList gl)
{
int length = 0;//记录长度
GLNode *p = gl->tp;//从头结点的下一个结点开始统计(因为头结点不存数据)
//当广义表还有元素,就继续遍历
while(p != NULL)
{
length++;//运到结点长度就加一
p = p->tp;//后移
}
return length; //返回广义表长度
}//求解广义表的深度
int GenListDepth(GenList gl)
{
//判断广义表是否为空
if(gl->tp == NULL)
return 1;//空表深度为1
GLNode *p = gl->tp; //指向头结点的下一个结点
int maxdepth = 0; //记录深度int dep;
//当广义表还未遍历完成,就一直遍历求取深度
while(p != NULL)
{
//判断结点类型是否为子表结点
if(p->tag == CHILDLIST)
{//为子表结点
//求取子表的深度
dep = GenListDepth(p->hp);
if(dep > maxdepth)//判断此时求得的深度是否比当前的最大深度大
maxdepth =dep;//是,将dep赋给maxdepth
}
p = p->tp;//后移
}
return maxdepth+1;//返回深度(该层的深度=该层以下的最大深度+加上当前层的深度1)
}//广义表的复制:gl复制到T
void CopyGenList(GenList gl,GenList &T)
{//判断广义表是否为空
if(gl == NULL)
{//为空
return;
}
//如果原来的广义表T已经存放数据
if(T!=NULL)
{
DestroyGenList(T);
}
//创建头结点(广义表的第一个结点为头结点,其余都为尾结点)
T = (GLNode*)malloc(sizeof(GLNode));
assert(T != NULL);
T->tag = gl->tag; //结点标记为头结点
T->hp = gl->hp;
T->tp = gl->tp; //把子表指针和尾指针都指向空
//由头结点指向下一个结点(头结点不存放数据)
GLNode *p = gl->tp; //为了不丢失gl的指向,定义一个指针来操作
GLNode *q = T; //为了不丢失T的指向,定义一个指针来操作//当还未遍历到最后一个结点,一直进行遍历
while(p != NULL)
{//创建结点
q = q->tp = (GLNode*)malloc(sizeof(GLNode));
assert(q != NULL);
q->tag = p->tag; //结点标记为头结点
q->hp = q->tp = NULL;//将新建结点的子表指针和尾指针都赋空//对结点类型进行判断
if(p->tag == ATOM)
{//结点类型为原子类型
q->atom =p->atom;//赋值
p = p->tp;//下移
}
else if(p->tag == CHILDLIST)
{//结点类型为子表
CopyGenList(p->hp,q->hp);//从子表的表头指针开始对子表进行遍历
p = p->tp;//子表遍历完成后下移
}
}
}
//插入元素str( 广义表的字符表示,如:((1,2),3) )作为广义表的第一元素
void InsertFirstGenList(GenList &gl, char *str)
{
GenList t;
InitGenList(t);
CreateGenList(t, str);//由头结点指向下一个结点(头结点不存放数据)
GLNode *p = t->tp;
//往后移动,一直移动到最后一个结点
while(p->tp != NULL)
{
p=p->tp;
}//将新创建的广义表t放到gl的前面
p->tp=gl->tp;//将gl的头结点连上广义表t
gl->tp=t->tp;free(t);//t的头结点无用,释放
}//删除广义表第一个位置的元素
void DeleteFirstGenList(GenList &gl,char* & str)
{
GenList t;
InitGenList(t);
t=gl->tp; //将第一个元素地址传给t
//将第一个元素从广义表中断开
gl->tp=gl->tp->tp;//判断要释放的结点是什么类型
if(t->tag==CHILDLIST)
{//子表结点
//保存要删除的值
str=GetGenListStr(t->hp,1);
//释放子表
DestroyGenList(t->hp);
}
else if(t->tag==ATOM)
{//原子结点
//保存值
str=GetGenListStr(t,0);
}
free(t);
}
//清空广义表
void ClearGenList(GenList &gl)
{
//由头结点指向下一个结点(头结点不存放数据)
GLNode *p = gl->tp;
//当还未遍历到最后一个结点,一直进行遍历
while(p != NULL)
{
//对结点类型进行判断
if(p->tag == ATOM)
{//结点类型为原子类型
gl->tp =p->tp;//将原子结点从广义表中取下
free(p); //释放原子结点的内存空间
p = gl->tp;//指向下一个结点
}
else if(p->tag == CHILDLIST)
{//结点类型为子表
ClearGenList(p->hp);//从子表的表头指针开始对子表进行清空
p = p->tp;//子表遍历完成后下移
}
}}
//销毁广义表
void DestroyGenList(GenList &gl)
{
//清空广义表
ClearGenList(gl);
free(gl);//释放广义表
gl= NULL;
}
main.c
#include"GenList.h"
void main()
{
GenList gl;
InitGenList(gl);
char *ga = "(1,2,3)";
char *gb = "(1,(2,3))";
char *gc = "(1,(2,3),4)";
char *gd = "((1,2),3)";
char *ge = "((1,2,3))";
char *gf = "()";
char *gg = "(1,(2,(3,(10,20),4),5),6)";
char *gh = "((((1,2),1),1),6,1)";CreateGenList(gl, gg);
ShowGenList(gl);
printf("\n");
int length = GenListLength(gl);
printf("length = %d\n",length);int depth = GenListDepth(gl);
printf("depth = %d\n",depth);GenList T;
InitGenList(T);
printf("----------------------------\n");
printf("复制:");
CopyGenList( gl,T);
ShowGenList(T);
printf("\n");printf("----------------------------\n");
printf("插入前:");
ShowGenList(gl);
printf("\n");
InsertFirstGenList(gl, ga);
printf("插入后:");
ShowGenList(gl);
printf("\n");
printf("----------------------------\n");
char* str;
printf("删除前:");
ShowGenList(gl);
printf("\n");
DeleteFirstGenList(gl,str);
printf("删除后:");
ShowGenList(gl);
printf("\n");
printf("删除的首元素为:%s\n",str);printf("----------------------------\n");
ShowGenList(gl);
printf("\n");
printf("头元素为:%s\n",GetHead(gl));
printf("尾元素为:%s\n",GetTail(gl));DestroyGenList(gl);
}
参考资料: