图的基本介绍
图的引入
线性表和树存在的不足:
- 线性表局限于一个直接前驱和一个直接后继的关系。
- 树也只能有一个直接前驱也就是父节点。
这两种数据结构都无法表示多对多的关系,所以我们需要引入图的概念
图的举例说明
图是一种
数据结构
,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。结点也可以称为顶点。如图:
图的常用概念
- 顶点(vertex):即组成图的各个节点。
- 边(edge):两个相关联的顶点的连线
- 路径:从u到v的一条路径是指一个序列v0,e1,v1,e2,v2,…ek,vk,其中ei的顶点为vi及vi – 1,k称作路径的长度。如果它的起止顶点相同,该路径是“闭”的,反之,则称为“开”的。一条路径称为一简单路径(simple path),如果路径中除起始与终止顶点可以重合外,所有顶点两两不等。
- 无向图:如果图的每条边无确定方向,那么得到的图称为无向图
- 有向图:如果给图的每条边规定一个方向,那么得到的图称为有向图
- 带权图:边带有权重的图。
以下为几张图片实例:
图的表示方式
邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,如下图所示:
邻接表
- 邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失。
- 邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成。
如下图:
图的构建
要求:代码实现下面的图结构(以邻接数组的形式实现):
代码实现:
package com.atschool.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* Description:
* Author:江洋大盗
* Date:2021/1/16 19:42
*/
public class Graph {
private List<String> vertexList;//存储顶点集合
private int[][] edges;//存储图对应的邻接矩阵
private int numOfEdge;//记录边的个数
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<>(n);
numOfEdge = 0;
}
public static void main(String[] args) {
//创建图对象
Graph graph = new Graph(5);
//循环的添加节点
String[] vertexes = {"A", "B", "C", "D", "E"};
for (String vertex : vertexes) {
graph.insertVertex(vertex);
}
//添加边
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 1);
graph.addEdge(1, 3, 1);
graph.addEdge(1, 4, 1);
//显示邻接矩阵
graph.showGraph();
}
// *******以下为图中的常用方法********
/**
* 添加节点
*
* @param vertex 待插入的节点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/**
* 显示邻接矩阵
*/
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
/**
* @param v1 第一个值的下标
* @param v2 第二个值的下标
* @param weight 两个值的权重
*/
public void addEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdge++;
}
}
图的深度优先遍历
深度优先遍历基本思想
图的深度优先搜索(Depth First Search);
- 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点,可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
- 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
- 显然,深度优先搜索是一个递归的过程。
代码实现深度优先遍历
/**
* 对i节点进行深度优先遍历
*
* @param isVisited 记录节点是否被访问的数组
* @param i 起始节点
*/
private void dfs(boolean[] isVisited, int i) {
//1.首先输出该节点
System.out.print(getVertexByIndex(i) + "->");
//2.将该节点设置为已经访问过
isVisited[i] = true;
//3.查找节点i的第一个邻接节点w
int w = getFirstNeighbor(i);
while (w != -1) {//说明下一个节点存在
if (!isVisited[w]) {//说明该节点没有被访问
dfs(isVisited, w);
}
//如果w节点已经被访问
w = getNextNeighbor(i, w);
}
}
/**
* 深度优先遍历
*/
public void dfs() {
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i])
dfs(isVisited, i);
}
}
图的广度优先遍历
广度优先遍历基本思想
图的广度优先搜索(Broad First Search),类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。
代码实现广度优先遍历
/**
* 对一个结点进行广度优先遍历的方法
*
* @param isVisited 记录节点是否被访问的数组
* @param i 起始节点
*/
private void bfs(boolean[] isVisited, int i) {
int u;//表示队列的头节点对应的下标
int w;//邻接节点w
//队列记录节点的访问顺序
LinkedList<Integer> queue = new LinkedList<>();
//访问节点,输出节点信息
System.out.print(getVertexByIndex(i) + "->");
//标记该节点为已经访问
isVisited[i] = true;
//将该节点加入队列
queue.addLast(i);
while (!queue.isEmpty()) {
//取出队列的头节点下标
u = queue.removeFirst();
//得到第一个邻接节点的下标w
w = getFirstNeighbor(u);
while (w != -1) {
//说明w存在
if (!isVisited[w]) {
//说明没有访问过
System.out.print(getVertexByIndex(w) + "->");
//标记为访问过
isVisited[w] = true;
//加入队列
queue.addLast(w);
}
//找w后面的下一个邻结点
w = getNextNeighbor(u, w);
}
}
}
/**
* 广度优先遍历
*/
public void bfs() {
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]) {
bfs(isVisited, i);
}
}
}
图的代码汇总
package com.atschool.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* Description:
* Author:江洋大盗
* Date:2021/1/16 19:42
*/
public class Graph {
private List<String> vertexList;//存储顶点集合
private int[][] edges;//存储图对应的邻接矩阵
private int numOfEdge;//记录边的个数
private boolean[] isVisited;//记录某个顶点是否被访问
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<>(n);
numOfEdge = 0;
}
public static void main(String[] args) {
//创建图对象
Graph graph = new Graph(5);
//循环的添加节点
String[] vertexes = {"A", "B", "C", "D", "E"};
for (String vertex : vertexes) {
graph.insertVertex(vertex);
}
//添加边
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 1);
graph.addEdge(1, 3, 1);
graph.addEdge(1, 4, 1);
//显示邻接矩阵
graph.showGraph();
System.out.println("深度优先遍历");
graph.dfs();
System.out.println("\n广度优先遍历");
graph.bfs();
}
/**
* 对i节点进行深度优先遍历
*
* @param isVisited 记录节点是否被访问的数组
* @param i 起始节点
*/
private void dfs(boolean[] isVisited, int i) {
//1.首先输出该节点
System.out.print(getVertexByIndex(i) + "->");
//2.将该节点设置为已经访问过
isVisited[i] = true;
//3.查找节点i的第一个邻接节点w
int w = getFirstNeighbor(i);
while (w != -1) {//说明下一个节点存在
if (!isVisited[w]) {//说明该节点没有被访问
dfs(isVisited, w);
}
//如果w节点已经被访问
w = getNextNeighbor(i, w);
}
}
/**
* 深度优先遍历
*/
public void dfs() {
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i])
dfs(isVisited, i);
}
}
/**
* 对一个结点进行广度优先遍历的方法
*
* @param isVisited 记录节点是否被访问的数组
* @param i 起始节点
*/
private void bfs(boolean[] isVisited, int i) {
int u;//表示队列的头节点对应的下标
int w;//邻接节点w
//队列记录节点的访问顺序
LinkedList<Integer> queue = new LinkedList<>();
//访问节点,输出节点信息
System.out.print(getVertexByIndex(i) + "->");
//标记该节点为已经访问
isVisited[i] = true;
//将该节点加入队列
queue.addLast(i);
while (!queue.isEmpty()) {
//取出队列的头节点下标
u = queue.removeFirst();
//得到第一个邻接节点的下标w
w = getFirstNeighbor(u);
while (w != -1) {
//说明w存在
if (!isVisited[w]) {
//说明没有访问过
System.out.print(getVertexByIndex(w) + "->");
//标记为访问过
isVisited[w] = true;
//加入队列
queue.addLast(w);
}
//找w后面的下一个邻结点
w = getNextNeighbor(u, w);
}
}
}
/**
* 广度优先遍历
*/
public void bfs() {
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]) {
bfs(isVisited, i);
}
}
}
//********两个得到邻接节点的方法******
/**
* @param index 当前节点的下标
* @return 如果存在就返回下一个节点的下标,否则返回-1.
*/
public int getFirstNeighbor(int index) {
for (int i = 0; i < vertexList.size(); i++) {
if (edges[index][i] > 0) {
return i;
}
}
return -1;
}
/**
* @param v1 第一个节点的下标
* @param v2 第二个节点的下标(此时,此下标已经被访问过)
* @return 根据前一个邻接结点的下标来获取下一个邻接结点, 找不到返回-1
*/
public int getNextNeighbor(int v1, int v2) {
for (int i = v2 + 1; i < vertexList.size(); i++) {
if (edges[v1][i] > 0) {
return i;
}
}
return -1;
}
// *******以下为图中的常用方法********
/**
* @return 返回顶点的个数
*/
public int getNumOfVertex() {
return vertexList.size();
}
/**
* @return 返回边的个数
*/
public int getNumOfEdge() {
return numOfEdge;
}
/**
* 显示邻接矩阵
*/
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
/**
* @param index 要得到顶点的下标
* @return 返回要找的顶点
*/
public String getVertexByIndex(int index) {
return vertexList.get(index);
}
/**
* @param v1 第一个值的下标
* @param v2 第二个值的下标
* @return 返回两个值的权重
*/
public int getWeight(int v1, int v2) {
return edges[v1][v2];
}
/**
* 添加节点
*
* @param vertex 待插入的节点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/**
* @param v1 第一个值的下标
* @param v2 第二个值的下标
* @param weight 两个值的权重
*/
public void addEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdge++;
}
}
测试结果
(注:深度优先遍历与广度优先遍历结果不一定相同,本例中因为图的形式较为简单,所以结果相同):
结语
只要能收获甜蜜,荆棘丛中也有蜜蜂忙碌的身影,未来的你一定会感谢现在努力的自己。
版权声明:本文为m0_52099927原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。