在线性表中,数据元素是串起来的,每个元素都有且只有一个直接前驱和直接后继,所以存在线性关系,元素关系是一对一的,而在树型结构中,元素之间存在明显的层次关系,上一层的元素对应多个下一层元素,而下一层的元素只能对应一个上一层元素,就好像一个父亲可以有多个孩子,但是一个孩子只能有一个父亲,元素关系是一对多的,但是,这生活中,我们的数据之间的关系却是杂乱无章的,例如,一个车站,它可以去往多个其他车站,也有很多其他车站可以抵达这个车站,这样的关系是多对多的,而接下来我们要学习的图就是一种多对多的数据结构。
知识框架:
图的概念和术语
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中 G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。
注意:
在线性表中,我们把数据元素叫元素,在数中我们将数据元素叫节点,在图中我们将数据元素称为顶点,顶点和顶点之间的逻辑关系我们称为边。
各类图的定义
1.无向图
如果图中任意两个顶点之间都是无向边(无向边表示顶点到顶点之间没有方向,仅仅是存在联系),则称这样的图为无向图,下图就是一个无向图,所以连接 AD 的边我们可以表示为(A,D)也可以表示为(D,A).
2.有向图
如果图中任意两个顶点之间的边都是有向边(从顶点到顶点的边有方向,称为有向边,也称为图),则称该图为有向图,下图表示有向图,连接 AD 的边就是弧,A 是弧尾,D 是弧头,<A,D>表示弧,不能写成<D,A>。
对于上面有向图 G 来说,表示尾 G=(V,E),其中顶点集合 V={A,B,C,D};弧集合为E={<A,D>,<B,A>,<C,A>,<B,C>},在表示有向边集合时用<>,在表示无向边集合时使用(),
3.完全图
在无向图中,如果任意两个顶点都存在边,则称这样的图为无向完全图。含有 n个顶点的无向图有 n*(n-1)/2 条边,如下图
同理,在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称为有向完全图,如下图
4.子图
假如有两个图 G1
=(V
1
,E
1
)和 G
2
=(V
2
,E
2
),如果 V
2
是 V
1
的子集且 E
2
是 E
1
的子集,则称G2
是 G
1
的子图。如下:
5.连通图
若图中任意两个顶点之间都是连通的,则称该图为连通图,否则称为非连通图,如下,左图是连通图,右图是非连通图
在有向图中,若从 w 顶点到 v 顶点和从 v 顶点到 w 顶点都有通路,则称这两个顶点是强连通的,如图中任意一对顶点都是强连通的,则此图称为强连通图。
顶点的度,出度和入度
顶点的度表示以该顶点为一个端点的边的数目,入度是指有向图中以该顶点为终点的有向边的数目,出度是以顶点 v 为起点的有向边的数目。
边的权和网
在一个图中,每条边都可以标上具有某种含义的数值,该值称为该边的权值,这种边上带权值的图称为带权图,也称为网。
路径,路径长度和回路
一个顶点经过 n 条边到达另一个顶点,这个称为一条路径,路径长度就是这个路径所要经过边的数目,而起始顶点到终点顶点相同的这条路径称为回路或环,如果一个图有 n 个顶点,并且有大于 n-1 条边,则此图一定存在环。
有向树
一个顶点的入度为 0
、其余顶点的入度均为
1
的有向图,称为有向树。
图的存储结构
邻接矩阵存储
一张图既有顶点信息,又有边信息,合在一块比较困难,所以邻接矩阵采用两个数据结构来保存一张图,使用一个一维数组来保存顶点数据,边的话因为保存是顶点和顶点的联系,所以采用一个二维数组来保存。
例图:
二维数组中保存的就是边的权值信息,是横坐标顶点和纵坐标顶点的联系,在有向图图,值为 0 表示是和自身的联系,为无穷大表示没有联系。
程序定义示例:
#define MAX 2048
typedef char CHAR;
typedef int INT;
typedef struct figure //邻接矩阵
{
CHAR node[MAX]; //保存顶点
INT right[MAX][MAX]; //保存边
int node_num; //顶点个数
int side_num; //边的个数
}Figure;
初始化邻接矩阵示例:
Figure *Establish_figure()
{
Figure *f=(Figure*)malloc(sizeof(Figure));
//初始化图中数据
memset(f->node,0,MAX);
for(int i=0;i<MAX;i++)
for(int j=0;j<MAX;j++) //全部初始化为无联系
f->right[i][j]=INT32_MIN;
printf("请输入图的节点:\n");
scanf("%s",f->node); //输入顶点
getchar(); //清除回车符
f->node_num=strlen(f->node); //保存顶点个数
printf("输入节点间的数据关系,输入形式为 节点节点 边权值:\n 输入#结束\n");
while (1)
{
char c1,c2;
int i,j,num;
scanf("%c%c%d",&c1,&c2,&num);
getchar();
if(c1=='#')
break;
i=label(f,c1); //根据输入顶点找到保存顶点的数组下标 ,没有返回-1
j=label(f,c2);
if((i==-1)||(j==-1))
continue; //输入顶点错误,重新输入
f->right[i][j]=num;
f->side_num++;
}
return f;
}
//根据节点找到节点下标 ,没有返回-1
int label(Figure *f,char a)
{
int i;
for(i=0;i<f->node_num;i++)
{
if(a==f->node[i])
return i;
}
return -1;
}
邻接表存储
在对邻接矩阵的分析中我们不难发现,如果对于顶点很多,但是顶点间的联系很少的图的话,那么就会浪费很大一片空间。例如下图
为了解决这种资源浪费,我们可以采用邻接表的方式进行图表存储,其思想是使用一个一维数组用来保存顶点信息,但是这个一维数组的每个元素中不仅保存顶点信息还保存一个指向联系顶点的节点,如下图
无向图的邻接表
有向图的邻接表
程序定义示例:
#define MAX 2048
typedef char CHAR;
typedef int INT;
int Peak_num = 0; //记录顶点数量
typedef struct relation //边结构体
{
CHAR node; //联系的顶点
int subscript_1; //顶点在数组中的下标
int subscript_2; //联系顶点在数组中的下标
INT weight; //边权值
struct relation *next; //下一个出度顶点
}RM_A;
typedef struct figureNode //顶点结构体
{
CHAR node; //顶点
RM_A *List; //出度联系表
}node;
node Figure[MAX];
十字链表存储
在前面的邻接表存储中,考虑到了资源浪费的问题,可是对于顶点的出度入度却缺少关注,例如,我要查看一个顶点的出度情况,这个很容易,直接访问这个顶点后面的边节点链表就好了,但是我要查看该节点的入度情况呢,却要遍历整个表来查看,于是,考虑到这种问题,就有了我们的十字链表存储。具体改动是在我们的顶点结构体中增加一个入度联系表指针,在边结构体中增加一个入度联系表指针。
程序定义示例:
#define MAX 2048
typedef char CHAR;
typedef int INT;
int Peak_num = 0; //记录顶点数量
typedef struct relation //边结构体
{
CHAR node; //联系的顶点
int subscript_1; //顶点在数组中的下标
int subscript_2; //联系顶点在数组中的下标
INT weight; //边权值
struct relation *GoNext; //下一个出度顶点
struct relation *BeNext; //下一个入度顶点
}RM_A;
typedef struct figureNode //顶点结构体
{
CHAR node; //顶点
RM_A *GoList; //出度联系表
RM_A *BeList; //入度联系表
}node;
node Figure[MAX];
此外,除了以上三种常见的存储结构之外,还有分便无向图边操作的邻接多重表
存储
,还有克鲁斯卡尔算法要用到的
边集数组存储,
这里可以简单提一下边集数组存储,其实就是使用两个一维数组来保存图,一个一维数组用来保存顶点信息,一个一维数组用来保存边信息,其中边信息包括存储起点下标,存储终点下标和权值。如下图:
图的遍历操作
深度优先遍历
深度优先遍历,也称作深度优先搜索,简称 DFS,它的原理其实很简单,就是从一个顶点出发,然后找到下一个与之关联且未被访问的顶点,类似于树的先根遍历。
递归程序实现示例:
//深度优先算法
int Access[MAX]={0}; //判断是否被访问
void printf_Label(Figure *f)
{
if(f==NULL||f->node_num==0)
return;
int i;
for(int j=0;j<f->node_num;j++) //初始化标志数组
Access[j]=0;
printf("遍历整个图:");
for(i=0;i<f->node_num;i++) //从下标为 i 的顶点出发
{
if(Access[i] == 0)
label_Traverse(f,i); //从下标为 i 的顶点出发的遍历
}
printf("\n--------------------------\n");
}
上面程序图的存储是采用邻接矩阵存储的,在进行深度遍历操作时,我们需要定义一个标记数组用来记录哪些顶点以被访问,被访问的标 1,未被访问标 0.在开始时我们首先对标记数组进行初始化操作,表示还没访问任何顶点,然后从第一个顶点开始出发,依次访问下去,因为存在不连通图,从一个顶点不可能将不连通图的所有结点都遍历到,所以要依次遍历所有节点。
//深度遍历从 v1 这个位置开始的节点
void label_Traverse(Figure *f,int v1)
{
if(Access[v1]==0)
{
int i;
printf("%c\t",f->node[v1]); //访问顶点
Access[v1]=1; //标记访问了该顶点
for(i=Big_Children(f,v1);i<f->node_num;i=Other_Children(f,v1,i+1)) //遍历该顶点的联系顶点
{
label_Traverse(f,i); //递归
}
}
}
遍历操作采用递归操作,和树的先根遍历操作一样,先访问顶点,再访问该顶点的关联顶点,Big_Children 函数返回的是首个于该顶点关联的顶点下标,Other_Children 函数返回的下一个与该顶点关联的顶点下标。下面是这两个函数的一个示意。
//返回 v1 这个位置第一个孩子的位置
int Big_Children(Figure *f,int v1)
{
for(int i=0;i<f->node_num;i++)
{
if((f->right[v1][i]!=INT32_MIN)&&Access[i]==0)
return i;
}
return MAX;
}
//返回 v 这一条第 i 个孩子后面的那个孩子的位置
int Other_Children(Figure *f,int v,int i)
{
for(int j=i;j<f->node_num;j++)
{
if(f->right[v][j]!=INT32_MIN&&Access[j]==0)
return j;
}
return MAX;
}
堆栈程序遍历实现示例:
//深度优先算法
void printf_Label_Stack(Figure *f)
{
if(f==NULL||f->node_num==0)
return;
int i,j;
Stack *s=linkstack(); //建立堆栈
for(i=0;i<f->node_num;i++) //开始从第 i 个顶点开始遍历
{
if(Access[i]==0) //未被访问
{
push(s,i); //顶点下标进栈
while (stackisair(s)) //判断堆栈不为空
{
int t=s->top->date; //访问栈顶元素
if(Access[t]==0) //判断是否被访问
{
Access[t]=1; //标记被访问
printf("%c\t",f->node[t]); //访问顶点
}
for(j=Big_Children(f,t);j<f->node_num;i=Other_Children(f,t,j+1)) //遍历该顶点的下一个联系顶点
{
if(Access[j]==0) //未被访问
{
push(s,j); //顶点下标进栈
break;
}
}
if(j>=f->node_num) //没有可遍历顶点
{
pop(s); //出栈
}
}
}
}
deletestack(&s); //销毁栈
printf("\n");
}
//返回 v1 这个位置第一个孩子的位置
int Big_Children(Figure *f,int v1)
{
for(int i=0;i<f->node_num;i++)
{
if((f->right[v1][i]!=INT32_MIN)&&Access[i]==0)
return i;
}
return MAX;
}
//返回 v 这一条第 i 个孩子后面的那个孩子的位置
int Other_Children(Figure *f,int v,int i)
{
for(int j=i;j<f->node_num;j++)
{
if(f->right[v][j]!=INT32_MIN&&Access[j]==0)
return j;
}
return MAX;
}
广度优先遍历
广度优先遍历,也称作广度优先搜索,简称 BFS,类似树的层次遍历。相信学过树的遍历的同学就大概能找到层次遍历该怎么去设计了,我们需要利用队列层次上一层的顶点,然后再依次访问过后又将下一层与之关联的顶点入队列,话不多说,看下面示例程序。
广度优先程序示例:
//广度优先算法
void printf_Breadth(Figure *f)
{
if(f==NULL||f->node_num==0)
return;
int i;
for(i=0;i<f->node_num;i++) //初始化标记数组
Access[i]=0;
Queue *q=initqueue(); //创建队列
for(i=0;i<f->node_num;i++) //从顶点下标为 i 的顶点开始遍历
{
if(Access[i] == 0)
breadth_Traverse(f,q,i); //广度优先遍历
}
deletequeue(&q); //销毁队列
printf("\n--------------------------\n");
}
//广度遍历从 v1 这个位置开始的节点
void breadth_Traverse(Figure *f,Queue *q,int v1)
{
if(Access[v1]==0) //未被访问
{
enterqueue(q,v1); //顶点下标入队列
Access[v1]=1; //标记访问
while (airqueue(q)) //队列不为空
{
printf("%c\t",f->node[q->frist->date]); //访问队头顶点
int t=q->frist->date; //记录队头顶点下标
Outqueue(q); //出队列
for(int i=Big_Children(f,t);i<f->node_num;i=Other_Children(f,t,i+1)) //遍历出队顶点的所有联系顶点
{
if(Access[i]==0) //未访问
{
enterqueue(q,i); //顶点下标入队列
Access[i]=1; //标记访问
}
}
}
}
}