16.图结构 总览
笔记思维导图链接
参考左程云体系算法课程笔记
参考慕课网算法体系课程笔记
算法题目汇总:
1. 图结构认识
1.1 图结构简介
- 图(Graph)结构是一种非线性的数据结构
- 图在实际生活中有很多例子,比如交通运输网,地铁网络,社交网络,计算机中的状态执行(自动机)等等都可以抽象成图结构。
- 图结构比树结构复杂的非线性结构。
1.2 图结构构基本构成
**1.顶点(vertex):**图中的数据元素,如图一。
**2.边(edge):**图中连接这些顶点的线,如图一。
-
所有的顶点构成一个顶点集合,所有的边构成边的集合,
-
一个完整的图结构就是由==顶点集合和边集合组成==。图结构在数学上记为以下形式:
- G=(V,E) 或者 G=(V(G),E(G))
- 其中 V(G)表示图结构所有顶点的集合,顶点可以用不同的数字或者字母来表示。
- E(G)是图结构中所有边的集合,每条边由所连接的两个顶点来表示。
图结构中顶点集合V(G)不能为空,必须包含一个顶点,而图结构边集合可以为空,表示没有边。
- G=(V,E) 或者 G=(V(G),E(G))
1.3 图的基本概念
1.无向图(undirected graph)
- 如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。
- 由于无向图中的边没有方向性,表示边的时候对两个顶点的顺序没有要求。
- 例如顶点VI和顶点V5之间的边,可以表示为(V2, V6),也可以表示为(V6,V2)。
无向图
-
对于无向图,对应的顶点集合和边集合如下:
V(G)= {V1,V2,V3,V4,V5,V6} E(G)= {(V1,V2),(V1,V3),(V2,V6),(V2,V5),(V2,V4),(V4,V3),(V3,V5),(V5,V6)}
2.有向图(directed graph)
-
一个图结构中,边是有方向性的,那么这种图就称为有向图,
-
由于图的边有方向性,表示边的时候对两个顶点的顺序就有要求。
- 我们采用尖括号表示有向边,例如:
- <V2,V6>表示从顶点V2到顶点V6,
- <V6,V2>表示从顶点V6到顶点V2。
- 我们采用尖括号表示有向边,例如:
有向图
-
对于有向图,对应的顶点集合和边集合如下:
V(G)= {V1,V2,V3,V4,V5,V6} E(G)= {<V2,V1>,<V3,V1>,<V4,V3>,<V4,V2>,<V3,V5>,<V5,V3>,<V2,V5>,<V6,V5>,<V2,V6>,<V6,V2>}
有向图与无向图关系
注意:
无向图也可以理解成一个特殊的有向图,就是边互相指向对方节点,A指向B,B又指向A。
3.混合图(mixed graph)
一个图结构中,边同时有的是有方向性有的是无方向型的图。
在生活中混合图这种情况比较常见,比如城市道路中有些道路是单向通行,有的是双向通行。
1.4 图的常见表示方法
1.邻接表法
key:表示顶点,用数组储存
- 顶点表也就是个结构体数组,是存放顶点的结构,
- 顶点表中有data元素,存放顶点信息 firstarc是一个边结构体表指针,存放邻接点的信息。
value: 表示顶点的邻居,用链表连起来
- 邻居表是一个结构体,
- 内有adivex元素,存放邻接点的下标,
- weight存放顶点与邻接点之间线的权重,
- next是边表结构体指针,存放该顶点的下一个邻接点,next就是负责将顶点的邻接点连起来
- 有权重无序表
2.邻接矩阵法
- 无向图的邻接矩阵,两个顶点有边则为1,否则,为0;
- 因为是无向图arc[i][j] = arc[j][i],所以矩阵为对称矩阵,对角线为自己到自己的边,
- 邻接矩阵中,行之和或者列之和都为各顶点度的总数。
- 无向网图和无向图差不多,就是加了权值,两个顶点之间无边的话距离是∞。
2. 图的统一表示方法
- 虽然给定的图有不同的表示方法,根据图的基本构成可知,所有图结构均可由点集合和边集合组成
2.1 点集合类定义
顶点的度
- 连接顶点的边的数量称为该顶点的度。
- 顶点的度在有向图和无向图中具有不同的表示。
- 对于无向图,一个顶点V的度比较简单,其是连接该顶点的边的数量,记为D(V)
- 对于有向图,一个顶点的度有入度和出度之分。
- 入度:是以该顶点为端点的入边数量, 记为ID(V)。
- 出度:是以该顶点为端点的出边数量, 记为OD(V)。
- 这样,有向图中,一个顶点V的总度便是入度和出度之和,即D(V) = ID(V) + OD(V)。
- 例如,有向图中,顶点V5的入度为3,出度为1,因此,顶点V5的总度为4。
邻接顶点
- 邻接顶点是指图结构中一条边的两个顶点。
- 邻接顶点在有向图和无向图中具有不同的表示。
- 对于无向图,无方向性,两个顶点互为邻接顶点
- 对于有向图,根据连接顶点V的边的方向性,两个顶点分别称为**起始顶点(起点或始点)和结束顶点(终点)。**有向图的邻接顶点分为两类:
- 入边邻接顶点:连接该顶点的边中的起始顶点。例如,对于组成<V2,V6>这条边的两个顶点,V2是V6的入边邻接顶点。
- **出边邻接顶点:**连接该顶点的边中的结束顶点。例如,对于组成<V2,V6>这条边的两个顶点,V6是V2的出边邻接顶点。
点集合类的定义
- value: 编号, id
- in: 入度:有多少个点通过走路连向它
- out:出度: 自己直接出去的边有多少
- nexts: 出边邻接顶点:直接邻居(从自己出发能到谁)
- edges: 出边:从自己出发能找到的边
public class Node {
int value; // 点的编号id
int in; // 入度
int out; // 出度
ArrayList<Node> nexts; // 出边邻接点
ArrayList<Edge> edges; // 出边
public Node(int value) {
this.value = value;
in = 0;
out = 0;
nexts = new ArrayList<Node>();
edges = new ArrayList<Edge>();
}
}
2.2 边集合类定义
以有向边、有权重定义,其他情况可以由此结构进行更改
- weight; 边的权重
- from; 起始点
- to; 终止点
public class Edge {
int weight; //权重
Node from; // 起始顶点
Node to; // 结束顶点
public Edge(int weight, Node from, Node to) {
super();
this.weight = weight;
this.from = from;
this.to = to;
}
}
2.3 图结构定义
- 顶点属性:用哈希表,一个编号对应一个顶点
- 例如,给你一个序号,建立一个以此序号为标记的顶点城市
- 边的属性:用集合表示,去重作用
public class Graph {
HashMap<Integer, Node> nodes;
HashSet<Edge> edges;
public Graph() {
this.nodes = new HashMap<Integer, Node>();
this.edges = new HashSet<Edge>();
}
}
2.4 定义转化为统一图结构的接口示例
给定的图结构
-
给出所有边的集合,用二维数组表示,
int[][] matrix
- 每个边用一维数组表示,并有权重
- 即:[weight, from节点上面的值,to节点上面的值]
- 例如:[ 5 , 0 , 7],表示,起点0,终点7,权重为5
转换方法接口实现
1.创建自定义的空图结构,
- 往里面填数据
Graph graph = new Graph();
2.遍历二维数组的行
- 拿到每条边的信息
for(int i = 0; i < matrix.length; i ++) {
// 将数组的信息拿到
int weight = matrix[i][0];
int fromId = matrix[i][1];
int toId = matrix[i][2];
....
3.根据每条边的信息-构建图的顶点类
-
根据点id创建点
-
维护点集合中的几个变量-出边集合、出边邻接顶点集合、出度、入度
// 用这些信息构建图的顶点类 // 根据点id创建点 if(!graph.nodes.containsKey(fromId)) { graph.nodes.put(fromId, new Node(fromId)); } if(!graph.nodes.containsKey(toId)) { graph.nodes.put(toId, new Node(toId)); } Node fromNode = graph.nodes.get(fromId); Node toNode = graph.nodes.get(toId); // 维护点集合中的几个变量-出边集合、出边邻接顶点集合、出度、入度 // 出边 Edge toEdge = new Edge(weight, fromNode, toNode); fromNode.edges.add(toEdge); // 添加进出边邻接顶点集合 fromNode.nexts.add(toNode); // 更新出度、入度 fromNode.out ++; toNode.in ++;
4.根据每条边的信息-构建图的边类
// 构建图的边类
graph.edges.add(toEdge);
整体代码如下:
public class GraphGenerator {
/**
*
* @param matrix 边集合:N*3的二维数组,[weight, from节点上面的值,to节点上面的值]
* @return 自定义的统一图结构
*/
public static Graph createGraph(int[][] matrix) {
Graph graph = new Graph();
// 处理每条边的信息-二维数组的行
for(int i = 0; i < matrix.length; i ++) {
// 将数组的信息拿到
int weight = matrix[i][0];
int fromId = matrix[i][1];
int toId = matrix[i][2];
// 用这些信息构建图的顶点类
// 根据点id创建点
if(!graph.nodes.containsKey(fromId)) {
graph.nodes.put(fromId, new Node(fromId));
}
if(!graph.nodes.containsKey(toId)) {
graph.nodes.put(toId, new Node(toId));
}
Node fromNode = graph.nodes.get(fromId);
Node toNode = graph.nodes.get(toId);
// 维护点集合中的几个变量-出边集合、出边邻接顶点集合、出度、入度
// 出边
Edge toEdge = new Edge(weight, fromNode, toNode);
fromNode.edges.add(toEdge);
// 添加进出边邻接顶点集合
fromNode.nexts.add(toNode);
// 更新出度、入度
fromNode.out ++;
toNode.in ++;
// 构建图的边类
graph.edges.add(toEdge);
}
return graph;
}
}
3. 图的宽度优先跟深度优先遍历
3.1 图的宽度优先遍历
- 宽度优先只需要点就够了,需要先指定一个出发点
- 除了使用辅助队列外,还需要一个辅助集合set
- 需要使用set过滤重复点: 可能有环, 防止代码跑不完,保证一个节点不要重复进队列
- 即每次加入队列前,要检查set登记表
public class Code01_BFS {
// 从node出发,进行宽度优先遍历
public static List<List<Node>> bfs(Node start) {
List<List<Node>> res = new ArrayList<List<Node>>();
// 定义两个辅助容器
Queue<Node> queue = new PriorityQueue<Node>();
Set<Node> set = new HashSet<Node>();
// 从第一个顶点开始处理-队列中每加一个点,就在set中检查并登记一下
queue.add(start);
set.add(start);
while(!queue.isEmpty()) {
List<Node> list = new ArrayList<Node>();
int size = queue.size();
while(size > 0) {
Node cur = queue.poll();
// 处理层序遍历的顶点-每层放入list集合中
list.add(cur);
// 将改点的所有出边邻接顶点加入到队列中-先set检查
for(Node next : cur.nexts) {
if(!set.contains(cur)) {
queue.add(next);
set.add(next);
}
}
size --;
}
res.add(list);
}
return res;
}
}
3.2 图的深度优先遍历
-
相当于二叉树的前序遍历
- 可以用自定义栈迭代法 代替 使用系统的栈的递归法
- 当前弹出节点去枚举他的后代, 没有进过栈的,
- 先把父压回去, 再把后代压回去
- 因为图的顶点的出边邻接点有很多条,相当于多叉树,故,要将父压回去,便于找其他子节点
- 栈里其实记录的是 深度优先遍历的路径
- 当前弹出节点去枚举他的后代, 没有进过栈的,
- 可以用自定义栈迭代法 代替 使用系统的栈的递归法
-
同样需要set集合过滤重复顶点
- set可以防止走环路
- 可以检查一个顶点的出边邻接点(子节点)是否遍历完了
方式一:按照子节点的遍历顺序进行深度遍历
-
结果是
a,b,e,f,c,d,k
-
如果是多叉树的深度优先遍历,对从左到右的顺序有要求,可以用此方法
public static List<Node> dfs2(Node start) {
List<Node> res = new ArrayList<Node>();
if (start == null) {
return res;
}
// 使用辅助栈模拟递归-辅助集合set排重
Stack<Node> stack = new Stack<Node>();
Set<Node> set = new HashSet<Node>();
// 处理第一个顶点
stack.add(start);
set.add(start);
res.add(start); // 先处理第一个节点
while(!stack.isEmpty()) {
Node cur = stack.pop();
for(Node next : cur.nexts) {
if(!set.contains(next)) {
stack.push(cur); // 弹出节点重新压入,为处理下个子节点
stack.push(next);
set.add(next); // set中放着目前整条路径
res.add(next);
break; // 处理完一个字节点,跳出,进入下一层
}
}
}
return res;
}
方式二:子节点的遍历反顺序深度遍历
-
结果是;
a,k,c,d,b,e,f
public static List<Node> dfs(Node start) {
List<Node> res = new ArrayList<Node>();
if (start == null) {
return res;
}
// 使用辅助栈模拟递归-辅助集合set排重
Stack<Node> stack = new Stack<Node>();
Set<Node> set = new HashSet<Node>();
// 处理第一个顶点
stack.add(start);
set.add(start);
while(!stack.isEmpty()) {
Node cur = stack.pop();
res.add(cur);
for(Node next : cur.nexts) {
if(!set.contains(next)) {
stack.add(next);
set.add(next);
}
}
}
return res;
}