本文章对不带权的无向图作出实现(结点间的关系采用邻接数组 / 邻接矩阵表示),新人菜鸟写文章,难免有一些疏漏或者错误,如有发现,欢迎评论区指出,在此感谢各位看官老爷。
具体代码可以在主页获取。
如果对你有帮助,可以给卑微的博主留个赞、关注、收藏 (不是)
目录
一.工具
以C/C++语言实现数据结构,编程环境Code::Blocks 17.12 ,使用C++提供标准容器queue和string ,整体使用c语言,但因为c语言的string需要采用char数组模拟,所以采用c++的string类型,为了实现方便,同时直接采用c++标准的队列,使代码更简洁,无需做一些额外操作。
在使用到c++的内容时会进行注释说明。
二.抽象数据类型定义
以string类型为节点类型(VexType) , 采用邻接数组法,实现数据类型UD_Graph (无向图);
数据对象:Node={刘某,张某,李某.......∈ VexType} ; (一些诗人的名字,string类型)
数据间关系:{Arcs[i] , i<图的边数e } (Arcs[i]元素中有2个图节点,代表之间有通路)。
(Arcs数组在存储结构定义里可以找到)
三.存储结构定义
宏定义
//(所有存储结构定义打包成头文件UD_Graph_ Structure.h)
#include <stdlib.h>
#include <stdio.h>
#include <queue> //包含C++标准的队列
#include <string> //包含C++标准的字符串
#define UNVISITED 0 //节点未访问
#define VISITED 1 //节点已访问
#define OK 0 //函数执行成功
#define ERROR -1 //函数执行失败
#define YES 1 //有朋友关系
#define NO 0 //无朋友关系
#define OVERFLOW -2 //分配内存异常
#define VexType string //节点为字符串类型,存储姓名
#define MAXSIZE 100 //最大节点数目
#define Status int //状态为int型
using namespace std; //c++语法 ,使用标准命名空间
Arcs数组定义
typedef struct
{
VexType v,w; //代表v,m两个结点存在相连接的边
}Arcs;
无向图的存储结构定义
typedef struct
{
VexType * vexs; //存放顶点的数组,VexType为节点类型
Status ** arcs; //关系数组 arcs[1][3]==1代表 位序1和位序3的节点间有通路
int n,e; //图的节点数和边数
int * tags; //标记数组,用于遍历时判断是否节点已经访问
}UD_Graph;
四.实现的基本操作定义
注:在函数参数列表中,&代表的是引用型参数,不是取地址,使用&参数效果是:在函数中对
该参数的操作相当于操作原始数据。 (详细了解请查询百度)
1.初始化含n个顶点且无边的无向图G
结果:成功返回0,否则返回-1
InitGraph(UD_Graph &G , VexType * vex , int n);
2.创建n节点,e条边的无向图G,vexs是节点信息,arcs是边信息
结果:成功返回0,否则返回-1
CreateGraph(UD_Graph &G , VexType * vexs , int n , Arcs* arcs , int e);
3.销毁一个无向图G (要求图G存在)
结果:成功返回0,否则返回-1
DestoryGraph(UD_Graph &G );
4.查找顶点v在无向图G中的位序 (要求图G存在)
结果:若图中存在v,返回v的位序,否则返回-1
Locate_vex(UD_Graph G ,VexType v);
5.取无向图G位序为k的顶点的值到w (要求图G存在 )
结果:成功返回0,否则返回-1
Get_vex(UD_Graph G,int k,VexType &w);
6.对无向图G的位序为k顶点赋值w (要求图G存在 )
结果: 赋值成功返回0,否则返回-1
Put_Vex(UD_Graph G,int k ,VexType w);
7.求无向图G中位序为k顶点的第一个邻接顶点的位序 (要求图G存在)
结果:若0 <= k < 图的节点数,且有邻接顶点,返回其位序,否则返回-1
First_AdjVex(UD_Graph G , int k);
8.m顶点为k顶点的邻接顶点,求无向图G中k顶点相对于m顶点的下一个邻接顶点的位序 (要求图G存在)
结果: 若0 <= k < 图的节点数 , 0 <= m < 图的节点数,在m之后还有k的邻接顶 点,返回其位序,否则返回-1 。
Next_Adjvex(UD_Graph G , int k , int m);
9.在无向图G中添加k到m的边 (要求图G存在)
结果:成功返回0,否则返回-1
Add_Arc(UD_Graph &G,int k ,int m);
10.在无向图G中删除k到m的边 (要求图G存在)
结果:成功返回0,否则返回-1
Delete_Arc(UD_Graph &G,int k ,int m);
11.无向图G的深度优先遍历 (要求图G存在)
结果:成功返回0,否则返回-1
DFSTraverse(UD_Graph G ,status(visit)(int) );
12.无向图G的广度优先遍历 (要求图G存在)
结果:成功返回0,否则返回-1
BFSTraverse(UD_Graph G ,Status(visit)(int) );
五.算法设计
//1.初始化操作
Status InitGraph(UD_Graph &G , VexType * vexs , int n)
//传入实例化图变量G , 存放节点的数组vexs,节点数量n
{ int i , j;
if(n<0 || n>MAXSIZE ||(n==0 && vexs==nullptr)) //传入参数不合理,直接结束
return ERROR;
G.n=n;
G.e=0;
if(n==0)
return OK;
if(nullptr== (G.vexs= new VexType[n]) )
//为图的节点数组分配内存,由于节点是string类型,只能用new开辟空间
return OVERFLOW;
for(i=0;i<n;i++)
G.vexs[i]=vexs[i]; //为图节点赋值
if(nullptr== (G.arcs=(Status**)malloc(n*sizeof(Status*) ) ) )
//为图的关系数组分配内存,n*n的数组,故采用二重指针
return OVERFLOW; //G.arcs指向存放n个指针的数组,
for(i=0;i<n;i++) //该数组每个元素G.arcs[i]都是一个 指向n个
//Status(int)类型元素 的指针
{
if(nullptr== (G.arcs[i]=(Status*)malloc(n*sizeof(Status) ) ) )
return OVERFLOW;
}
if(nullptr== (G.tags=(int*)malloc(n*sizeof(int) ) ) )
//为图的标记数组分配内存,该数组用来存放 对应节点是否已访问
return OVERFLOW;
for(i=0;i<n;i++)
{ G.tags[i]=UNVISITED; //G.tags数组初始化为UNVISITED(未访问)
for(j=0;j<n;j++)
G.arcs[i][j]=NO; //G.arcs数组初始化为NO(没有朋友关系)
}
return OK;
}
//2.创建无向图
Status CreateGraph(UD_Graph &G , VexType * vexs , int n , Arcs* arcs , int e)
//传入实例化图变量G,节点集合vexs,节点数量n,边集合arcs,边数量e
{
if(ERROR==InitGraph(G,vexs,n) ) //初始化操作失败,直接返回ERROR
return ERROR;
if(e<0 || (e==0 && arcs!=nullptr) || (e>0 && arcs==nullptr) )
//参数不合理直接返回ERROR
return ERROR;
G.e=e;
int i;
int v_num; //v节点在图节点集合的位序
int w_num; //w节点在图节点集合的位序
for(i=0;i<e;i++)
{
v_num=Locate_vex(G,arcs[i].v);
//Locate_vex函数返回输入节点v在图G节点集合中的位序 后面会定义Locate函数
w_num=Locate_vex(G,arcs[i].w);
//Locate_vex函数返回输入节点w在图G节点集合中的位序
if(v_num==ERROR || w_num==ERROR)
//如果Locate_vex函数返回ERROR,直接返回ERROR
return ERROR;
G.arcs[v_num][w_num]=YES; //数组元素G.arcs[v_num][w_num]赋值为YES,
G.arcs[w_num][v_num]=YES; //表示位序 v_num 和 w_num 的两节点有通路(朋友关系)
} //同时G.arcs[w_num][v_num]也需要赋值
return OK;
}
//3.销毁无向图
Status DestoryGraph(UD_Graph &G ) //传入无向图G
{
delete []G.vexs; //图的节点数组是string类型,释放需要用delete
G.vexs=nullptr; //释放后指针置空,防止误操作
int i;
for(i=0;i<G.n;i++)
{
free(G.arcs[i]);
//二重指针相当于指向一个 指针数组 的指针,需要先将数组中的指针释放
}
free(G.arcs); //释放数组中的指针后,释放指向该数组的指针
G.arcs=nullptr; //释放后指针置空,防止误操作
G.e=0; //边数和节点数置零
G.n=0;
free(G.tags); //释放标记数组
G.tags=nullptr; //释放后指针置空,防止误操作
return OK;
}
//4.查找v节点在图G中的位序
int Locate_vex(UD_Graph G ,VexType v) //传入无向图G,要查询的节点v
{
int i;
for(i=0;i<G.n;i++)
if(G.vexs[i]==v) //遍历节点数组,找到v返回则位序,否则最后返回ERROR
return i;
return ERROR;
}
//5.获取无向图位序为k的节点到w
Status Get_vex(UD_Graph G ,int k,VexType &w)
//传入无向图G,要获取节点的位序k, w用来接收节点信息
{
if(k>=G.n || k<0 )
return ERROR; //参数不合理直接返回ERROR
w=G.vexs[k]; //将图位序为k的节点赋值给w
return OK;
}
//6.为无向图位序为k的节点赋值w
Status Put_Vex(UD_Graph G ,int k ,VexType w)
//传入无向图G,要赋值节点的位序k, w为想赋的值
{
if(k>=G.n || k<0 )
return ERROR; //参数不合理直接返回ERROR
G.vexs[k]=w; //将w赋值给图位序为k的节点
return OK;
}
//7.查找位序为k的节点 的第一个临接点 的位序
int First_AdjVex(UD_Graph G , int k) //传入无向图G,要查找节点的位序k
{
if(k>=G.n || k<0 )
return ERROR; //参数不合理直接返回ERROR
int i;
for(i=0;i<G.n;i++)
if(G.arcs[k][i]==YES) //遍历位序为k的节点的 关系数组行,
return i; //找到的话返回位序,否则最后返回ERROR
return ERROR;
}
//8.查找无向图位序为k的节点 在位序m之后的第一个临接点
int Next_Adjvex(UD_Graph G , int k , int m)
//传入无向图G,要查找节点的位序k,要找的临接点的起始位序的前一位m
{
if(k>=G.n || k<0 || m>=G.n-1 || m<0)
return ERROR; //参数不合理直接返回ERROR
int i;
for(i=m+1;i<G.n;i++)
if(G.arcs[k][i]==YES) //从m+1开始遍历位序为k的节点的 关系数组行,
return i; //找到的话返回位序,否则最后返回ERROR
return ERROR;
}
//9.在无向图中增加 位序为k的节点 到 位序为m的节点 的边
Status Add_Arc(UD_Graph &G , int k ,int m) //传入无向图G,要添加边的节点位序k、m
{
if(k>=G.n || k<0 || m>=G.n || m<0) //参数不合理直接返回ERROR
return ERROR;
if(G.arcs[k][m]==YES || G.arcs[m][k]==YES)
{
cout << "已有此边,添加失败。" <<endl; //边已经存在,提示出错,返回ERROR
return ERROR;
}
G.arcs[k][m]=YES; //将图的关系数组元素G.arcs[k][m]和G.arcs[m][k]赋值为YES
G.arcs[m][k]=YES;
G.e++; //图的边数加一
return OK;
}
//10.在无向图中删除 位序为k的节点 到 位序为m的节点 的边
Status Delete_Arc(UD_Graph &G , int k ,int m)
//传入无向图G,要删除边的节点位序k、m
{
if(k>=G.n || k<0 || m>=G.n || m<0)
return ERROR; //参数不合理直接返回ERROR
if(G.arcs[k][m]==NO && G.arcs[m][k]==NO)
{ //边不存在,提示出错,返回ERROR
cout << "没有此边,删除失败。" <<endl;
return ERROR;
}
G.arcs[k][m]=NO; //将图的关系数组元素G.arcs[k][m]和G.arcs[m][k]赋值为NO
G.arcs[m][k]=NO;
G.e--; //图的边数减一
return OK;
}
//11.连通子图的深度优先遍历 (递归实现)
Status DFS_SubGraph(UD_Graph G,int i,Status(*visit)(string) )
//传入无向图G,起始访问的节点位序i,访问节点所用函数visit
{
int j;
if(i>=G.n || i<0)
return ERROR; //参数不合理直接返回ERROR
if( ERROR==visit(G.vexs[i]) )
return ERROR; //节点访问失败,直接返回ERROR
G.tags[i]=VISITED; //将标记数组对应元素赋值为VISITED (表示已访问)
for(j=First_AdjVex(G,i);j!=ERROR;j=Next_Adjvex(G,i,j) )
//将位序为i的节点的 第一个临接点 的位序赋值给j
{
if(UNVISITED==G.tags[j] )
//如果位序为j的节点还没访问,就对其进行子图的深度优先遍历
if(ERROR==DFS_SubGraph(G,j,visit) ) //如果遍历失败,直接返回ERROR
return ERROR;
}
return OK;
}
//12.图的深度优先遍历 (递归实现)
由于无向图可能出现不连通的情况,这会导致从某个节点出发,只能访问到这个节点所在的连通子图,永远无法访问到图另外的连通子图,所以检查标记数组,看看还有没有节点未访问。
Status DFSTraverse(UD_Graph G ,Status(*visit)(string) )
//传入无向图G,访问节点所用函数visit
{
int i;
for(i=0;i<G.n;i++)
G.tags[i]=UNVISITED; //标记数组都初始化为UNVISITED (表示未访问)
for(i=0;i<G.n;i++)
{
if(G.tags[i]==UNVISITED) //从0开始遍历标记数组,如果还没访问,
if(ERROR==DFS_SubGraph(G,i,visit) ) //就进行当前位序的 子图深度优先遍历
return ERROR; //如果遍历失败返回ERROR
}
return OK;
}
//12.无向图的广度优先遍历 (非递归实现,使用队列)
Status BFSTraverse(UD_Graph G ,Status(*visit)(string) )
//传入无向图G,访问节点所用函数visit
{
int i,j,k;
queue<int> Q; //C++标准模板库的队列,队列存储int型
for(i=0;i<G.n;i++)
G.tags[i]=UNVISITED; //标记数组都初始化为UNVISITED (表示未访问)
for(i=0;i<G.n;i++)
{
if(G.tags[i]==UNVISITED) //从0开始遍历标记数组,如果还没访问,就访问位序对应的节点
{
if(ERROR==visit(G.vexs[i]) ) //如果访问失败,直接返回ERROR
return ERROR;
G.tags[i]=VISITED; //将标记数组对应元素赋值为VISITED (表示已经访问)
Q.push(i); //将该位序入队
while(Q.empty()!=true) //如果队列非空
{
k=Q.front(); //获取队头位序到k
Q.pop(); //队头出队
for(j=First_AdjVex(G,k);j!=ERROR;j=Next_Adjvex(G,k,j) )
//将图位序为k 的第一个临接点位序赋值给j,
{ //如果赋值成功,判断位序为j的节点是否已访问
if(G.tags[j]==UNVISITED) //如果没访问,就访问该节点
{
if(ERROR==visit(G.vexs[j]) ) //访问失败,直接返回ERROR
return ERROR;
G.tags[j]=VISITED; //将标记数组对应元素赋值为VISITED (表示已访问)
Q.push(j); //该位序入队
}
}
}
}
}
return OK;
}
//14.节点访问函数 (visit)
Status VisitVex (VexType name)
{
if(name.empty()==true) //string类型提供的判空函数,如果字符串为空,返回true
return ERROR;
cout<< name <<endl; //c++语法,打印name的值
return OK; //也可使用printf
}
六.运行测试
1.实例化一个图G , 创建一个6元素节点数组 ,对G进行初始化
预期结果: 节点数G.n=6 边数G.e=0 G.tags数组元素都是0 ,图的0号节点是李白 ,5号节点是王之涣
测试结果:
2.创建边集,包含2条边(李白与孟浩然,孟浩然与王之涣)
预期结果: 节点数G.n=6 边数G.e=2 G.tags数组元素都是0
图的0号节点是李白 ,5号节点是王之涣,关系数组[0][1]代表李白到孟浩然, 应该为1,
关系数组[0][3]代表李白到王昌龄,应该是0 ,关系数组[5][1]代表王之涣到孟浩然,应该是1
测试结果:
3.销毁图
预期结果:节点数G.n=0 边数G.e=0 , tags、vexs、arcs数组访问时报错
测试结果:
4.查找节点 李白,王昌龄,张三 在图中的位序
预期结果:李白在0号位序,王昌龄在3号,张三不在图中,为-1
测试结果:
5.获取位序为2,7,4的节点到s1,s2,s3
预期结果:2号位序是高适, 7号超出图的节点数,s2没有被赋值,4号位序是岑参
测试结果:
6.给位序为0,7的节点分别赋值 杜甫,张三
预期结果:0号节点原先是李白 , 赋值后是杜甫,7号节点超出节点数,赋值失败
测试结果:
7.查找位序为1 , 5 , 3节点的第一个临接点位序
预期结果:1号节点的第一个临接点是0号, 5号节点的第一个临接点是1号,3号节点没有临接点,输出-1
测试结果:
8.查找位序为0号节点在 位序1号以后的 第一个临接点,
查找位序为1号节点在 位序3号以后的 第一个临接点,
查找位序为4号节点在 位序0号以后的 第一个临接点。
预期结果:李白有到孟浩然和王昌龄的边,1号位序是孟浩然,之后的第一个是王昌龄3
孟浩然有到李白和王之焕的边,3号位序是王昌龄,之后的第一个是王之涣5
岑参没有到其他节点的边,0号位序是李白,之后找不到临接点,返回-1
测试结果:
9.原有李白到孟浩然,孟浩然到王之涣的边,添加李白到王昌龄、李白到位序为7号的节点、李白到孟浩然的边
预期结果: 李白到王昌龄添加成功,李白到7号节点添加失败,李白到孟浩然添加失败
测试结果:
10.删除1号节点到0号节点、1号节点到6号节点,2号节点到3号节点的边。
预期结果:原先李白到孟浩然有边,arcs[0][1]是1,删除后变0,
6号节点超出允许范围,删除失败,
2号到3号节点本身没有边,删除失败。
测试结果:
11.遍历测试,该图有7个节点,分成2个连通子图
预期结果:
从0号节点开始进行子图深度优先遍历,应该输出 李白、孟浩然、王昌龄、岑参、王之涣、高适;杜甫无法访问到。
从6号节点开始进行子图深度优先遍历,应该输出杜甫,其他节点不能访问到。
深度优先遍历也是从0号节点开始遍历,所以遍历序列和 从0号节点开始子图的深度优先遍历相同,但是最后会检测tags数组,发现6号没有访问到,于是对6号进行子图深度优先遍历。
遍历序列:李白、孟浩然、王昌龄、岑参、王之涣、高适、杜甫。
广度优先遍历也是从0号节点开始遍历,遍历完一次后也是检测tags数组,发现6号未遍历,于是对6号节点进行广度优先遍历。
遍历序列:李白、孟浩然、高适、王昌龄、岑参、王之涣、杜甫。
测试结果: