图G由顶点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合,用|V|表示图G中顶点的个数,也称图G的阶,用|E|表示图G中边的条数
注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集
有向图
若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为
无向图
若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的有序对,记为(v,w)。(v,w) 等于 (w,v)
简单图
①不存在重复的边
②不存在顶点到自身的边
多重图
图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联
顶点的度、入读、出度
对于无向图:顶点v的度是指依附于该顶点的边的条数,记为TD(v)
对于有向图:
入度是以顶点v为终点的有向边的数目,记为ID(v)
出度是以顶点v为起点的有向边的数目,记为OD(v)
顶点v的度等于其入度和出度之和,即TD(v)=ID(v)+OD(v)
顶点-顶点的关系
- 路径——顶点到顶点之间的一条路径
连通图、强连通图
若图G中 任意两个顶点都是连通的,则称图G为 连通图,否则称为非连通图
对于n个顶点的无向图G
若G是连通图,则最少有 n-1 条边
若G是非连通图,则最多可能有条边
若图中 任何一对顶点都是强连通的,则称此图为 强连通图
对于n个顶点的有向图G
若G是强连通图,则最少有n条边(形成回路)
图的局部——子图
设有两个图G(V,E)和G’=(V’,E’),若V’是V的子集,且E’是E的子集,则称G‘是G的 子图
若有满足V(G’)=V(G)—— 顶点相同的子图G’,则称其为G的 生成子图
连通分量
无向图中的 极大连通子图(子图必须连通,且包含尽可能多的顶点和边)称为连通分量
强连通分量
有向图中的 极大强连通子图(子图必须连通,且包含尽可能多的顶点和边)称为强连通分量
生成树
连通图的生成树是包含图中全部顶点的一个极小连通子图
若图中顶点数为n,则它的生成树含有n-1条边,对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路
生成森林
在非连通图中,连通分量的生成树构成了非连通图的生成森林
边的权、带权图/网
边的权——在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值
带权图/网——边上带有权值的图称为带权图,也称网
带权路径长度——当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度
几种特殊形态的图
无向完全图——无向图中任意两个顶点之间存在边
有向完全图——有向图中任意两个顶点之间都存在方向相反的两条弧
树——不存在回路,且连通的无向图
有向树——一个顶点的入度为0,其余顶点的入度均为1的有向图
总结
对于n个顶点的无向图G
所有顶点的度之和等于2|E|
若G是连通图,则最少有n-1条边(树),若 |E|>n-1,则一定有回路
若G是非连通图,则最多可能有条边
无向完全图共有条边
对于n个顶点的有向图G
所有顶点的出度之和=入度之和= |E|
所有顶点的度之和等于 2|E|
若G是强连接图,则最少有n条边(形成回路)
有向完全图共有条边
#define MaxVertexNum 100 // 顶点数目的最大值
typedef struct{
char Vex[MaxVertexNum]; // 顶点表
int Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum,arcnum; // 图的当前顶点数和边数/弧数
}MGraph;
邻接矩阵存储带权图
#define MaxVertexNum 100 // 顶点数量的最大值
#define INFINITY // 宏定义常量“无穷”
typedef char VertexType; // 顶点的数据类型
typedef int EdgeType; // 带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; // 顶点
EdgeType Edge[MaxVertexNum][MaxVertexNum]; // 边的权
int vexnum,arcnum; // 图的当前顶点数和弧数
}MGraph;
空间复杂度
O(|V|²) ——只和顶点数相关,和实际的边数无关
适合于存储稠密图
无向图的邻接矩阵是对称矩阵,可以压缩存储(只存储上三角区/下三角区)
邻接矩阵法的性质
设图G的邻接矩阵为A(矩阵元素为0/1),则Aⁿ的元素Aⁿ[ i ][ j ]等于由顶点 i 到顶点 j 的长度为 n 的路径的数目
举个:
A:
| A | B | C | D |
| A | 0 | 1 | 0 | 0 |
| B | 1 | 0 | 1 | 1 |
| C | 0 | 1 | 0 | 1 |
| D | 0 | 1 | 1 | 0 |
A² [ 1 ] [ 4 ] = a(1,1)a(1,4) + a(1,2)a(2,4) + a(1,3)a(3,4) + a(1,4)a(4,4) = 1
说明 顶点A到顶点D的长度为2的路径有1条
邻接表法——顺序+链式存储
// 边/弧
typedef struct ArcNode{
int adjvex; // 边/弧指向哪个结点
struct ArcNode *next; // 指向下一条弧的指针
}ArcNode;
// 顶点
typedef struct VNode{
VertexType data; // 顶点信息
ArcNode *first; // 第一条边/弧
}VNode,AdjList[MaxVertexNum];
// 用邻接表存储的图
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
无向图——边结点的数量是2|E|,整体空间复杂度为 O(|V|+2|E|)
有向图——边结点的数量是|E|,整体空间复杂度为 O(|V|+|E|)
邻接表 | 邻接矩阵 | |
---|---|---|
空间复杂度 | 无向图:O(∣V∣+2∣E∣);有向图:O(∣V∣+∣E∣) | O(∣V∣²) |
适用于 | 存储稀疏图 | 存储稠密图 |
表示方式 | 不唯一 | 唯一 |
计算度/出度/入度 | 计算有向图的度、入度不方便,其余很方便 | 必须遍历对应行或列 |
找相邻的边 | 找有向图的入边不方便,其余很方便 | 必须遍历对应行或列 |
Adjacent(G,x,y):判断图G是否存在边
邻接矩阵————O(1)
邻接表 ————O(1)~O(|V|)
Neighbors(G,x):列出图G中与结点x邻接的边
无向图:
邻接矩阵————O(|V|)
邻接表 ————O(1)~O(|V|)
有向图:
邻接矩阵————O(|V|)
邻接表 ————出度:O(1)~O(|V|);入度:O(|E|)
InsertVertex(G,x):在图G中插入顶点x
邻接矩阵————O(1)
邻接表 ————O(1)
DeleteVertex(G,x):在图G中删除顶点x
无向图:
邻接矩阵————O(|V|)
邻接表 ————O(1)~O(|V|)
有向图:
邻接矩阵————O(|V|)
邻接表 ————删出边:O(1)~O(|V|);删入边:O(|E|)
AddEdge(G,x,y):若无向边(x,y)或有向边
邻接矩阵————O(1)
邻接表 ————O(1)
FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号,若没有邻接点或图中不存在x,则返回-1
无向图:
邻接矩阵————O(1)~O(|V|)
邻接表 ————O(1)
有向图:
邻接矩阵————O(1)~O(|V|)
邻接表 ————找出边:O(1);找入边:O(1)~O(|E|)
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1
邻接矩阵————O(1)~O(|V|)
邻接表 ————O(1)
Get_edge_value(G,x,y):获取图G中边(x,y)或
Set_edge_value(G,x,y):设置图G中边(x,y)或
图的广度优先遍历和树的层次遍历很相似
代码实现:
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
// 广度优先遍历
void BFS(Graph G, int v){ // 从顶点v出发,广度优先遍历图G
visit(v); // 访问初始顶点v
visited[v] = true; // 对v做已访问标记
EnQueue(Q,v); // 顶点v入队列Q
while(!isEmpty(Q)){
DeQueue(Q,v); // 顶点v出队列
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
// 检测v所有邻接点
if(!visited[w]){ // w为v的未访问的邻接顶点
visit(w); // 访问顶点w
visited[w]=true; // 对w做已访问标记
EnQueue(Q,w); // 顶点w入队列
}
}
}
}
如果是非连通图,则无法遍历完所有的结点,以上的代码就出现了Bug
所以先对所有的顶点遍历,还需要加上以下代码
void BfsTraverse(Graph G){
for(i=0;i<G.vexnum;++i){
visited[i]=false; // 访问标记数组
}
InitQueue(Q); // 初始化辅助队列Q
for(i=0;i<G.vexnum;++i){ // 从0号顶点开始遍历
if(!visited[i]) // 对每一个连通分量调用依次BFS
BFS(G,i); // vi未访问过,从vi开始BFS
}
}
结论:对于无向图,调用BFS函数的次数=连通分量数
注意:
同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一
同一个图的邻接表表示方式不唯一,因此广度优先遍历序列不唯一
复杂度分析
对于邻接矩阵存储的图:O(|v|²)
对于邻接表存储的图:O(|V|+|E|)
广度优先生成树
由广度优先遍历过程确定,将遍历过程中没有经过的边去掉
由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一
遍历非连通图可以得到广度优先生成森林
图的深度优先遍历和树的先根遍历很相似
bool visited[MAX_VERTEX_NUM]; // 访问标记数组
void DFSTraverse(Graph G){
for(v=0;v<G.vexnum;++v)
visited[v]=false;
for(v=0;v<G.vexnum;++v)
if(!visited[v])
DFS(G,v)
}
void DFS(Graph G,int v){
visit(v);
visited[v]=true;
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
if(!visited[w]){
DFS(G,w);
}
}
}
结论:对于无向图,调用D FS函数的次数=连通分量数
复杂度分析
空间复杂度:O(|v|)
对于邻接矩阵存储的图:O(|v|²)
对于邻接表存储的图:O(|V|+|E|)
深度优先遍历序列
注意:
同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯一
同一个图的邻接表表示方式不唯一,因此深度优先遍历序列不唯一
深度优先生成树
由深度优先遍历过程确定,将遍历过程中没有经过的边去掉
由于邻接表的表示方式不唯一,因此基于邻接表的深度优先生成树也不唯一
遍历非连通图可以得到深度优先生成森林
无向图:
DFS/BFS函数调用次数=连通分量数
有向图:
1、若从起始顶点到其他顶点都有路径,则只需要调用1次DFS/BFS函数
2、对于强连通图,从任一顶点出发都只需要调用1次DFS/BFS函数