图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成的,通常表示为G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
注意:
1.线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点。
2.图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
各种图的定义
无向边:若顶点vi到vj之间的边没有方向,则称这条边为无向边,用无序偶对(vi,vj)来表示。
有向边:若从顶点vi到vj之间的边有方向,则称这条边为有向边,也称为弧。
如果任意两个顶点之间的边都是有向边,则称该图为有向图。
无向边用小括号()表示,而有向边则是用尖括号<>表示。
无向图中,如果任意两个顶点之间都存在边,则称该图为五向完全图。
在有向图中,如果任意两个顶点之间都存在方向相反的两条弧,则称该图为有向完全图。
有很少条边或弧的图称为稀疏图,反之称为稠密图。
与图的边或弧相关的树叫做权。这种带权的称为网。
图的顶点与边间的关系
路径的长度是路径上的边或弧的数目。
第一个顶点和最后一个顶点相同的路径称为回路或环。序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
连通图的相关术语
在无向图中,若一个顶点到另一个顶点有路径,则称两个顶点连通。如果图中任意两个点都是连通的,则该图为连通图。
无向图中的极大连通子图称为连通分量
1.要是子图
2.子图要是连通的
3.连通子图含有极大顶点数
4.具有极大顶点数的连通子图包含依附于这些顶点的所有边
在有向图中,如果每一对vi,vj和vj,vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。
一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
如果一个有向图恰有一个顶点入度为0,其余顶点的入度均为1,则是一个有向数。
一个有向图的生成森林由若干棵有向数组成,含有图中的全部顶点,但只有足以构成若干棵不相交的有向树的弧。
图的存储结构
邻接矩阵
图的邻接矩阵存储方式是用连个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
arc[i][j]= 1,若(vi,vj)属于E或<vi,vj>属于E
0,其他
v0 v1 v2 v3
v0 0 1 1 1
v1 1 0 1 0
v2 1 1 0 1
v3 1 0 1 0
怎么用邻接矩阵存图呢?输入u,v,w,让arc[u][v]=w;
arc[i][j] =Wij
=0,当i==j时
=无穷
void shuru()
{
int u,v,w;
int n;
//初始化.
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(i==j)
arc[i][j]=0;
else src[i][j]=999999999;
}
for(int i=1;i<=n;i++)
{
scanf("%d %d %d",&u,&v,&w)
arc[u][v]=w;//赋值
}
}
邻接表
用邻接矩阵在边数相对顶点来说较少的时候。这种结构会对空间造成极大的浪费。
我们把数组与链表相结合的存储方式称为邻接表。
逆邻接表:即对每个顶点vi都建立一个链接为vj为弧头的表。
邻接表的处理方法如下:
1.图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向一个邻接表的指针,以便于查找该顶点的边信息。
2.图中每个顶点vi的所有邻接点构成一个线性表,由于邻接表的个数不定,所以用的单链表存储,无向图称为vi的边表,有向图则称为顶点vi的边表,有向图则称顶点vi作为狐表的出边表。
/* 建立图的邻接表结构 */
void CreateALGraph(GraphAdjList *G)
{
int i,j,k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
for(i = 0;i < G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
{
scanf(&G->adjList[i].data); /* 输入顶点信息 */
G->adjList[i].firstedge=NULL; /* 将边表置为空表 */
}
for(k = 0;k < G->numEdges;k++)/* 建立边表 */
{
printf("输入边(vi,vj)上的顶点序号:\n");
scanf("%d,%d",&i,&j); /* 输入边(vi,vj)上的顶点序号 */
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
e->adjvex=j; /* 邻接序号为j */
e->next=G->adjList[i].firstedge; /* 将e的指针指向当前顶点上指向的结点 */
G->adjList[i].firstedge=e; /* 将当前顶点的指针指向e */
e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
e->adjvex=i; /* 邻接序号为i */
e->next=G->adjList[j].firstedge; /* 将e的指针指向当前顶点上指向的结点 */
G->adjList[j].firstedge=e; /* 将当前顶点的指针指向e */
}
}
十字链表
十字链表:邻接表和逆邻接表结合起来。
邻接多重表
ivex和jvex是与某条边衣服的两个顶点在顶点表中的下标;link指向依附顶点ivex的下一条边;jlink指向依附顶点jvex的下一条边。
边集数组
边集数组是由两个一维数组构成的。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标、终点下标和权组成。
图的遍历
我之前在看《啊哈!算法》的时候写了图的遍历的学习笔记
图的遍历--学习笔记_MogulNemenis的博客-CSDN博客
最小生成树
这个也写过学习笔记了
最小生成树--学习笔记_MogulNemenis的博客-CSDN博客
最短路径
这个也...
最短路径--学习笔记_MogulNemenis的博客-CSDN博客
拓扑排序
我们这里以一个例子来讲,例子来自哔哩哔哩某博主
算法步骤
1.如上图,首先我们要把上表,转化成AOV网
2.找到无入度的顶点(无入度,即没有箭头指向该点)
3.任选一点写入排序队列,并删除这点,以及该点射出的箭头
4.重复上面的1,2点,直到输出全部顶点或者AOV网中不存在入度为0的顶点。
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网。(AOV网中不存在回路)
算法实现
由于拓扑排序需要删除顶点,用邻接表会更方便。
构造时会出现两个结果,如果网的全部顶点都被输出,则说明它不存在环的AOV网;如果输出的顶点数少了,说明该网有环,表示AOV网。
/* 邻接矩阵结构 */
typedef struct
{
int vexs[MAXVEX];
int arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
/* 邻接表结构****************** */
typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
int weight; /* 用于存储权值,对于非网图可以不需要 */
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode /* 顶点表结点 */
{
int in; /* 顶点入度 */
int data; /* 顶点域,存储顶点信息 */
EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes,numEdges; /* 图中当前顶点数和边数 */
}graphAdjList,*GraphAdjList;
/* **************************** */
/* 拓扑排序,若GL无回路,则输出拓扑排序序列并返回1,若有回路返回0。 */
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i,k,gettop;
int top=0; /* 用于栈指针下标 */
int count=0;/* 用于统计输出顶点的个数 */
int *stack; /* 建栈将入度为0的顶点入栈 */
stack=(int *)malloc(GL->numVertexes * sizeof(int) );
for(i = 0; i<GL->numVertexes; i++)
if(0 == GL->adjList[i].in) /* 将入度为0的顶点入栈 */
stack[++top]=i;
while(top!=0)
{
gettop=stack[top--];
printf("%d -> ",GL->adjList[gettop].data);
count++; /* 输出i号顶点,并计数 */
for(e = GL->adjList[gettop].firstedge; e; e = e->next)
{
k=e->adjvex;
if( !(--GL->adjList[k].in) ) /* 将i号顶点的邻接点的入度减1,如果减1后为0,则入栈 */
stack[++top]=k;
}
}
printf("\n");
if(count < GL->numVertexes)
return ERROR;
else
return OK;
}
关键路径
在前面讲的AOV网前提下,我们来Joe少一个新的概念。在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网。
路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫做关键活动。
关键路径算法的原理
比如你有四个小时,其中有两个小时要用来写作业。但是你妈妈买了课外作业给你做,需要两个小时才能做完,那么现在你的四个小时都要写作业,不能休息。
即活动开始的最晚时间和活动开始的最早时间相等,此时活动为关键活动,活动间的路径为关键路径,吐过不等,就不是。
算法实现
nt *etv,*ltv; /* 事件最早发生时间和最迟发生时间数组 */
int *stack2; /* 用于存储拓扑序列的栈 */
int top2; /* 用于stack2的指针 */
/* 拓扑排序 */
Status TopologicalSort(GraphAdjList GL)
{ /* 若GL无回路,则输出拓扑排序序列并返回1,若有回路返回0。 */
EdgeNode *e;
int i,k,gettop;
int top=0; /* 用于栈指针下标 */
int count=0;/* 用于统计输出顶点的个数 */
int *stack; /* 建栈将入度为0的顶点入栈 */
stack=(int *)malloc(GL->numVertexes * sizeof(int) );
for(i = 0; i<GL->numVertexes; i++)
if(0 == GL->adjList[i].in) /* 将入度为0的顶点入栈 */
stack[++top]=i;
top2=0;
etv=(int *)malloc(GL->numVertexes * sizeof(int) ); /* 事件最早发生时间数组 */
for(i=0; i<GL->numVertexes; i++)
etv[i]=0; /* 初始化 */
stack2=(int *)malloc(GL->numVertexes * sizeof(int) );/* 初始化拓扑序列栈 */
printf("TopologicalSort:\t");
while(top!=0)
{
gettop=stack[top--];
printf("%d -> ",GL->adjList[gettop].data);
count++; /* 输出i号顶点,并计数 */
stack2[++top2]=gettop; /* 将弹出的顶点序号压入拓扑序列的栈 */
for(e = GL->adjList[gettop].firstedge; e; e = e->next)
{
k=e->adjvex;
if( !(--GL->adjList[k].in) ) /* 将i号顶点的邻接点的入度减1,如果减1后为0,则入栈 */
stack[++top]=k;
if((etv[gettop] + e->weight)>etv[k]) /* 求各顶点事件的最早发生时间etv值 */
etv[k] = etv[gettop] + e->weight;
}
}
printf("\n");
if(count < GL->numVertexes)
return ERROR;
else
return OK;
}