算法与数据体系课笔记之-16.图结构概念及相关题目解析(进行中)

本文深入探讨了图结构的概念,包括无向图、有向图和混合图,以及它们的表示方法,如邻接表和邻接矩阵。重点讲解了图的统一表示方法,定义了顶点类和边类,并提供了将二维数组转换为图结构的示例代码。此外,还介绍了图的宽度优先遍历和深度优先遍历两种遍历算法,强调了遍历过程中如何避免环路和处理节点顺序。
摘要由CSDN通过智能技术生成

16.图结构 总览

笔记思维导图链接

算法与数据结构思维导图

参考左程云体系算法课程笔记
参考慕课网算法体系课程笔记

参考博客

参考博客

参考博客

算法题目汇总:

1. 图结构认识

1.1 图结构简介

  • 图(Graph)结构是一种非线性的数据结构
  • 图在实际生活中有很多例子,比如交通运输网,地铁网络,社交网络,计算机中的状态执行(自动机)等等都可以抽象成图结构。
  • 图结构比树结构复杂的非线性结构。

1.2 图结构构基本构成

**1.顶点(vertex):**图中的数据元素,如图一。

**2.边(edge):**图中连接这些顶点的线,如图一。

img

  • 所有的顶点构成一个顶点集合,所有的边构成边的集合

  • 一个完整的图结构就是由==顶点集合和边集合组成==。图结构在数学上记为以下形式:

    • G=(V,E) 或者 G=(V(G),E(G))
      • 其中 V(G)表示图结构所有顶点的集合,顶点可以用不同的数字或者字母来表示。
      • E(G)是图结构中所有边的集合每条边由所连接的两个顶点来表示

    图结构中顶点集合V(G)不能为空,必须包含一个顶点,而图结构边集合可以为空,表示没有边

1.3 图的基本概念

1.无向图(undirected graph)
  • 如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图
  • 由于无向图中的边没有方向性,表示边的时候对两个顶点的顺序没有要求
    • 例如顶点VI和顶点V5之间的边,可以表示为(V2, V6),也可以表示为(V6,V2)。

img

无向图

  • 对于无向图,对应的顶点集合和边集合如下:

    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。

    img

有向图

img

  • 对于有向图,对应的顶点集合和边集合如下:

    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就是负责将顶点的邻接点连起来
img
  • 有权重无序表

在这里插入图片描述

2.邻接矩阵法
img

img

  • 无向图的邻接矩阵,两个顶点有边则为1,否则,为0;
  • 因为是无向图arc[i][j] = arc[j][i],所以矩阵为对称矩阵,对角线为自己到自己的边,
  • 邻接矩阵中,行之和或者列之和都为各顶点度的总数。

img

  • 无向网图和无向图差不多,就是加了权值,两个顶点之间无边的话距离是∞

2. 图的统一表示方法

  • 虽然给定的图有不同的表示方法,根据图的基本构成可知,所有图结构均可由点集合和边集合组成

2.1 点集合类定义

顶点的度
  • 连接顶点的边的数量称为该顶点的度
  • 顶点的度在有向图和无向图中具有不同的表示。
    • 对于无向图,一个顶点V的度比较简单,其是连接该顶点的边的数量,记为D(V)
    • 对于有向图,一个顶点的度有入度和出度之分
      • 入度:是以该顶点为端点的入边数量, 记为ID(V)。
      • 出度:是以该顶点为端点的出边数量, 记为OD(V)。
    • 这样,有向图中,一个顶点V的总度便是入度和出度之和,即D(V) = ID(V) + OD(V)。
    • 例如,有向图中,顶点V5的入度为3,出度为1,因此,顶点V5的总度为4。

img

邻接顶点
  • 邻接顶点是指图结构中一条边的两个顶点
  • 邻接顶点在有向图和无向图中具有不同的表示。
    • 对于无向图,无方向性,两个顶点互为邻接顶点
    • 对于有向图,根据连接顶点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可以防止走环路
    • 可以检查一个顶点的出边邻接点(子节点)是否遍历完了
    img
方式一:按照子节点的遍历顺序进行深度遍历
  • 结果是

    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;
	}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值