图(Graph)——图G是由两个集合V(G)和E(G)组成的。记为G=(V,E)
其中:V(G)是顶点的非空有限集
E(G)是边的有限集合,边是顶点的无序对或有序对
权——与图的边或弧相关的树叫权
网)带权的图叫做网
对无向图来讲,假若顶点v和顶点w之间存在一条边,则称顶点v和w互为邻接点,边(v,w)和顶点v和w相关联
顶点的度
在无向图中,顶点的度为与每个顶点相连的边数
有向图中,顶点的度分为入度和出度
入度:以该顶点为头的弧的数目
出度:以该顶点为尾的弧的数目
路径——路径是顶点的序列,顶点V到顶点V’的路径{V=Vi,0,Vi,1,……Vi,n=V’},
其中
路径长度—— 路径上的边或弧的数目
回路——第一个顶点和最后一个顶点相同的路径
简单路径——序列中的顶点不重复出现的路径
连通——从顶点V到顶点W有一条路径,则说V和W是连通的
连通图——图中任意两个顶点都是连通的
连通分量——非连通图的每一个连通部分
强连通图——有向图中,如果对每一对Vi,Vj属于V,Vi不等于Vj.从Vi到Vj和从Vj到Vi都存在路径,则称G是强连通图
假设设一个连通图有n 个顶点和e 条边,其中 n-1 条边和n 个顶点构成一个极小连通子图, 称该极小连通子图为此连通图的生成树
对非连通图,则称由各个连通分量的生成树 的集合为此非连通图的生成森林
由于"弧“是有方向的,因此称有顶点集和弧集构成的图称为有向图
若
由顶点集和边集构成的图称作无向图
假设图中有n个顶点,e条边:
假设图中有n个顶点,e条边: 特点: 网的邻接矩阵定义: 缺点::n个顶点需要 n*n 个单元存储边;空间效率为 O(n2)。对稀疏图而言尤其浪费空间。 邻接矩阵实现: 采用邻接矩阵表示法创建无向网: 算法描述: 有向图的逆邻接表: 特点: 采用邻接表表示法创建无向网: 算法描述: 邻接表表示法的优缺点: 邻接矩阵与邻接表表示法的关系: 有向图的结构表示(十字链表) 边的结构表示: 顶点的结构表示: 无向图的结构表示: 从图中某个顶点出发游历图,访遍 图中其余顶点,并且使图中的每个顶点 仅被访问一次的过程。 连通图的深度优先搜索遍历 从深度优先搜索遍历连通图的过 程类似于树的先根遍历 解决的办法是:为每个顶点设立一 个“访问标志visited[w]” 非连通图的深度优先搜索遍历 首先将图中每个顶点的访问标志设 为FALSE, 之后搜索图中每个顶点,如 果未被访问,则以该顶点为起始点,进 行深度优先搜索遍历,否则继续检查下 一顶点。 时间复杂度分析: 遍历图的过程实质上是对每个顶点查找其邻 接点的过程。其耗费的时间取决于所采用的 存储结构。当用邻接矩阵做存储结构时,查 找每个顶点的邻接点所需时间为O(n^2),当 以邻接表做存储结构时,找邻接点所需时间 为O(e)。因此,当以邻接表作存储结构时,深 度优先搜索遍历图的时间复杂度为O(n+e) 注意: 从图中的某个顶点V0出发,并在访问此 顶点之后依次访问V0的所有未被访问过的 邻接点,之后按这些顶点被访问的先后次 序依次访问它们的邻接点,直至图中所有 和V0有路径相通的顶点都被访问到。 若此时图中尚有顶点未被访问,则另选 图中一个未曾被访问的顶点作起始点,重 复上述过程,直至图中所有顶点都被访问 到为止。 时间复杂度分析: 遍历图的过程实质上是通过边或弧找邻接点 的过程,因此广度搜索遍历图的时间复杂度 和深度优先搜索遍历相同,两者不同之处仅 仅在于对顶点的访问顺序不同。 结论: 定义: 说明: 假设要在n 个城市之间建立通讯 联络网,则连通n 个城市只需要修建 n-1条线路,如何在最节省经费的前 提下建立这个通讯网? 该问题等价于: 构造网的一棵小生成树,即:在e 条 带权的边中选取n-1 条边(不构成回路), 使“权值之和”为小 算法的基本思想: 取图中任意一个顶点v 作为生成树的根, 之后往生成树上添加新的顶点w。在添加 的顶点w 和已经在生成树上的顶点v 之间 必定存在一条边,并且该边的权值在所有 连通顶点v 和w 之间的边中取值最小。之 后继续往生成树上添加顶点,直至生成树 上含有n-1 个顶点为止。 算法步骤: 算法实现:使用邻接矩阵表示图 时间复杂度分析: 假设网中有n个顶点,则第一个进行初始化的 循环语句的频度为n,第二个循环语句的频度 为n-1。其中有两个内循环:其一是在 closedge[v].lowcost中求小值,其频度为n-1 ;其二是重新选择具有小代价的边,其频 度为n。由此,普里姆算法的时间复杂度为 O(n^2),与网中的边数无关,因此适用于求 边稠密的网的小生成树。 考虑问题出发点: 具体做法: 算法描述: 算法的时间复杂度: 克鲁斯卡尔算法的时间复杂度为O(eloge),适 合于求边稀疏的网的小生成树。 Prim算法:归并顶点,与边数无关,适于稠密网 假设以有向图表示一个工程的施工图或程序 的数据流图,则图中不允许出现回路。 检查有向图中是否存在回路的方法之一,是 对有向图进行拓扑排序。 什么是拓扑排序: 对有向图进行如下操作:按照有向图给出的次序关系,将图中顶 点排成一个线性序列,对于有向图中没有 限定次序关系的顶点,则可以人为加上任 意的次序关系。 一个AOV网的拓扑序列不是唯一的 如何进行拓扑排序: 说明: 算法实现如下: 假设以有向网表示一个施工流图,弧上的权 值表示完成该项子工程所需时间。 问:哪些子工程项是“关键工程”? 即:哪些子工程项将影响整个工程的完成期限? 整个工程完成的时间为:从有向图的源点到汇点 的最长路径 这种用边表示活动的网,称AOE网。AOE网 是一个带权的有向无环图,其中,顶点表 示事件,权表示活动持续的时间 如何求关键活动: 算法的实现要点: 算法的基本思想: 依最短路径的长度递增的次序求得 各条路径 路径长度最短的最短路径的特点: 再下一条路径长度次短的最短路径的特点: 其余最短路径的特点: 求最短路径的迪杰斯特拉算法: 一般情况下, Dist[k] = <源点到顶点k 的弧上的权值> 或者= <源点到其它顶点的路径长度> +<其它顶点到顶点k 的弧上的权值>。 弗洛伊德算法的基本思想是:
若边或弧的个数e二、基本操作
CreateGraph(&G,V,VR); //按定义(V,VR)构造图
DestroyGraph(&G); //销毁图
LocateVex(G,u); //若G中存在顶点u 则返回该顶点在图中位置,否则返回其他信息
GetVex(G,V); 返回V的值
PutVex(&G,V,value); //对v赋值value
// 在一个图中,顶点是没有先后次序的,
// 但当采用某一种确定的存储方式存储后,
// 存储结构中的顶点存储次序构成顶点之间的相对次序
FirstAdjVex(G,v); //返回v的第一个邻接点 若该顶点
//在G中没有邻接点 则返回空
NextAdjVex(G,v,w);
//返回v的(相对于w的)下一个邻接点
// 若w是v的最后一个邻接点 则返回空
InsertVex(&G,v); //在图G中增加新顶点v
DeleteVex(&G,v); //删除G中顶点v及其相关的弧
InsertArc(&G,v,w); //在G中增添
图的存储结构
一、图的数组(邻接矩阵)存储表示
邻接矩阵表示法的优缺点:
优点:容易实现图的操作,如:求某顶点的度、判 断顶点之间是否有边、找顶点的邻接点等等typedef struct AreCell { //弧的定义
VRType adj; //VRType是顶点关系类型
//对无权图 用1或0表示是否相邻
//对有权图 则为权值类型
InfoType *info; // 该弧相关信息的指针
}ArcCell;
AdjMatrix[MAX_VERTEX_NUM] [MAX_VERTEX_NUM];
typedef struct { //图的定义
VertexType vexs[MAX_VERTEX_NUM]; // 顶点信息
AdjMatrix arcs; //邻接矩阵
int vexnum,arcnum; //顶点数,弧数
GraphKind kind; //图的种类标志
}MGraph;
//邻接矩阵的存储表示
//用两个数组分别存储顶点表和邻接矩阵
#define MaxInt 32767 //表示极大值 即∞
#define MAX_VERTEX_NUM100 //最大顶点数
typedef char VerTexType; //假设顶点的数据类型为字符型
typedef int VRType; (最简单的) //假设边的权值类型为整型
typedef struct{
VerTexType vexs[MVNum]; //顶点表
AdjMatrix arcs; //邻接矩阵
//或ArcCell arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
//VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
int vexnum,arcnum; //图的当前顶点数和边数
}AMGraph;
算法思想:
Status CreateUDN(AMGraph &G) {
//采用邻接矩阵表示法,创建无向网G
cin>>G.vexnum>>G.arcnum; //输入总顶点数 总边数
for(i = 0;i<G.vexnum; ++i)
cin>>G.vexs[i]; //依次输入顶点的信息
for(i = 0;i<G.vexnum; ++i) //初始化邻接矩阵 边的权值均置为极大值
for(j = 0; j<G.vexnum; ++j)
G.arcs[i][j] = MaxInt;
for(k = 0; k<G.arcnum; ++k) { //构造邻接矩阵
cin>>v1>>v2>>w; //输入一条边依附的顶点和权值
i = LocateVex(G,v1);
j = LocateVex(G,v2); //确定v1和v2在G中的位置
G.arcs[i][j].adj = w; //边
二、图的邻接表存储表示
在有向图的逆邻接 表中,对每个顶点,链接的是指向该顶点的弧。
typedef struct ArcNode {
int adjvex; //该弧所指向的顶点的位置
struct ArcNode *nextarc; // 指向下一条弧的指针
InfoType *info; // 该弧相关信息的指针
}ArcNode;
typedef structVNode {
VertexType data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧
}VNode, AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices;
int vexnum, arcnum;
intkind; // 图的种类标志
}ALGraph;
算法思想:
Status CreateUDG(ALGraph &G) {
//采用邻接表表示法 创建无向图G
cin>>G.vexnum>>G.arcnum; //输入总顶点数 总边数
for(i = 0;i<G.vexnum; ++i) { //输入各点 构造表头结点
cin>>G.vertices[i].data; //输入顶点值
G.vertices[i].firstarc = NULL; //初始化表头结点的指针域为NULL
}
for(k = 0;k<G.arcnum;++k) { //输入各边 构造邻接表
cin>>v1>>v2; //输入一条边依附的两个顶点
i = LocateVex(G,v1);
j = LocateVex(G, v2);
p1=new ArcNode; //生成一个新的边结点*p1
p1->adjvex=j; //邻接点序号为j
p1->nextarc= G.vertices[i].firstarc; G.vertices[i].firstarc=p1;
//将新结点*p1插入顶点vi的边表头部
p2=new ArcNode; //生成另一个对称的新的边结点*p2
p2->adjvex=i; //邻接点序号为i
p2->nextarc= G.vertices[j].firstarc; G.vertices[j].firstarc=p2;
// /将新结点*p2插入顶点vj的边表头部
}
return OK;
}
优点:空间效率高,容易寻找顶点的邻接点
缺点:判断两顶点间是否有边或弧,需要搜索两结点对应的单链表,没有邻接矩阵方便
对于任一确定的无向图,邻接矩阵是唯一的(行列 号与顶点编号一致),但邻接表不唯一(链接次序 与顶点编号无关)。
邻接矩阵的空间复杂度为O(n2),而邻接表的空间复 杂度为O(n+e)。三、有向图的十字链表存储表示
typedef struct ArcBox { //弧的结构表示
int tailvex.headvex;
InfoType *info;
struct ArcBox *hlink,*tlink;
}ArcBox;
typedef struct VexNode { //顶点的结构表示
VeertexType data;
ArcBox *firstin,*firstout;
}VexNode;
typedef struct {
VexNode xlist[MAX_VERTEX_NUM];
// 顶点结点(表头向量)
int vexnum, arcnum; //有向图的当前顶点数和弧数
}OLGraph;
四、无向图的邻接多重表存储表示
typedef struct EBox {
VisitIf mark; //访问标记
int ivex,jvex; //该边依附的两个顶点的位置
struct EBox *ilink,*jlinkl; //分别指向依附这两个顶点的下一条边
InfoType *info; // 该边信息指针
}
typedef struct VexBox {
VertexType data;
EBox *firstedge; // 指向第一条依附该顶点的边
}VexBox;
typedef struct { // 邻接多重表
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum, edgenum;
}AMLGraph;
图的遍历
一、深度优先搜索
从图中某个顶点V0 出发,访问此顶 点,然后依次从V0的各个未被访问的邻接 点出发深度优先搜索遍历图,直至图中所 有和V0有路径相通的顶点都被访问到
如何判别V的邻接点是否被访问:void DFS(Graph G,int v) {
//从第v个顶点出发,深度优先搜索遍历连通图G
visited[v] = TRUE;
VisitFunc(v);
for(w = FirstAdjVex(G,v);w>=0; w=NextAdjVex(G,v,w))
if (!visited[w]) DFS(G, w); // 对v的尚未访问的邻接顶点w // 递归调用DFS
}
bool visited[MAX];// 访问标志数组
Status (*VisitFunc)(int v);
//函数变量,是 一个静态分配的指针,VisitFunc指向一 个函数,
//这个函数原型为Status fun(int v),即返回类型为Status,
//形参类型为 int,VisitFunc是定义的指向具有这种函数 原型的指针。
void DFSTraverse(Graph G, Status(*Visit)(int v)){
// 对图G 作深度优先遍历
VisitFunc = Visit; //使用全局变量VisitFunc,使DFS不必设函数指针参数
for(v=0; v<G.vexnum; ++v)
visited[v] = FALSE; // 访问标志数组初始化
for(v=0; v<G.vexnum; ++v)
if(!visited[v])
DFS(G, v); // 对尚未访问的顶点调用DFS
}
二、广度优先搜索
void BFSTraverse(Graph G,Status (*Visit)(int v)) {
for(v=0; v<G.vexnum; ++v)
visited[v] = FALSE; //初始化访问标志
InitQueue(Q);// 置空的辅助队列Q
for( v=0; v<G.vexnum; ++v )
if (!visited[v]){// v 尚未访问
visited[v] = TRUE; Visit(v); // 访问v
EnQueue(Q, v);// v入队列
while(!QueueEmpty(Q)) {
DeQueue(Q, u); // 队头元素出队并置为u
for(w=FirstAdjVex(G, u); w>=0; w=NextAdjVex(G,u,w))
if ( ! visited[w]){
visited[w]=TRUE; Visit(w);
EnQueue(Q, w);// 访问的顶点w入队列
} // if
} // while
}
}
三、遍历应用举例
求一条从顶点i 到顶点s 的简单路径
voidDFSearch( int v, int s, char*PATH) {
// 从第v个顶点出发递归地深度优先遍历图G,
// 求得一条从v到s的简单路径,并记录在PATH中
visited[v] = TRUE; // 访问第v 个顶点
Append(PATH, getVertex(v));// 第v个顶点加入路径
for(w=FirstAdjVex(v); w>=0&&!found; w=NextAdjVex(v) )
if(w=s) {
found = TRUE;
Append(PATH, w);
Output(PATH);
}
else if (!visited[w])
DFSearch(w, s, PATH);
if (!found) Delete (PATH,v);// 从路径上删除第v个顶点
}
求两个顶点之间的一条路径长度短的路 径
typedef DuLinkList QueuePtr;
void InitQueue(LinkQueue &Q) {
Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode));
Q.front->next = Q.rear->prior = NULL;
}
void EnQueue( LinkQueue &Q, QelemType e ) {
p = (QueuePtr) malloc(sizeof(QNode));
p->data = e; p->next = NULL;
p->prior = Q.front;
Q.rear->next = p; Q.rear = p;
}
void DeQueue( LinkQueue &Q, QelemType &e ) {
Q.front = Q.front->next;
e = Q.front->data
}
void BFSSearch(int v,int s){
for(v=0; v<G.vexnum; ++v)
visited[v] = FALSE; //初始化访问标志
InitQueue(Q);// 置空的辅助队列Q
if (!visited[v]){// v 尚未访问
visited[v] = TRUE;
Visit(v); // 访问v
EnQueue(Q, v);// v入队列
while(!QueueEmpty(Q)) {
DeQueue(Q, u); // 队头元素出队并置为u
for(w=FirstAdjVex(G, u); w>=0&&!found; w=NextAdjVex(G,u,w))
if ( ! visited[w]){
visited[w]=TRUE;
Visit(w);
EnQueue(Q, w);// 访问的顶点w入队列
if (w==s) {
outshortpath(Q);
found=TRUE;
}
} // if
if (found)
break;
} // while
}
}
生成树
所有顶点均由边连接在一起,但不 存在回路的图叫生成树
深度优先生成树与广度优先生成树
生成森林:
非连通图每个连通分量的生成 树一起组成非连通图的生成森林。
(连通网的)最小生成树
解决方案一:普里姆算法
一般情况下所添加的顶点应满足下列条件:
在生成树的构造过程中,图中n 个 顶点分属两个集合:已落在生成树上的 顶点集U和尚未落在生成树上的顶点集 V-U,则应在所有连通U中顶点和V-U中 顶点的边中选取权值最小的边
//设置一个辅助数组,对当前V-U集 中的每个顶点,
//记录和顶点集U中顶点 相连接的代价最小的边:
struct {
VertexType adjvex; // U集中的顶点序号
VRType lowcost; // 边的权值
}closedge[MAX_VERTEX_NUM];
void MiniSpanTree_P(MGraph G, VertexType u) {
//用普里姆算法从第u个顶点出发构造网G的最小生成树
k = LocateVex ( G, u );
for ( j=0; j<G.vexnum; ++j ) // 辅助数组初始化
if(j!=k)
closedge[j] = { u, G.arcs[k][j].adj };
closedge[k].lowcost = 0; // 初始,U={u}
for(i=0; i<G.vexnum; ++i) {
// 继续向生成树上添加顶点
k = minimum(closedge); // 求出加入生成树的下一个顶点(k)
printf(closedge[k].adjvex, G.vexs[k]); // 输出生成树上一条边
closedge[k].lowcost = 0; // 第k顶点并入U集
for(j=0; j<G.vexnum; ++j) //修改其它顶点的最小边
f (G.arcs[k][j].adj < closedge[j].lowcost)
closedge[j] = { G.vexs[k], G.arcs[k][j].adj };
}
}
算法评价:T(n)=O(n²)解决方案二:克鲁斯卡尔算法
为使生成树上边的权 值之和达到最小,则应使生成树中每一条 边的权值尽可能地小。
先构造一个只含n 个顶点的子图 SG,然后从权值最小的边开始,若它的添 加不使SG 中产生回路,则在SG 上加上这 条边,如此重复,直至加上n-1 条边为止。构造非连通图ST=( V,{ } );
k = i = 0; // k 计选中的边数
while(k<n-1) {
++i;
检查边集E 中第i 条权值最小的边(u,v);
若(u,v)加入ST后不使ST中产生回路,
则输出边(u,v);且k++;
}
两种算法比较
Kruskal算法:归并边,适于稀疏网拓扑排序
这种用顶点表示活动,用弧表示活动间的 优先关系的有向图称为顶点表示活动的网 (Activity On Vertex Network),简称AOV网
取入度为零的顶点v;
while (v<>0){
printf(v);
++m;
w=FirstAdj(v);
while (w<>0) {
inDegree[w]--;
w=nextAdj(v,w);
}
取下一个入度为零的顶点v;
}
if m<n printf(“图中有回路”);
Status TopologicalSort(ALGraph G){
FindInDegree(G,indegree); //对各顶点求入度
InitStack(S);
for( i=0; i<G.vexnum; ++i)
if (!indegree[i]) Push(S, i);
//入度为零的顶点入栈
count=0; //对输出顶点计数
while(!EmptyStack(S)) {
Pop(S, i); printf(i,G.vertices[i].data);++count;
for(p=G.vertices[i].firstarc;p;p=p->nextarc){
k=p->adjvex; //取出弧头顶点在图中的位置
if(!(--indegree[k]) Push(S, k);
//弧头顶点的入度减一,新产生的入度为零的顶点入 栈
}
}
if(count<G.vexnum)
return ERROR; //图中有回路”)
else
return OK;
}
关键路径
假设第i 条弧为<j, k>
则对第i 项活动而言
“活动(弧)”的最早开始时间ee(i)
ee(i) = ve(j);
“活动(弧)”的最迟开始时间el(i)
el(i) = vl(k) –dut(<j,k>);
事件发生时间的计算公式:
ve(源点) = 0;
ve(k) = Max{ve(j) + dut(<j, k>)}
vl(汇点) = ve(汇点);
vl(j) = Min{vl(k) –dut(<j, k>)}
两点之间的最短路径问题
求从某个源点到其余各点的短路径
在这条路径上,必定只含一条弧,并且 这条弧的权值最小。
下一条路径长度次短的最短路径的特点:
它只可能有两种情况:或者是直接从源 点到该点(只含一条弧);或者是从源点经 过顶点v1,再到达该顶点(由两条弧组成)。
它可能有三种情况:或者是直接从源点到 该点(只含一条弧);或者是从源点经过顶点 v1,再到达该顶点(由两条弧组成);或者是 从源点经过顶点v2,再到达该顶点。
它或者是直接从源点到该点(只含一条 弧);或者是从源点经过已求得最短路径 的顶点,再到达该顶点。
设置辅助数组Dist,其中每个分量Dist[k] 表示当前所求得的从源点到其余各顶点k 的最短路径每一对顶点之间的短路径
从v i到v j的所有 可能存在的路径中,选 出一条长度最短的路径。