数据结构编程笔记十九:第七章 图 图的邻接表存储表示及各基本操作的实现

时间:2021-09-25 10:45:31

上次我们介绍了图的邻接矩阵存储结构基本操作的实现,这次介绍图的邻接表存储表示各基本操作的实现。

还是老规矩:

程序在码云上可以下载。
地址:https://git.oschina.net/601345138/DataStructureCLanguage.git

图的ADT不再重复贴了,请参考:《数据结构编程笔记十八:第七章 图 图的邻接矩阵存储表示各基本操作的实现》

邻接表就是一个结点数组,数组每个元素后面挂一个链表。这种“顺序+链式”的风格很常见,比如第九章 查找中提到的散列表(链地址法P258页)就有类似的设计。
数据结构编程笔记十九:第七章 图 图的邻接表存储表示及各基本操作的实现

本次程序用到的源文件清单:
my_constants.h 各种状态码
ALGraph.h 图的邻接表存储结构表示定义
ALGraph.cpp 基于邻接表的基本操作实现
DFS_BFS_ALGraph.cpp 基于邻接表的深度优先遍历和广度优先遍历算法实现
LinkedQueue.cpp 链队列的实现
LinkedList.cpp 不带头结点单链表的实现
基本操作及遍历测试.cpp 主函数,调用基本操作完成演示

本次程序的广度优先遍历用到了队列,我使用了第三章的程序:链队列。
链队列在:《数据结构编程笔记九:第三章 栈和队列 链队列的实现》一文中有介绍。我对这篇文章中的程序进行了精简,但主要函数没有改动。 具体程序我放在总结后面。如有需要请到那里查看。

为了操作实现方便,我使用了单链表程序来简化部分操作,但是这个单链表与《 数据结构编程笔记四:第二章 线性表 单链表的实现》一文介绍的单链表有所区别,本文用到的单链表不带头结点,很多操作与带头结点的单链表有区别。望读者注意。具体程序我放在总结后面。如有需要请到那里查看。

需要注意的是:为了能够复用单链表的部分操作,我把书上P163页的存储结构做了一些修改,具体看源代码。

一起来看看程序的实现。
源文件:my_constants.h

//************************自定义符号常量*************************** 

#define OVERFLOW -2 //内存溢出错误常量
#define OK 1 //表示操作正确的常量
#define ERROR 0 //表示操作错误的常量
#define TRUE 1 //表示逻辑真的常量
#define FALSE 0 //表示逻辑假的常量

//***********************自定义数据类型****************************

typedef int Status; //指定状态码的类型是int

源文件:ALGraph.h

//---------------------图的邻接表存储表示---------------------- 

//指定顶点类型是int
typedef int VertexType;

//指定弧的权值类型为int
typedef int InfoType;

//最大顶点数
#define MAX_VERTEX_NUM 50

//图的种类标志
typedef enum {

// {有向图 = 0,有向网 = 1,无向图 = 2,无向网 = 3}
DG, DN, UDG, UDN
} GraphKind;

//存储边信息的结点

typedef struct ElemType{

//该弧所指向的顶点的位置,也就是从当前顶点出发指向哪个顶点
//这个变量存储的是指向顶点在AdjList数组中的下标
int adjvex;

//该弧相关信息的指针
//弧的相关信息就是存放权值的,当图的类型是网的时候这个变量才有意义
InfoType *info;
} ElemType;

typedef struct ArcNode{

//除了弧指针外的其他部分均属于data
ElemType data;

//指向下一条弧的指针
struct ArcNode *nextarc;
} ArcNode;

//存储顶点信息的结点
typedef struct VNode {

//顶点名称
VertexType data;

//指向第一条依附该顶点的弧(存储第一条边结点地址的指针域)
ArcNode *firstarc;
} VNode, AdjList[MAX_VERTEX_NUM];

//图的邻接表存储表示
typedef struct ALGraph {

//顶点向量,在数组中存储了图中所有的顶点
AdjList vertices;

//图的当前顶点数和弧数(边数)
int vexnum, arcnum;

//图的种类标志
GraphKind kind;
} ALGraph;

//为了复用单链表中的插入、删除等基本操作,采用宏定义方式配合引入的单链表实现
//完成与图的邻接表存储结构的操作对接

//定义单链表的结点类型是图的表结点的类型
#define LNode ArcNode

//定义单链表结点的指针域是表结点指向下一条弧的指针域
#define next nextarc

//定义指向单链表结点的指针是指向图的表结点的指针
typedef ArcNode *LinkList;

源文件:ALGraph.cpp

//-----------------------------图的基本操作------------------------------

/*
函数:LocateVex
参数:ALGraph G 图G(邻接表存储结构)
VertexType u 顶点u
返回值:若G中存在顶点u,则返回该顶点在图中位置;否则返回-1
初始条件:图G存在,u和G中顶点有相同特征
作用:在图G中查找顶点u的位置
*/

int LocateVex(ALGraph G, VertexType u){

//i是临时变量,循环控制用
int i;

//与图中每个结点的名称作对比
for(i = 0; i < G.vexnum; ++i) {

//在图中找到了结点
if(G.vertices[i].data == u) {

//返回顶点在图中的位置
return i;
}//if
}//for

//没有在图中找到顶点,返回-1
return -1;
}//LocateVex

/*
函数:CreateGraph
参数:ALGraph &G 图的引用
返回值:状态码,操作成功返回OK。
作用:根据用户的输入构造图(四种)。
*/

Status CreateGraph(ALGraph &G) {

//w是权值
//i、j、k全是临时变量,循环用
int i, j, k, w;

//连接边或弧的2顶点
VertexType va, vb;

//弧结点
ElemType e;

//确定图的类型
printf("请输入图的类型(有向图输入0, 有向网输入1,无向图输入2,无向网输入3): ");
scanf("%d", &G.kind);

//确定图的顶点数和边数
printf("请输入图的顶点数,边数: ");
scanf("%d,%d", &G.vexnum, &G.arcnum);

//从键盘输入顶点值,构造顶点向量
printf("请输入%d个顶点的值(用空格隔开):\n", G.vexnum);
for(i = 0; i < G.vexnum; ++i) {

//输入顶点的值
scanf("%d", &G.vertices[i].data);
getchar(); //吃掉多余字符

//初始化与该顶点有关的出弧链表
G.vertices[i].firstarc = NULL;
}//for

//如果是构造网,则需要继续输入权值,如果是构造图则不需要
//枚举变量中{有向图 = 0,有向网 = 1,无向图 = 2,无向网 = 3}
//G.kind % 2 != 0 表示构造的是网,否则是图
if(G.kind % 2) { //if(G.kind % 2) <=> if(G.kind % 2 != 0)
printf("请输入每条弧(边)的弧尾、弧头和权值(以逗号作为间隔):\n");
}//if
else { //构造图
printf("请输入每条弧(边)的弧尾和弧头(以逗号作为间隔):\n");
}//else

//构造相关弧链表
for(k = 0; k < G.arcnum; ++k){

//如果构造的是网,则需要接收权值
//if(G.kind % 2) <=> if(G.kind % 2 != 0)
if(G.kind % 2) {
scanf("%d,%d,%d", &va, &vb, &w);
}//if
else { //构造的是图,不需要接收权值
scanf("%d,%d", &va, &vb);
}//else

//先处理弧头和弧尾部分,这是图和网都需要执行的公共操作。

//弧尾
i = LocateVex(G, va);

//弧头
j = LocateVex(G, vb);

//给待插表结点e赋值,图没有权值,网有权值。由于所有结点的初始化
//都用到e,所以这块空间每轮新的循环都需要重置为NULL(清零)。
//如果构造的是网,还会连接一小块保存权值的空间,否则保持NULL。
e.info = NULL;

//弧头
e.adjvex = j;

//如果构造的是网,则需要将权值存放到弧的相关信息info中
//注意:此时info的空间还没有申请到
//if(G.kind % 2) <=> if(G.kind % 2 != 0)
if(G.kind % 2) {

//申请存放权值的空间
e.info = (InfoType *)malloc(sizeof(int));

//将弧的相关信息填入存放权值的空间中
*(e.info) = w;
}//if

//将弧结点插在第i个顶点的表头
//本项操作调用的是单链表的插入算法,把G.vertices[i].firstarc当成了头结点
//采用头插法插入
ListInsert(G.vertices[i].firstarc, 1, e);

//如果构造的是无向图或无向网,产生第2个表结点,并插在第j个元素(入弧)的表头
//第二个表结点与第一个结点沿主对角线(左上至右下)对称
if(G.kind >= 2) {

//主对角线对称位置的结点权值不变,所以e.info不变,不必再赋值
//也就是说:邻接矩阵中沿主对角线对称的两个结点在邻接表*用一块权值空间
e.adjvex = i;

//插在对角线对称位置顶点的表头,也就是第j个位置。
ListInsert(G.vertices[j].firstarc, 1, e);
}//if
}//for

//操作成功
return OK;
}//CreateGraph

/*
函数:DestroyGraph
参数:ALGraph &G 图的引用
返回值:状态码,操作成功返回OK。
作用:若图G存在,销毁图G
*/

Status DestroyGraph(ALGraph &G) {

//i是临时变量,循环控制用
int i;

//带回从邻接表中删除的弧结点,如果是网,还需要继续释放存储权值的存储单元的空间
ElemType e;

//对于所有顶点
for(i = 0; i < G.vexnum; ++i) {

//如果被销毁的是网,还需要继续销毁权值
//if(G.kind % 2) <=> if(G.kind % 2 != 0)
if(G.kind % 2) {

//顶点对应的弧或边链表不空
while(G.vertices[i].firstarc) {

//删除链表的第1个弧结点,并将值赋给e
ListDelete(G.vertices[i].firstarc, 1, e);

//释放保存权值的内存空间
//需要注意:在构造网时为了节约空间,让沿主对角线对称的两个结点
//的权值信息指针e.info指向了同一块内存区域。所以释放内存空间的时候
//要注意已经释放过的内存空间不可以再次释放。所以在释放内存空间之前
//需要进行判断。但是C语言没有提供判断内存是否被释放的函数。
//这就需要我们自己写判断条件。观察邻接矩阵不难发现:
//主对角线上方的结点和沿主对角线对称的下方的结点之间存在一个规律:
//上方结点的列下标 > 对称位置(下方)结点的列下标
//由于释放弧结点时是按照顶点顺序从小到大进行的,
//列下标大的弧结点(上三角)总比列下标小的弧结点(下三角)先释放。
//所以判断结点是否被释放的标志就是:顶点序号 > i
if(e.adjvex > i) {
free(e.info);
}//if
}//while
}//if
else {//若被销毁的是图,只销毁弧链表就行了

//调用单链表销毁函数销毁弧链表
DestoryList(G.vertices[i].firstarc);
}//else
}//for

//重置顶点数为0
G.vexnum = 0;

//重置边或弧数为0
G.arcnum = 0;

//操作成功
return OK;
}//DestroyGraph

/*
函数:GetVex
参数:ALGraph G 图G
int v 某个顶点的序号v
返回值:顶点v的值
作用:若图G存在,v是G中某个顶点的序号,返回顶点v的值
*/

VertexType& GetVex(ALGraph G, int v) {

//检查顶点序号v是否越界
if(v >= G.vexnum || v < 0) {

//若越界访问则直接退出程序
exit(ERROR);
}//if

//返回顶点v的值
return G.vertices[v].data;
}//GetVex

/*
函数:PutVex
参数:ALGraph &G 图G的引用
VertexType v 待修改顶点v
VertexType value 将顶点v的值修改为value
返回值:状态码,操作成功返回OK,失败返回ERROR
作用:若图G存在,v是G中某个顶点,对v赋新值value
*/

Status PutVex(ALGraph &G, VertexType v, VertexType value) {

//i记录了在图的邻接表中定位顶点操作得到的v在顶点数组中的下标
int i;

//通过定位操作得到下标
i = LocateVex(G, v);

//得到下标之后需判断下标是否合法:即图中是否存在顶点v
//若图中不存在顶点v,定位函数LocateVex返回-1
if(i > -1) { //图中存在顶点v

//修改顶点v的值为value
G.vertices[i].data = value;

//操作成功
return OK;
}//if

//操作失败
return ERROR;
}//PutVex

/*
函数:FirstAdjVex
参数:ALGraph G 图G
VertexType v 顶点v
返回值:若v在图G中有邻接顶点,返回v的第一个邻接顶点的序号,否则返回-1。
作用:若图G存在,v是G中某个顶点,返回v的第一个邻接顶点的序号。
若顶点在G中没有邻接顶点,则返回-1
*/

int FirstAdjVex(ALGraph G, VertexType v) {

//工作指针p,指向顶点v弧链表中的顶点
ArcNode *p;

//i为顶点v在图G中的序号
int i;

//通过定位操作找出顶点v的序号
i = LocateVex(G, v);

//检查图中是否存在顶点v
if(i == -1) {
return -1;
}//if

//工作指针p指向序号i指示的顶点v
p = G.vertices[i].firstarc;

//如果v有邻接顶点
if(p) { //if(p) <=> if(p != NULL)

//返回顶点v第一个邻接顶点的序号
return p->data.adjvex;
}//if
else { //顶点v没有邻接顶点

//返回-1
return -1;
}//else
}//FirstAdjVex

/*
函数:equalvex
参数:ElemType a 顶点A
ElemType b 顶点B
返回值:状态码,操作成功返回OK,失败返回ERROR
作用:判断两个弧结点是否有相同的邻接点。
DeleteArc()、DeleteVex()和NextAdjVex()要调用的函数
*/

Status equalvex(ElemType a, ElemType b) {

//如果顶点A和顶点B的邻接顶点相同
if(a.adjvex == b.adjvex) {

//返回1
return OK;
}//if
else {

//返回0
return ERROR;
}//else
}//equalvex

/*
函数:NextAdjVex
参数:ALGraph G 图G
VertexType v v是G中某个顶点
VertexType w w是v的邻接顶点
返回值:返回v的(相对于w的)下一个邻接顶点的序号。
若w是v的最后一个邻接点,则返回-1
作用:得到顶点v相对于顶点w的下一个邻接顶点的序号。
*/

int NextAdjVex(ALGraph G, VertexType v, VertexType w) {

//p是工作指针
//p1在Point()中用作辅助指针
LinkList p, p1;

//e保存了弧结点的信息
ElemType e;

//找出顶点v在图G中的序号v1
int v1 = LocateVex(G, v);

//找出顶点w在图G中的序号,由e.adjvex保存
e.adjvex = LocateVex(G, w);

//p指向顶点v的链表中邻接顶点为w的结点
p = Point(G.vertices[v1].firstarc, e, equalvex, p1);

//没找到w或w是最后一个邻接点
if(!p || !p->next) {
return -1;
}//if
else { //找出了与顶点v邻接的顶点w

//返回v的相对于w的下一个邻接顶点的序号
return p->next->data.adjvex;
}//else
}//NextAdjVex

/*
函数:InsertVex
参数:ALGraph &G 图G的引用
VertexType v 被插入的顶点v
返回值:状态码,操作成功返回OK,否则返回ERROR
作用:在图G中增添新顶点v,但不增添与顶点相关的弧。
*/

Status InsertVex(ALGraph &G, VertexType v) {

//加入新的顶点,设置其名称
G.vertices[G.vexnum].data = v;

//由于还没有添加邻接弧的信息,所以firstarc的值为空
G.vertices[G.vexnum].firstarc = NULL;

//图G的顶点数加1
G.vexnum++;

//操作成功
return OK;
}//InsertVex

/*
函数:DeleteVex
参数:ALGraph &G 图G的引用
VertexType v 被删除的顶点v
返回值:状态码,操作成功返回OK,否则返回ERROR
作用:删除G中顶点v及其相关的弧
*/

Status DeleteVex(ALGraph &G, VertexType v) {

//i是临时变量,存储了以v为出度的弧或边数
//j是临时变量,保存顶点v的序号
int i, j, k;

//临时变量,用于设置查找以顶点v为邻接顶点的弧结点的条件
ElemType e;

//工作指针p,指向找到的以顶点v为邻接顶点的弧结点
//工作指针p1指向p的前驱
LinkList p, p1;

//j是顶点v的序号
j = LocateVex(G, v);

//检查在图G中是否找到顶点v
if(j < 0) {
return ERROR;
}//if

//i存储了以v为出度的弧或边数(也就是弧链表长度)
i = ListLength(G.vertices[j].firstarc);

//边或弧数-i
G.arcnum -= i;

//如果图G是网,还需要另外销毁保存权值的内存空间
if(G.kind % 2) {

//对应的弧或边链表不空
while(G.vertices[j].firstarc) {

//删除链表的第1个结点,并将值赋给e
ListDelete(G.vertices[j].firstarc, 1, e);

//考虑到无向网的权值空间共享问题,所以要先判断
//权值空间是否已经被释放再执行空间释放操作
if(e.info != NULL) {
//释放e.info指示的动态生成的权值空间
free(e.info);
e.info == NULL; //指针置空
}//if
}//while
}//if
else { //如果是图

//只需要销毁弧或边链表就可以了,不用考虑权值问题
DestoryList(G.vertices[j].firstarc);
}//else

//顶点数减1
G.vexnum--;

//保存顶点采用了顺序存储结构,所以删除顶点v之后,后面的顶点需要前移
for(i = j; i < G.vexnum; i++) {
G.vertices[i] = G.vertices[i + 1];
}//for

//之前的操作只是删除了顶点v和其后面的弧链表。还有很多善后工作没有做:
//1.之前的删除操作只考虑了出度,没考虑入度。我们不仅需要删除掉v指向
// 其他顶点的信息,还需要删除其他顶点指向顶点v的弧。
//2.如果顶点v不是顶点数组中的最后一个顶点,由于在删除顶点v后,顶点v
// 后面的顶点需要做前移操作。此时后面顶点在顶点数组中的位序就会改变
// 但是与后面顶点相关的弧结点中存储的邻接顶点的位序并未随之更新,
// 所以我们还需要将这些邻接顶点的弧结点中的邻接顶点的位序全部更新一遍。
// 保证删除操作后存储的图的邻接信息的正确性。
//3.如果是网,还需要考虑权值存储空间的释放问题。

//删除以v为入度的弧或边且必要时修改表结点的顶点位置值
for(i = 0; i < G.vexnum; i++) {

//j保存了顶点v的序号,接下来要利用e找出所有与v邻接的顶点
e.adjvex = j;

//p指向在G.vertices[i].firstarc指示的弧链表中以v为邻接点的弧结点
//p1指向p的前驱,若p指向首元结点,则p1为NULL
p = Point(G.vertices[i].firstarc, e, equalvex, p1);

//顶点i的邻接表上有v为入度的结点
if(p) { //if(p) <=> if(p != NULL)

//p不指向首元结点
if(p1) { //if(p1) <=> if(p1 != NULL)

//从链表中删除p所指结点
p1->next = p->next;
}//if
else { // p指向首元结点

//头指针指向下一结点
G.vertices[i].firstarc = p->next;
}//else

//有向图或者有向网
if(G.kind < 2) {

//边或弧数-1
G.arcnum--;

//有向网直接释放权值内存空间就行,不用考虑对称位置的问题
if(G.kind == DN) {
//释放动态生成的权值空间
free(p->data.info);
}//if
}//if

//释放v为入度的结点
free(p);
}//if

//解决由于结点前移造成后面结点弧的邻接信息不正确的问题
//对于adjvex域>j的结点,其序号-1
for(k = j + 1; k <= G.vexnum; k++) {

//将以顶点v后面顶点为邻接顶点的顶点在图中的序号
//保存在e.adjvex中
e.adjvex = k;

//使p指向以顶点v为入度的弧结点
//p1指向p的前驱,若p指向头结点,p1=NULL
p = Point(G.vertices[i].firstarc, e, equalvex, p1);

//判断是否找到这样的弧结点
if(p) { //if(p) <=> if(p != NULL)

//由于顶点v被删除造成v后面的顶点前移。所以存储在弧结点中的
//序号应该和前移之后的顶点序号保持一致,即序号-1
p->data.adjvex--;
}//if
}//for
}//for

//操作成功
return OK;
}//DeleteVex

/*
函数:InsertArc
参数:ALGraph &G 图G的引用
VertexType v 图中的顶点w (弧尾)
VertexType w 图中的顶点v (弧头)
返回值:状态码,操作成功返回OK,否则返回ERROR
作用:在G中增添弧<v,w>,若G是无向的,则还增添对称弧<w,v>
*/

Status InsertArc(ALGraph &G, VertexType v, VertexType w) {

//临时变量,存储了弧结点的信息
ElemType e;

//i是顶点v在图G中的序号
int i = LocateVex(G, v);

//j是顶点w在图G中的序号
int j = LocateVex(G, w);

//检查顶点v或w是否在图G中存在,若不存在则终止函数向下执行
if(i < 0 || j < 0) {
return ERROR;
}//if

//插入一条边之后图G的弧或边的数目加1
//注意:网中两个顶点之间正向和反向的弧算一条,不要重复添加
G.arcnum++;

//在顶点v的弧链表中插入邻接点w的信息
e.adjvex = j;

//图没有权值,所以将权值指针e.info的初始值设置为NULL
e.info = NULL;

//如果是在网中插入弧,还需要设置弧的权值
if(G.kind % 2) { //if(G.kind % 2) <=> if(G.kind % 2 != 0)

//动态申请存放权值的空间
e.info = (InfoType *)malloc(sizeof(int));

//从键盘输入权值的信息
printf("请输入弧(边)%d→%d的权值: ", v, w);
scanf("%d", &e.info);
}//if

//将e插在弧尾v的弧链表表头
ListInsert(G.vertices[i].firstarc, 1, e);

//在无向图或无向网中插入弧,还要加入反向的另一条弧
if(G.kind >= 2) {

//无向图没有权值,两条弧的e.info都是NULL
//无向网的两条弧共用一个权值存储单元,e.info不变
e.adjvex = i;

//将e插在顶点w弧链表的表头
ListInsert(G.vertices[j].firstarc, 1, e);

//注意:网中两个顶点之间正向和反向的弧算一条,不要重复添加
}//if

//操作成功
return OK;
}//InsertArc

/*
函数:DeleteArc
参数:ALGraph &G 图G的引用
VertexType v 图中的顶点w (弧尾)
VertexType w 图中的顶点v (弧头)
返回值:状态码,操作成功返回OK,否则返回ERROR
作用:在G中删除弧<v,w>,若G是无向的,则还删除对称弧<w,v>
*/

Status DeleteArc(ALGraph &G, VertexType v, VertexType w) {

//k记录了删除操作的结果
Status k;

//临时变量e存储了查找邻接点的比较条件
ElemType e;

//i是顶点v(弧尾)在图G中的序号
int i = LocateVex(G, v);

//j是顶点w(弧头)在图G中的序号
int j = LocateVex(G, w);

//检查i和j是否合法
if(i < 0 || j < 0 || i == j) {
return ERROR;
}//if

//删除<v,w>这条弧

//设置在顶点v的弧链表中插入的弧结点的邻接点的序号为j
e.adjvex = j;

//从顶点v的弧链表中删除邻接顶点w的弧结点
k = DeleteElem(G.vertices[i].firstarc, e, equalvex);

//删除弧<v,w>成功
if(k) { //if(k) <=> if(k != ERROR)

//图中弧或边数减1
G.arcnum--;

//如果是网,还需要释放权值存储单元的空间
if(G.kind % 2) {
free(e.info);
}//if

//如果是无向网或无向图则还需要删除对称弧<w,v>
if(G.kind >= 2) {

//设置w的邻接点为v
e.adjvex = i;

//删除w到v的弧
DeleteElem(G.vertices[j].firstarc, e, equalvex);
}//if

//操作成功
return OK;
}//if
else { // 没找到待删除的弧
return ERROR;
}//else
}//DeleteArc

/*
函数:Display
参数:ALGraph &G 图G的引用
VertexType v 图中的顶点w (弧尾)
VertexType w 图中的顶点v (弧头)
返回值:状态码,操作成功返回OK,否则返回ERROR
作用:以邻接矩阵形式输出邻接表
*/

void Display(ALGraph G) {

//工作指针p
ArcNode *p;

//打印图的类型
switch(G.kind) {
case DG: printf("此图为有向图!\n");
break;
case DN: printf("此图为有向网!\n");
break;
case UDG:printf("此图为无向图!\n");
break;
case UDN:printf("此图为无向网!\n");
}//switch

//打印图的总顶点数,总边数以及每条边
printf("\n图*有%d个顶点,%d条弧(边),它们分别是:\n", G.vexnum, G.arcnum);
printf("+----+-----------------------------------------------\n");
printf("|顶点| 邻接顶点(和权值) \n");
printf("+----+-----------------------------------------------\n");
for(int i = 0; i < G.vexnum; i++){

printf("| %d |", G.vertices[i].data);

//p指向某个顶点弧链表的首元结点
p = G.vertices[i].firstarc;

//遍历整个弧链表
while(p) { //while(p) <=> while(p != NULL)

//打印弧头和弧尾的信息
printf(" →%d ", G.vertices[p->data.adjvex].data);

//如果图G是网,还需要打印出权值
if(G.kind % 2) {
printf(",权值:%d ", *(p->data.info));
}//if

//p指向弧链表下一个结点
p = p->nextarc;
}//while

//输出完一个顶点之后换行
printf("\n+----+-----------------------------------------------\n");
}//for

//输出换行,使结果美观
printf("\n");
}//Display

源文件:DFS_BFS_ALGraph.cpp

//------------------------元素访问函数----------------------

/*
函数:print
参数:VertexType v 顶点v
返回值:无
作用:元素访问函数,供遍历使用。
*/

void print(VertexType v) {

printf(" %d ", v);

}//print


//------------------------深度优先遍历----------------------

//访问标志数组(长度等于最大顶点数)
int visited[MAX_VERTEX_NUM];

//访问函数(通过全局变量的方式传递可以减少参数传递的次数)
void(*VisitFunc)(VertexType v);

/*
函数:DFS
参数:ALGraph G 图G
int v 访问图G中的第v个顶点
返回值:无
作用:从第v个顶点出发递归地深度优先遍历图G。
*/

void DFS(ALGraph G, int v) {

//w是临时变量,循环控制用
int w;

//设置访问标志为TRUE,表示该顶点已访问
visited[v] = TRUE;

//访问第v个顶点
VisitFunc(G.vertices[v].data);

//依次访问v的未被访问的邻接点
for(w = FirstAdjVex(G, G.vertices[v].data); w >= 0;
w = NextAdjVex(G, G.vertices[v].data, G.vertices[w].data)) {

//对v的尚未访问的邻接点w递归调用深度优先遍历算法DFS
if(!visited[w]) {
DFS(G, w);
}//if
}//for
}//DFS

/*
函数:DFS
参数:ALGraph G 图G
void(*Visit)(char*) 指向遍历函数的指针
返回值:无
作用:对图G作深度优先遍历。
*/

void DFSTraverse(ALGraph G, void(*Visit)(VertexType)) {

//使用全局变量VisitFunc,使DFS不必设函数指针参数
VisitFunc = Visit;

//初始化访问标志数组每个顶点的标志值为FALSE,表示所有顶点都未被访问
for(int v = 0; v < G.vexnum; v++) {
visited[v] = FALSE;
}//for

//依次访问图中每个顶点
for(int v = 0; v < G.vexnum; v++) {

//对尚未访问的顶点调用DFS
if(!visited[v]) {
DFS(G, v);
}//if
}//for

//遍历完成后换行,使输出美观
printf("\n");
}//DFSTraverse

//------------------------广度优先遍历-----------------------

/*
函数:BFSTraverse
参数:ALGraph G 图G
void(*Visit)(char*) 指向遍历函数的指针
返回值:无
作用:按广度优先非递归遍历图G。使用辅助队列Q和访问标志数组visited。
*/

void BFSTraverse(ALGraph G, void(*Visit)(VertexType)) {

//v是临时变量
int v, u, w;

//声明队列
Queue Q;

//设置结点访问标记数组的初始值为FALSE,表示没有顶点被访问过
for(v = 0; v < G.vexnum; ++v) {
visited[v] = FALSE;
}//for

//初始化辅助队列Q,得到一个空队列
InitQueue(Q);

//如果是连通图,只要一次循环就可以遍历全部顶点
for(v = 0; v < G.vexnum; v++) {

//v尚未访问
if(!visited[v]) {

//设置顶点v的标志为TRUE,表示顶点v已被访问
visited[v] = TRUE;

//调用访问函数Visit访问顶点v
Visit(G.vertices[v].data);

//v入队列
EnQueue(Q, v);

//队列不空
while(!QueueEmpty(Q)) {

//队头元素出队并置为u
DeQueue(Q, u);

//访问u尚未访问的邻接顶点
for(w = FirstAdjVex(G, G.vertices[u].data); w >= 0;
w = NextAdjVex(G, G.vertices[u].data, G.vertices[w].data)) {

//w为u的尚未访问的邻接顶点
if(!visited[w]) {

//设置顶点w的访问标志为TRUE
visited[w] = TRUE;

//访问顶点w
Visit(G.vertices[w].data);

//顶点w入队
EnQueue(Q, w);
}//if
}//for
}//while
}//if
}//for

//遍历结束
printf("\n");
}//BFSTraverse

源文件:基本操作及遍历测试.cpp

//**************************引入头文件*****************************
#include <stdio.h> //使用了标准库函数
#include <stdlib.h> //使用了动态内存分配函数

#include "my_constants.h" //引入自定义的符号常量,主要是状态码
#include "ALGraph.h" //引入图的邻接表存储结构定义
#include "LinkedList.cpp" //引入单链表实现,用到其中的插入、删除等操作
#include "LinkedQueue.cpp" //引入链队列实现
#include "ALGraph.cpp" //引入图的邻接表存储结构基本操作实现
#include "DFS_BFS_ALGraph.cpp" //引入图的深度优先遍历和广度优先遍历实现

int main() {

printf("----------------图的邻接表存储表示引用版演示程序------------------\n\n");

//临时变量
int j, k, n;

//图的邻接表存储方式
ALGraph g;

//临时变量
VertexType v1, v2;

//创建图g并打印初始状态
printf("->测试图G的创建:");
CreateGraph(g);
printf("图G创建成功!\n");
printf("->打印创建后的图G:\n");
Display(g);

//测试顶点和弧的插入
printf("->测试顶点和弧的插入:\n");
printf("->插入新顶点,请输入顶点(将作为有向图弧尾)的值: ");
scanf("%d", &v1);
InsertVex(g, v1);

printf("->插入与新顶点有关的弧或边,请输入弧或边数: ");
scanf("%d", &n);
for(int i = 0; i < n; i++) {

printf("->请输入新的弧头顶点的值: ");
scanf("%d", &v2);
//无向图的对称弧会自动被InsertArc函数创建,
//无需再次调用InsertArc函数创建对称的弧结点
InsertArc(g, v1, v2);
}//for

//以邻接矩阵形式打印图G,查看插入边之后图G发生了什么变化
printf("->插入新的顶点和弧之后,图G的邻接矩阵:\n");
Display(g);

printf("->基于邻接表存储结构的深度优先遍历序列(从%d顶点开始):\n", g.vertices[0].data);
DFSTraverse(g, print);
printf("\n");

printf("->基于邻接表存储结构的广度优先遍历序列(从%d顶点开始):\n", g.vertices[0].data);
BFSTraverse(g, print);
printf("\n");

//测试无向网弧的删除
printf("->测试删除一条边或弧,请输入待删除边或弧的弧尾 弧头(逗号隔开):");
scanf("%d,%d", &v1, &v2);
DeleteArc(g, v1, v2);
printf("->删除弧后的图G:\n");
Display(g);

//测试修改顶点的值
printf("->测试修改顶点的值,请输入原值 新值(逗号隔开): ");
scanf("%d,%d", &v1, &v2);
PutVex(g, v1, v2);
printf("->修改顶点后的图G:\n");
Display(g);

//测试删除顶点及相关的弧或边
printf("->测试删除顶点及相关的弧或边,请输入顶点的值: ");
scanf("%d", &v1);
DeleteVex(g, v1);
printf("->删除顶点后的图G:\n");
Display(g);

//测试销毁图G
printf("->测试销毁图G:");
DestroyGraph(g);
printf("销毁成功!");

printf("演示结束");
}//main

我测试用的是书上P157页的无向图G2。
测试程序的输入和输出:

----------------图的邻接表存储表示引用版演示程序------------------

->测试图G的创建:请输入图的类型(有向图输入0, 有向网输入1,无向图输入2,无向网输入3): 2
请输入图的顶点数,边数: 5,6
请输入5个顶点的值(用空格隔开):
1 2 3 4 5
请输入每条弧(边)的弧尾和弧头(以逗号作为间隔):
1,2
1,4
2,3
2,5
3,4
3,5
图G创建成功!
->打印创建后的图G:
此图为无向图!

图*有5个顶点,6条弧(边),它们分别是:
+----+-----------------------------------------------

|顶点| 邻接顶点(和权值)
+----+-----------------------------------------------

| 1 | →4 →2
+----+-----------------------------------------------

| 2 | →5 →3 →1
+----+-----------------------------------------------

| 3 | →5 →4 →2
+----+-----------------------------------------------

| 4 | →3 →1
+----+-----------------------------------------------

| 5 | →3 →2
+----+-----------------------------------------------


->测试顶点和弧的插入:
->插入新顶点,请输入顶点(将作为有向图弧尾)的值: 6
->插入与新顶点有关的弧或边,请输入弧或边数: 2
->请输入新的弧头顶点的值: 1
->请输入新的弧头顶点的值: 2
->插入新的顶点和弧之后,图G的邻接矩阵:
此图为无向图!

图*有6个顶点,8条弧(边),它们分别是:
+----+-----------------------------------------------

|顶点| 邻接顶点(和权值)
+----+-----------------------------------------------

| 1 | →6 →4 →2
+----+-----------------------------------------------

| 2 | →6 →5 →3 →1
+----+-----------------------------------------------

| 3 | →5 →4 →2
+----+-----------------------------------------------

| 4 | →3 →1
+----+-----------------------------------------------

| 5 | →3 →2
+----+-----------------------------------------------

| 6 | →2 →1
+----+-----------------------------------------------


->基于邻接表存储结构的深度优先遍历序列(从1顶点开始):
1 6 2 5 3 4

->基于邻接表存储结构的广度优先遍历序列(从1顶点开始):
1 6 4 2 3 5

->测试删除一条边或弧,请输入待删除边或弧的弧尾 弧头(逗号隔开):3,4
->删除弧后的图G:
此图为无向图!

图*有6个顶点,8条弧(边),它们分别是:
+----+-----------------------------------------------

|顶点| 邻接顶点(和权值)
+----+-----------------------------------------------

| 1 | →6 →4 →2
+----+-----------------------------------------------

| 2 | →6 →5 →3 →1
+----+-----------------------------------------------

| 3 | →5 →2
+----+-----------------------------------------------

| 4 | →3 →1
+----+-----------------------------------------------

| 5 | →3 →2
+----+-----------------------------------------------

| 6 | →2 →1
+----+-----------------------------------------------


->测试修改顶点的值,请输入原值 新值(逗号隔开): 3,7
->修改顶点后的图G:
此图为无向图!

图*有6个顶点,8条弧(边),它们分别是:
+----+-----------------------------------------------

|顶点| 邻接顶点(和权值)
+----+-----------------------------------------------

| 1 | →6 →4 →2
+----+-----------------------------------------------

| 2 | →6 →5 →7 →1
+----+-----------------------------------------------

| 7 | →5 →2
+----+-----------------------------------------------

| 4 | →7 →1
+----+-----------------------------------------------

| 5 | →7 →2
+----+-----------------------------------------------

| 6 | →2 →1
+----+-----------------------------------------------


->测试删除顶点及相关的弧或边,请输入顶点的值: 4
->删除顶点后的图G:
此图为无向图!

图*有5个顶点,6条弧(边),它们分别是:
+----+-----------------------------------------------

|顶点| 邻接顶点(和权值)
+----+-----------------------------------------------

| 1 | →6 →2
+----+-----------------------------------------------

| 2 | →6 →5 →7 →1
+----+-----------------------------------------------

| 7 | →5 →2
+----+-----------------------------------------------

| 5 | →7 →2
+----+-----------------------------------------------

| 6 | →2 →1
+----+-----------------------------------------------


->测试销毁图G:销毁成功!演示结束
--------------------------------

Process exited with return value 0
Press any key to continue . . .

总结:
邻接表的操作比邻接矩阵要麻烦很多,除了链表的各项基本操作之外还多出一个info,这个info指向一个存储权值的存储单元,这个存储单元需要动态的内存分配和释放,而且只在图的类型是网的时候才被创建和使用,如果是无向网,一个权值存储单元要供两个弧结点使用,释放的时候也要小心,要防止释放已经被释放的内存空间。维护info很是麻烦。

下次的文章将介绍图的最小生成树算法的实现。希望大家继续关注我的博客,再见!

附:链队列和不带头结点单链表的程序实现如下:

源文件:LinkedQueue.cpp

//------------------队列的链式存储表示----------------------- 
typedef int QElemType; //队列元素为二叉树指针类型

typedef struct QNode{ //链队列的C语言表示
QElemType data; //数据域
struct QNode * next; //指针域
}QNode,* QueuePtr;

typedef struct{
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}Queue;

//--------------------------队列的相关函数(供非递归层序遍历使用)----------------------------
/*
函数:MallocQNode
参数:无
返回值:指向新申请结点的指针
作用:为链队列结点申请内存的函数
*/

QueuePtr MallocQNode(){

//工作指针p,指向新申请的结点
QueuePtr p;

//if(!(p = (QueuePtr)malloc(sizeof(QNode)))) 相当于以下两行代码:
//p = (QueuePtr)malloc(sizeof(QNode));
//if(!p) <=> if(p != NULL)
//申请结点的内存空间,若失败则提示并退出程序
if(!(p = (QueuePtr)malloc(sizeof(QNode)))){
printf("内存分配失败,程序即将退出!\n");
exit(OVERFLOW);
}//if

//返回新申请结点的地址
return p;
}//MallocQNode

/*
函数:InitQueue
参数:Queue &Q 链队列引用
返回值:状态码,操作成功返回OK
作用:构建一个空队列 Q
*/

Status InitQueue(Queue &Q) {

//申请头结点的内存空间,并使队头和队尾指针同时指向它
Q.front = Q.rear = MallocQNode();

//由于头结点刚刚初始化,后面还没有元素结点
Q.front->next = NULL;

//头结点数据域记录了链队列长度
//由于此时链队列没有数据节点,所以将头结点数据域设为0
Q.front->data = 0;

//操作成功
return OK;
}//InitQueue

/*
函数:DestoryQueue
参数:Queue &Q 链队列引用
返回值:状态码,操作成功返回OK
作用:销毁队列Q
*/

Status DestoryQueue(Queue &Q){

//从头结点开始向后逐个释放结点内存空间
while(Q.front){ //while(Q.front) <=> while(Q.front != NULL)

//队尾指针指向被删除结点的后继结点
Q.rear = Q.front->next;

//释放Q.front指向的被删除结点的空间
free(Q.front);

//队头指针后移,指向下一个待删除结点
Q.front = Q.rear;
}//while

//操作成功
return OK;
}//DestoryQueue

/*
函数:QueueEmpty
参数:Queue Q 链队列Q
返回值:状态码,若Q为空队列,则返回TRUE;否则返回FALSE
作用:判断队列Q是否为空
*/

Status QueueEmpty(Queue Q){

//队头指针和队尾指针均指向链队列头结点表示链队列为空
if(Q.rear == Q.front){
return TRUE;
}//if
else {
return FALSE;
}//else
}//QueueEmpty

/*
函数:EnQueue
参数:Queue &Q 链队列Q的引用
QElemType e 被插入的元素e
返回值:状态码,操作成功后返回OK。
作用:插入元素e为Q的新的队尾元素
*/

Status EnQueue(Queue &Q, QElemType e){

//申请一个新的结点,并使p指向这个新结点
QueuePtr p = MallocQNode();

//将待插入元素e保存到新结点数据域
p->data = e;

//由于新结点要插在队尾,后面没有其他结点,所以后继指针域的值为NULL
p->next = NULL;

//将新结点链入到队尾
//队列要求插入操作只能发生在队尾
Q.rear->next = p;

//修正队尾指针,使之指向p所指向的新插入的结点
Q.rear = p;

//由于插入一个结点,所以存储在头结点中的队列长度+1
Q.front->data++;

//插入操作成功
return OK;
}//EnQueue

/*
函数:DeQueue
参数:Queue &Q 链队列Q的引用
QElemType &e 带回被删除结点的元素e
返回值:状态码,操作成功后返回OK。
作用:若队列不空,则删除Q的队头元素,用e返回其值
*/

Status DeQueue(Queue &Q, QElemType &e){

//注意队列为空和队列不存在的区别,队列为空,头结点一定存在,
//队列不存在时头结点一定不存在
//对空队列执行出队操作没有意义,出队操作执行前要先检查队列是否为空
if(QueueEmpty(Q)) { //if(QueueEmpty(Q)) <=> if(QueueEmpty(Q) != TRUE)
return ERROR;
}//if

//工作指针p指向队头第一个结点(不是头结点,是头结点的后继)
//队列要求删除操作只能发生在队头,所以p指向的就是待删除节点
QueuePtr p = Q.front->next;

//保存被删除结点的值
e = p->data;

//在删除操作执行前修正队头指针的位置,使之在删除结点后指向新的队头结点
Q.front->next = p->next;

//若被删除结点恰好是队尾结点,那么该结点被删除后,队列将会变成空队列
//此时刚好满足空队列条件:Q.rear == Q.front,所以要修正队尾指针的位置,使之指向头结点
if(Q.rear == p) {
Q.rear = Q.front;
}//if

//在队头指针和队尾指针的位置都调整好了之后就可以
//放心地释放p指向的结点的内存空间了
free(p);

//由于从队列中删除了一个结点,头结点存储的队列长度应当-1
Q.front->data--;

//操作成功
return OK;
}//DeQueue

/*
函数:Print
参数:ElemType e 被访问的元素
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:访问元素e的函数,通过修改该函数可以修改元素访问方式,
该函数使用时需要配合遍历函数一起使用。
*/

Status Print_Queue(QElemType e){

//指定元素的访问方式是控制台打印输出
printf("%6.2f ",e);

//操作成功
return OK;
}//Print

/*
函数:QueueTraverse
参数:Queue Q 链队列Q
Status (* visit)(QElemType) 函数指针,指向元素访问函数。
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:调用元素访问函数按出队顺序完成链队列的遍历,但并未真正执行出队操作
*/

Status QueueTraverse(Queue Q, Status (* visit)(QElemType)) {

//对空队列进行遍历操作没有意义,所以遍历操作前要先判断队列是否为空
//注意队列为空和队列不存在的区别,队列为空,头结点一定存在,
//队列不存在时头结点一定不存在
if(QueueEmpty(Q)) { //if(QueueEmpty(Q)) <=> if(QueueEmpty(Q) != TRUE)
return ERROR;
}//if

//工作指针p指向队头结点
QueuePtr p = Q.front->next;

//从队头结点开始依次访问每个结点,直到队尾
while(p) { //while(p) <=> while(p != NULL)

//调用元素访问函数
if(!visit(p->data)) {
printf("输出发生错误!\n");
return ERROR;
}//if

//工作指针p后移,指向下一个元素
p = p->next;
}//while

//输出换行,使结果清楚美观
printf("\n");

//操作成功
return OK;
}//QueueTraverse

源文件:LinkedList.cpp


//注意:此单链表不同于书上的单链表,书上的单链表是有头结点的,
// 但是此单链表无头结点,插入、删除和很多操作会有所区别。

/*
函数:InitList
参数:LinkList &L 单链表引用
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:构造一个空的不带头结点的单链表L
*/

Status InitList(LinkList &L) {

//没有头结点的空单链表头指针为空
L = NULL;

//操作成功
return OK;
}//InitList

/*
函数:DestoryList
参数:LinkList &L 单链表引用
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:若线性表L已存在则将L重置为空表。
*/

Status DestoryList(LinkList &L) {

//工作指针p
LinkList p;

//单链表L不空
while(L) {

//没有头结点的单链表头指针L指向首元结点
//p指向首元结点
p = L;

//L指向p的后继
L = L->next;

//释放首元结点
free(p);
}//while

//操作成功
return OK;
}//DestoryList

/*
函数:ListEmpty
参数:LinkList L 单链表L
返回值:状态码,若L为空表返回TRUE,否则返回FALSE
作用:判断单链表L是否为空
*/

Status ListEmpty(LinkList L) {

//头指针直接指向首元结点,若头指针为NULL,则链表为空。
if(L) { //if(L) <=> if(L != NULL)
return FALSE;
}//if
else {
return TRUE;
}//else
}//ListEmpty

/*
函数:ListLength
参数:LinkList L 单链表引用
返回值:返回L中数据元素个数
作用:得到表长
*/

int ListLength(LinkList L) {

//i是一个计数器,记录了表长
int i = 0;

//工作指针p指向单链表的首元结点
LinkList p = L;

//遍历整个单链表求出表长
while(p) { //while(p) <=> while(p != NULL)

//p指向下一个结点
p = p->next;

//找到一个结点计数器+1
i++;
}//while

//返回表长
return i;
}//ListLength

/*
函数:GetElem
参数:LinkList L 不带头结点的单链表的头指针
int i 查找第i个元素
ElemType &e 使用e带回第i个元素的值
返回值:状态码,找到第i个元素返回OK,否则返回ERROR
作用:当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
*/

Status GetElem(LinkList L, int i, ElemType &e) {

//临时变量,记录已经遍历过的元素个数
int j = 1;

//工作指针p指向单链表首元结点
LinkList p = L;

//检查参数i的值是否合法:i是否越界
if(i < 1) {
return ERROR;
}//if

//在单链表中从头开始向后查找第i个元素
//没到第i个元素,也没到表尾
//while(j < i && p) <=> while(j < i && p != NULL)
while(j < i && p) {

//计数器j+1
j++;

//p指向下一个结点
p = p->next;
}//while

//找到了第i个元素所在结点,此时p指向该结点
if(j == i) {

//复制第i个元素的值到e
e = p->data;

//操作成功
return OK;
}//if
else {
return ERROR;
}//else
}//GetElem

/*
函数:LocateElem
参数:LinkList L 不带头结点的单链表的头指针
ElemType e 查找元素e
Status(*compare)(ElemType, ElemType) 函数指针,指向元素比较函数compare()
返回值:返回单链表L中第1个与e满足关系compare()的数据元素的位序。
若这样的数据元素不存在,则返回值为0。
作用:当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
*/

int LocateElem(LinkList L, ElemType e, Status(*compare)(ElemType, ElemType)) {

//临时变量,计数器
int i = 0;

//工作指针p指向首元结点
LinkList p = L;

//在链表L中遍历每一个结点,查找值为e的元素
while(p) { //while(p) <=> while(p != NULL)

//计数器+1
i++;

//找到这样的数据元素
//if(compare(p->data, e)) <=> if(compare(p->data, e) != 0)
if(compare(p->data, e)) {
return i;
}//if

//p指向下一个结点
p = p->next;
}//while

//若没有找到值为e的结点,返回0
return 0;
}//LocateElem

/*
函数:ListInsert
参数:LinkList &L 单链表引用
int i 在第i个位置之前插入
ElemType e 被插入的元素e
返回值:状态码,操作成功返回OK,否则返回ERROR。
作用:不带头结点的单链线性表L中第i个位置之前插入元素e
*/

Status ListInsert(LinkList &L, int i, ElemType e) {

//计数器,记录查找过的结点数
int j = 1;

//p是工作指针,初始位置指向单链表首元结点
//s指向被插入结点
LinkList p = L, s;

//检查参数i的值是否合法
if(i < 1) { //i越界
return ERROR;
}//if

//创建被插入结点并使s指向新结点
s = (LinkList)malloc(sizeof(LNode));

//将被插入的值e填充到新结点s的数据域中
s->data = e;

//注意:此链表不带头结点,做插入操作需判断插入位置是否在表头
// 如果是在表头插入需要修改头指针,不在表头插入则不需要。
if(i == 1) { //插在表头

//直接把链表原有结点挂到新结点后面
s->next = L;

//使头指针L指向s指向的新结点,s成为新的首元结点
L = s;
}//if
else { //插入位置不在表头

//寻找第i-1个结点
while(p && j < i - 1) {

//p指向下一个结点
p = p->next;

//计数器j+1
j++;
}//while

//没找到第i-1个结点
if(!p) { //if(!p) <=> if(p != NULL)
//操作失败
return ERROR;
}//if

//将第i-1个结点后面的结点挂到s指向的结点后面
s->next = p->next;

//把s指向的结点连同后面的结点挂到原来的第i-1个结点后面
//s成为新的第i个结点。
p->next = s;
}//else

//操作成功
return OK;
}//ListInsert

/*
函数:ListDelete
参数:LinkList &L 单链表引用
int i 在第i个位置之前删除
ElemType &e 带回被删除的元素e
返回值:状态码,操作成功返回OK,否则返回ERROR。
作用:在不带头结点的单链线性表L中,删除第i个元素,并由e返回其值
*/

Status ListDelete(LinkList &L, int i, ElemType &e) {

//计数器,记录查找过的结点数
int j = 1;

//p是工作指针,初始值指向单链表的首元结点
//工作指针q指向被删除结点
LinkList p = L, q = NULL;

//注意:我们使用的是不带头结点的单链表,头指针直接指向首元结点
// 执行删除操作时需要判断删除位置是否发生在表头。
// 如果删除操作在表头执行,就需要修改头指针,否则不需要。
if(i == 1) { //被删除的是首元结点

//删除首元结点后,删除前的第二个元素结点将成为新的首元结点。
//所以需要修改单链表头指针L的指向,使其指向第二个结点,为删除做好准备。
L = p->next;

//保存被删除结点p的值到e
e = p->data;

//释放首元结点的内存空间
free(p);
}//if
else {

//寻找第i-1个结点,并令p指向它
//while(p->next && j < i - 1) <=> while(p->next != NULL && j < i - 1)
while(p->next && j < i - 1) {

//p指向下一个结点
p = p->next;

//计数器+1
j++;
}//while

//检查是否找到第i-1个结点,若没有找到则终止函数的执行
if(!p->next || j > i - 1) {

//操作失败
return ERROR;
}//if

//找到了第i-1个结点,p指向第i-1个结点。

//q指向p的后继,即第i个结点,也就是被删除结点
q = p->next;

//将q指向的被删除结点(第i个结点)后面的结点挂到p指示的
//第i-1个结点后面。也就是把q指向的第i个结点从链表中隔离出来。
p->next = q->next;

//保存被删除结点的元素值
e = q->data;

//释放q指向的被删除结点的内存空间
free(q);
}//else

//操作成功
return OK;
}//ListDelete

/*
函数:ListTraverse
参数:LinkList L 不带头结点单链表L的头指针
int i 在第i个位置之前删除
ElemType &e 带回被删除的元素e
返回值:状态码,操作成功返回OK,否则返回ERROR。
作用:遍历不带头结点的单链表L
*/

Status ListTraverse(LinkList L, Status(*Visit)(ElemType)) {

//工作指针p指向单链表的首元结点
LinkList p = L;

//依次对单链表中每个元素调用Visit函数进行访问依次且仅一次。
while(p) { //while(p) <=> while(p != NULL)

//一旦Visit失败则操作失败
//if(!Visit(p->data)) <=> if(Visit(p->data) == ERROR)
if(!Visit(p->data)) {
return ERROR;
}//if

//p指向下一个结点
p = p->next;
}//while

//输出换行使打印结果美观
printf("\n");
}//ListTraverse

/*
函数:Point
参数:LinkList L 单链表L的头指针(注意,单链表没有头结点)
ElemType e 在单链表中查找元素e所在位置
Status(*equal)(ElemType, ElemType) 函数指针,指向元素比较函数
LinkList &p 带回指向e结点前驱结点的指针
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:查找表L中满足条件的结点。如找到,返回指向该结点的指针,
p指向该结点的前驱(若该结点是首元结点,则p=NULL)。
如表L中无满足条件的结点,则返回NULL,p无定义。
函数equal()的两形参的关键字相等,返回OK;否则返回ERROR
*/

LinkList Point(LinkList L, ElemType e, Status(*equal)(ElemType, ElemType), LinkList &p) {

//i存储了查找元素e得到的位置
int i, j;

//查找元素e得到元素的位置i,若i=0则说明单链表中未找到元素e
//若找到元素e,则L指向找到的结点
i = LocateElem(L, e, equal);

//在单链表中找到元素e
if(i) { //if(i) <=> if(i != 0)

//若找到的是首元结点
if(i == 1) {

//首元结点没有前驱
p = NULL;

//返回指向首元结点的指针
return L;
}//if

//若找到的不是首元结点

//p指向首元结点
p = L;

//p指向找到结点的前驱
for(j = 2; j < i; j++) {
p = p->next;
}//for

//返回指向找到的结点的指针
return p->next;
}//if

//在单链表中没找到值为e的结点
return NULL;
}//Point

/*
函数:DeleteElem
参数:LinkList &L 单链表引用(注意,单链表没有头结点)
ElemType &e e携带比较条件
Status(*equal)(ElemType, ElemType) 函数指针,指向元素比较函数
返回值:状态码,删除表L中满足条件的结点成功返回TRUE;如无此结点,则返回FALSE。
作用:删除表L中满足条件的结点,并返回TRUE;如无此结点,则返回FALSE。
函数equal()的两形参的关键字相等,返回OK;否则返回ERROR
*/

Status DeleteElem(LinkList &L, ElemType &e, Status(*equal)(ElemType,ElemType)) {

//工作指针p和q
LinkList p, q;

//从单链表L中找出符合条件的结点,使q指向此结点,p指向q的前驱
//若q指向首元结点,p=NULL
q = Point(L, e, equal, p);

//找到此结点,且q指向该结点
if(q) { //if(q) <=> if(q != NULL)

//该结点不是首元结点,p指向其前驱
if(p) { //if(p) <=> if(p != NULL)

//将p作为头指针,删除第2个结点
ListDelete(p, 2, e);
}//if
else { // 该结点是首元结点

ListDelete(L, 1, e);

//操作成功
return TRUE;
}//else
}//if

//找不到此结点
return FALSE;
}//DeleteElem