数据结构详细笔记——图

文章目录

  • 图的定义
  • 图的存储
    • 邻接矩阵法
    • 邻接表法
    • 邻接矩阵法与邻接表法的区别
  • 图的基本操作
  • 图的遍历
    • 广度优先遍历(BFS)
    • 深度优先遍历(DFS)
    • 图的遍历和图的连通性


图的定义

图G顶点集V边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合,用|V|表示图G中顶点的个数,也称图G的阶,用|E|表示图G中边的条数

注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集

有向图
若E是有向边(也称)的有限集合时,则图G为有向图。弧是顶点的有序对,记为,其中v、w是顶点,v称为弧尾,w称为弧头。 不等于

无向图
若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)

顶点-顶点的关系

- 路径——顶点到顶点之间的一条路径

  • 回路——第一个顶点和最后一个顶点相同的路径称为回路或环
  • 简单路径——在路径序列中,顶点不重复出现的路径称为简单路径
  • 路径长度——路径上,边的数目
  • 点到点的距离——从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离;若从u到v根本不存在路径,则该距离为无穷(∞)
  • 无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的
  • 有向图中,若从顶点v到顶点w和从顶点w到顶点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是否存在边或(x,y)
    邻接矩阵————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)或有向边不存在,则向图G中添加该边
    邻接矩阵————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)或对应的权值

图的遍历

广度优先遍历(BFS)

图的广度优先遍历和树的层次遍历很相似
代码实现:

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|)

广度优先生成树
由广度优先遍历过程确定,将遍历过程中没有经过的边去掉

由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一

遍历非连通图可以得到广度优先生成森林

深度优先遍历(DFS)

图的深度优先遍历和树的先根遍历很相似

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函数

你可能感兴趣的:(数据结构,数据结构,笔记,图论)