图
概念
1、图:一种数据结构,表示多对多关系
2、顶点(Vertex):具有零个或多个相邻元素
3、边(Edge):两个结点之间的连接
4、无向图:顶点之间的连接没有方向,不区分入度、出度
(1)所有顶点的度数之和 = 边数的两倍
(2)n 个顶点的无向完全图有 n * (n - 1) / 2 条边
(3)n 个顶点的连通图至少有 n - 1 条边
5、有向图:顶点之间的连接带有方向,有入度、出度之分
(1)所有顶点的度数之和 = 边数的两倍
(2)所有顶点的入度之和 = 所有顶点的出度之和
(3)n 个顶点的有向完全图有 n * (n - 1) 条边
(4)n 个顶点的强连通图至少有 n 条边
6、带权图(网):边带权值
7、路径:从一个顶点到另一顶点途径的所有顶点组成的序列
(1)环:一条至少含有一条边且起点和终点相同的路径
(2)简单环:一条(除了起点和终点必须相同之外)不含有重复顶点和边的环
(3)简单路径:一条没有重复顶点的路径
8、路径长度:路径上边的数目
(1)对于不带权的图,两个顶点之间的路径长度是指该路径上所经过的边的数目
(2)对于带权的图,两个顶点之间的路径长度是指该路径上所经过的边上的权值之和
9、顶点的度数:依附于它的边的总数
10、特殊的图
(1)自环:一条连接一个顶点和其自身的边
(2)平行边:连接同一对顶点的两条边
11、多重图:含有平行边的图
12、简单图:没有平行边或自环的图
13、连通图:从任意一个顶点都存在一条路径到达另一个任意顶点
14、树:一幅无环连通图
(1)森林:互不相连的树组成的集合
(2)连通图的生成树:它的一幅子图,它含有图中的所有顶点且是一棵树
(3)图的生成树森林:它的所有连通子图的生成树的集合
15、二分图:能够将所有结点分为两部分的图,其中图的每条边所连接的两个顶点都分别属于不同的部分
16、连通性:给定一幅图,两个给定的顶点是否连通
17、连通分量:无向图中极大连通子图
(1)对于连通无向图:只有一个连通分量,即只有一个极大连通子图,就是它本身
(2)对于非连通无向图:不连通的无向图又可以分为若干个连通子图,其中有这样的连通子图,它包含了图中尽可能多的顶点以及尽可能多的边,以至于它再加上一个点或者边之后它就不连通了,此时这个图就是极大连通子图
(3)非连通图中有多个连通分量,也就是可以有多个极大连通子图
(4)极大连通子图不要求包含所有的顶点
(5)极大连通子图要求的是边和顶点都可能的多
18、极小连通子图
(1)极小连通子图和图中的另外一个定义生成树有关,即一个连通图的生成树是该连通图的顶点集所确定的极小连通子图
(2)极小连通子图为图的某一个顶点子集所确定的连通子图中,包含边最少且包含全部顶点的连通子图
(3)极小连通子图只在无向图中才存在
(4)极小连通子图中包含图中全部的顶点
(5)极小连通子图要求的是包含图中全部顶点的连通子图的边尽可能少
图的表示方式
1.邻接矩阵
(1)表示图形中顶点之间相邻关系的矩阵
(2)矩阵的行列,即二维数组下标与一维数组下标表示顶点与其他顶点,一维数组下的元素代表顶点之间边的关系
(3)邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定浪费
(4)用二维数组表示
2.邻接表
(1)邻接表的实现只关心存在的边,不关心不存在的边,因此没有空间浪费
(2)用一维数组 + 链表组成:数组下标表示顶点,数组储存的链表表示与顶点相连的顶点(在数组中的位置下标)
(3)在有向图中:邻接表:反映的是顶点出度的情况;逆邻接表:反映的是顶点的入度情况
图的遍历方式
1.深度优先搜索(Depth First Search)DFS
2.广度优先搜索(Broad First Search)BFS
深度优先搜索
1、基本思想
(1)从初始访问结点出发,初始访问结点可能有多个邻接结点,首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点
(2)访问策略:优先纵向遍历,横向遍历并访问到第一个邻接结点,就纵向跳转到该邻接结点并重复前过程
(3)深度优先搜索是一个递归的过程,只需用一个递归方法来遍历所有顶点,递归方法会标记给定的顶点并调用自己,来访问该顶点的相邻顶点列表中所有没有被标记过的顶点
(4)如果图是连通的,每个邻接链表中的元素都会被检查到
2、在访问其中一个顶点时
(1)将它标记为已访问
(2)递归地访问它的所有没有被标记过的邻居顶点
3、算法步骤
(1)访问初始结点 b,并标记结点 b 为已访问
(2)检测下一个邻接结点 c 与 b 是否存在边
(3)若 b 与 c 有边,则转入(4);若 b 与 c 无边,则以 b 的上一个结点 a,执行(2)
(4)若 c 未被访问,对 c 从(1)开始递归,即从 c 查找 d,:若 c 已被访问,则以 b 查找 d
4、深度优先搜索标记与起点连通的所有顶点所需的时间和顶点的度数之和成正比
5、使用深度优先搜索得到从给定起点到任意标记顶点的路径所需的时间与路径的长度成正比
广度优先搜索
1、基本思想
(1)类似于一个分层搜索的过程,需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
(2)访问策略:优先横向遍历,对一个结点的所有邻接结点进行横向访问,再纵向遍历,重复前面过程
2、算法步骤
(1)访问初始结点 v 并标记结点 v 为已访问
(2)结点 v 入队列尾
(3)当队列非空时,继续执行,否则当前层次节点结束遍历
(4)出队列,取得队列头结点 u
(5)查找结点 u 的第一个邻接结点 w
(6)若结点 u 的邻接结点 w 不存在,则转到(3);否则循环执行(7)(8)(9)
(7)若结点 w 尚未被访问,则访问结点 w 并标记为已访问
(8)结点w入队列
(9)查找结点 u 的继 w 邻接结点后的下一个邻接结点 x,转到(6)
3、对于从 s 可达的任意顶点 v,广度优先搜索都能找到一条从 s 到 v 的最短路径(没有其他从 s 到 v 的路径所含的边比这条路径更少)
4、广度优先搜索所需的时间在最坏情况下和 V+E 成正比
(1)广度优先搜索标记所有与 s 连通的顶点所需的时间也与它们的度数之和成正比
(2)如果图是连通的,这个和就是所有顶点的度数之和,也就是 2E
DFS、BFS 算法效率比较(V:顶点个数,E:边个数)
1、空间复杂度相同 O(V)(借用堆栈或队列)
2、时间复杂度只与存储结构(邻接表或邻接矩阵)有关,而与搜索路径无关
(1)DFS(邻接矩阵):O(V2)
(2)DFS(邻接表):O(V+ E)
(3)BFS(邻接矩阵):O(V2)
(4)BFS(邻接表):O(V+ E)
邻接矩阵的边规则
1.不带权无向图:0,代表有边;1,代表无边
2.不带权有向图:0,代表无边;1,代表该节点的出度
3.带权无向图:直接用数字代表边的权值;用极大值表示无边;自身权值为0或极大值
4.带权有向图:直接用数字代表出度的权值;用极大值表示无边;自身权值为0或极大值
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
public class UndirectedGraph {//无向图,不带权,邻接矩阵
public ArrayList<String> vertexList;//储存顶点集合
public int[][] matrix;//邻接矩阵
public int numOfEdges;//边的数目
public boolean[] isVisited;//记录下标对应结点是否被访问
//初始化
public Graph(int n) {
matrix = new int[n][n];
vertexList = new ArrayList<>(n);
numOfEdges = 0;
}
//插入顶点
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/**
* 插入边
* vertex1,vertex2表示两个顶点下标
* edge表示边,0:有直接连接;1:无直接连接
*/
public void insertEdge(int v1, int v2, int edge) {
matrix[v1][v2] = edge;
matrix[v2][v1] = edge;
numOfEdges++;
}
//显示图的矩阵
public void show() {
for (int[] row : matrix) {
System.out.println(Arrays.toString(row));
}
}
//重载bfs,用循环遍历避免非连通图
public void bfs() {
isVisited = new boolean[vertexList.size()];
//初始层次节点下标为0
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]) {//当前遍历层次结点i未被访问,进入广度优先遍历
bfs(isVisited, i);
}
}
}
//广度优先搜索
private void bfs(boolean[] isVisited, int i) {
int u;//表示队列的头节点对应的下标
int w;//当前节点的邻接结点w
LinkedList<Integer> queue = new LinkedList<>();//队列
//先输出并标记当前层次节点
System.out.print(vertexList.get(i) + "->");
isVisited[i] = true;
queue.addLast(i);//当前层次结点下标加入队列尾
while (!queue.isEmpty()) {//队列不为空
u = queue.removeFirst();//删除并返回队列头节点下标
w = getFirst(u);//获取当前层次节点的第一个邻接结点
while (w != -1) {
if (!isVisited[w]) {//w未被访问
System.out.print(vertexList.get(w) + "->");
isVisited[w] = true;//标记已经访问
queue.addLast(w);
}
//w已被访问,以u为当前层次节点,找w后面的下一个邻接劫点
w = getNext(u, w);
}
}
}
//重载dfs,用循环遍历避免非连通图
public void dfs() {
isVisited = new boolean[vertexList.size()];
//初始层次节点下标为0
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]) {//当前遍历层次结点i未被访问,进入深度优先遍历
dfs(isVisited, i);
}
}
}
//深度优先搜索
private void dfs(boolean[] isVisited, int i) {
System.out.print(vertexList.get(i) + "->");//首次访问该结点,输出
isVisited[i] = true;//将结点设置为已访问
int node = getFirst(i);//获取当前结点i的第一个邻接结点的下标
while (node != -1) {
if (!isVisited[node]) {//node未被访问
dfs(isVisited, node);//node进入递归
}
//node已被访问,在当前遍历层次结点i,寻找node之后的邻接结点
node = getNext(i, node);
}
}
//获取第一个邻接结点的下标
public int getFirst(int index) {
//index表示当前遍历层次的节点的下标
for (int i = 0; i < vertexList.size(); i++) {
if (matrix[index][i] > 0) {
return i;
}
}
return -1;//不存在,返回-1
}
//根据前一个邻接结点的下标来获取下一个邻接结点的下标
public int getNext(int v1, int v2) {
//v2前一个邻接结点的下标
for (int i = v2 + 1; i < vertexList.size(); i++) {
//v1表示当前遍历层次的节点的下标
if (matrix[v1][i] > 0) {
return i;
}
}
return -1;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战