图——创建、表示、遍历(深度优先和广度优先)
本篇博文通过学习尚硅谷韩老师《Java数据结构与算法》课程所做,在此非常感谢!
概述
须知:图是一种数据结构,其中节点可以具有零个或多个相邻元素,两个节点之间的连接称为边,节点也可以称为顶点;
图的概念
- 顶点(
vertex
) - 边(
edge
) - 路径
- 无向图
- 有向图
- …
无向图
有向图
带权图
图(无向)的创建和表示
图的两种表示方式
图的创建和邻接矩阵表示的实现
package edu.hebeu.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
/**
* 图
* @author 13651
*
*/
public class Graph {
/**
* 存放图的顶点的集合
*/
private ArrayList<String> vertexList;
/**
* 存放图对应的邻接矩阵
*/
private int[][] adjacencyMatrix;
/**
* 存放图的边的数目
*/
private int edgesNum;
/**
* 存放每个节点被访问的情况:true.已访问;false.未访问
*/
private boolean[] isVisiteds;
/**
* 创建图对象的构造器
* @param n 图的顶点个数
*/
public Graph(int n) {
adjacencyMatrix = new int[n][n]; // 初始化邻接矩阵为n*n
vertexList = new ArrayList<String>(n); // 初始化存放图顶点的集合
isVisiteds = new boolean[n]; // 初始化存放每个节点访问情况的数组
}
/**
* 插入顶点的方法
* @param vertex 顶点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
/**
* @param v1 连接边的一个顶点的下标
* @param v2 连接边的另一个顶点的下标
* @param weight 边的权重
*/
public void insertEdges(int v1, int v2, int weight) {
// TODO 因为这个图是无向的,所以要如下形式添加边
adjacencyMatrix[v1][v2] = weight;
adjacencyMatrix[v2][v1] = weight;
edgesNum++; // 将边的数目++
}
/**
* 获取当前节点(i索引对应的节点)的第一个邻接节点的下标值
* @param i 当前节点
* @return 当前节点(i索引对应的节点)的第一个邻接节点的下标值,存在返回其下标值,不存在返回-1
*/
public int getFirstAdjacencyVertexIndex(int i) {
for (int j = 0; j < vertexList.size(); j++) {
if (adjacencyMatrix[i][j] > 0) { // 当邻接矩阵此时的节点值大于0,即i与j节点连通
return j;
}
}
return -1;
}
/**
* 根据前一个邻接节点的下标来获取下一个邻接节点的下标
* @param v1
* @param v2
* @return 存在返回其下标值,不存在返回-1
*/
public int getNextAdjacencyVertexIndex(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (adjacencyMatrix[v1][j] > 0) {
return j;
}
}
return -1;
}
/**
* 显示打印这个图对应的邻接矩阵
*/
public void showAdjacencyMatrix() {
for (int[] tmp : adjacencyMatrix) {
System.out.println(Arrays.toString(tmp));
}
}
/**
* 获取图中的顶点个数
* @return
*/
public int getVertexNum() {
return vertexList.size();
}
/**
* 获取图中边的个数
* @return
*/
public int getEdgesNum() {
return edgesNum;
}
/**
* 返回下标为i的顶点的数据
* @param i 顶点对应的下标
* @return
*/
public String getVertexValue(int i) {
return vertexList.get(i);
}
/**
* 获取边(连接顶点v1和v2的边)的权值
* @param v1 边的一个顶点v1的下标
* @param v2 边的另一个顶点v2的下标
* @return
*/
public int getEdgeWeight(int v1, int v2) {
return adjacencyMatrix[v1][v2];
}
}
图(无向)的遍历
介绍
所谓图的遍历,就是对节点的访问,一般有两种访问策略:1、深度优先策略;2、广度优先策略;
深度优先
基本思想
- 深度优先遍历,从初始访问节点出发,初始访问节点可能有多个邻接节点,深度优先遍历的策略就是首先访问第一个邻接节点,然后再以这个被访问的邻接节点做为初始节点,访问它的第一个邻接节点可以理解为:每次都在访问完当前节点后首先访问当前节点的第一个邻接节点;
- 我们可以看到这样的访问策略是优先向纵向挖掘深入;
- 深度优先是一个递归的过程;
算法步骤:
- 访问初始节点V,并标记节点V为已访问;
- 查找节点V的第一个邻接节点W;
- 若W存在,则继续执行下一步;若W不存在,则回到第1步,将从V的下一个节点继续;
- 若W未被访问,对W进行深度优先遍历递归(即把W当作另一个V,再依次执行上面的1、2、3步);
- (执行到此,说明W已经被访问了)查找结点V的W邻接点的下一个邻接节点,转至第3步;
代码实现,在上面的图的类中添加如下的实例方法:
/** 外部调用的深度优先遍历方法
* 对dfs深度优先遍历的重载,遍历所有的节点并进行dfs深度优先遍历
*/
public void dfs() {
isVisiteds = new boolean[getVertexNum()]; // 初始化存放每个节点访问情况的数组
// TODO 遍历所有的节点,进行深度优先遍历dfs,相当于回溯
for (int i = 0; i < getVertexNum(); i++) {
if (!isVisiteds[i]) { // 如果该节点没有访问
dfs(isVisiteds, i);
}
}
}
/**
* 深度优先遍历算法
* 对一个节点v进行广度优先的方法
* @param isVisiteds
* @param i 当前进行深度优先遍历的节点
*
* 算法步骤:
1.访问初始节点V,并标记节点V为已访问;
2.查找节点V的第一个邻接节点W;
3.若W存在,则继续执行下一步;若W不存在,则回到第1步,将从V的下一个节点继续;
4.若W未被访问,对W进行深度优先遍历递归(即把W当作另一个V,再依次执行上面的1、2、3步);
5.(执行到此,说明W已经被访问了)查找结点V的W邻接点的下一个邻接节点,转至第3步;
*/
private void dfs(boolean[] isVisiteds, int v) {
// TODO 1. 首先我们访问该节点,然后将该节点设置为已访问
System.out.print(getVertexValue(v) + " -> "); isVisiteds[v] = true;
// TODO 2. 查找节点V的第一个邻接节点W
int w = getFirstAdjacencyVertexIndex(v);
// TODO 3. 若W存在,则继续执行下一步;若W不存在,则回到第1步,将从V的下一个节点继续;
while (w != -1) { // w存在
if (!isVisiteds[w]) { // w没有被访问
dfs(isVisiteds, w); // 对w进行深度优先遍历递归
}
// TODO 5. (执行到此,说明W已经被访问了)查找结点V的W邻接点的下一个邻接节点,转至第3步;
w = getNextAdjacencyVertexIndex(v, w);
}
}
广度优先
基本思想
图的广度优先类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的节点的顺序,以便按照这个顺序来访问这些节点的邻接节点;
算法步骤:
- 访问初始节点V并标记节点V为已访问;
- 节点V入队列;
- 当队列非空时,继续执行,否则算法结束(节点V的算法结束);
- 出队列,取得头结点U;
- 查找节点U的第一个邻接节点W;
- 若节点U的邻接节点W不存在,则转到3步骤;否则循环执行下面的3步;
- 若节点W尚未被访问,则访问节点W并标记为已访问;
- 节点W入队列;
- 查找节点U的继W邻接节点后的一个邻接节点W,转到6步骤;
代码实现,在上面的图的类中添加如下的实例方法:
/* 外部调用的广度优先遍历方法
* 对bfs广度优先遍历的重载,遍历所有的节点并进行bfs广度优先遍历
*/
public void bfs() {
isVisiteds = new boolean[getVertexNum()]; // 初始化存放每个节点访问情况的数组
// TODO 遍历所有的节点,进行广度优先遍历bfs,相当于回溯
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisiteds[i]) { // 如果该节点没有被访问
bfs(isVisiteds, i);
}
}
}
/** 广度优先遍历算法
* 对一个节点v进行广度优先的方法
* @param isVisiteds
* @param v 当前进行广度优先遍历的节点
*
* 算法步骤:
1.访问初始节点V并标记节点V为已访问;
2.节点V入队列;
3.当队列非空时,继续执行,否则算法结束(节点V的算法结束);
4.出队列,取得头结点U;
5.查找节点U的第一个邻接节点W;
6.若节点U的邻接节点W不存在,则转到3步骤;否则循环执行下面的3步;
6.1.若节点W尚未被访问,则访问节点W并标记为已访问;
6.2.节点W入队列;
6.3.查找节点U的继W邻接节点后的一个邻接节点W,转到6步骤;
*/
private void bfs(boolean[] isVisiteds, int v) {
int u; // 存放队列头结点对应的下标
int w; // 邻接节点w
LinkedList<Integer> queue = new LinkedList<>(); // 队列(使用LinkedList模拟),记录节点访问的顺序
// TODO 1. 访问初始节点V并标记节点V为已访问;
System.out.print(getVertexValue(v) + " -> "); isVisiteds[v] = true;
// TODO 2. 节点V入队列
queue.addLast(v);
// TODO 3. 当节点非空时,继续执行,否则算法结束(节点V的算法结束);
while (!queue.isEmpty()) {
// TODO 4. 出队列,取得头结点U;
u = queue.removeFirst();
// TODO 5. 查找节点U的第一个邻接节点W;
w = getFirstAdjacencyVertexIndex(u);
// TODO 6. 若节点U的邻接节点W不存在,则转到3步骤;否则循环执行下面的3步;
while (w != -1) { // W存在(找到了)
// TODO 6.1. 若节点W尚未被访问,则访问节点W并标记为已访问;
if (!isVisiteds[w]) {
System.out.print(getVertexValue(w) + " -> "); isVisiteds[w] = true;
// TODO 6.2. 节点W入队列;
queue.addLast(w);
}
// TODO 以U为前驱节点,找W后面的下一个邻接节点
w = getNextAdjacencyVertexIndex(u, w); // 这条语句体现出我们的广度优先
}
}
}
测试
我们已如下的图来创建测试代码
测试代码:
package edu.hebeu;
import edu.hebeu.graph.Graph;
public class Test {
public static void main(String[] args) {
createGraph();
}
/**
* 创建图并表示为邻接矩阵的测试方法
*/
public static void createGraph() {
// TODO 图对象创建
Graph graph = new Graph(8); // 创建顶点为8个的无向图对象
// TODO 添加点
String vertexs[] = { "A", "B", "C", "D", "E", "F", "G", "H" }; // 8个顶点
for (String vertex : vertexs) {
graph.insertVertex(vertex); // 将顶点依次添加到图
}
// TODO 添加边 A-B、A-C、B-C、B-D、B-E
graph.insertEdges(0, 1, 1); // A-B
graph.insertEdges(0, 2, 1); // A-C
graph.insertEdges(1, 3, 1); // B-D
graph.insertEdges(1, 4, 1); // B-E
graph.insertEdges(2, 5, 1); // C-F
graph.insertEdges(2, 6, 1); // C-G
graph.insertEdges(5, 6, 1); // F-G
graph.insertEdges(3, 7, 1); // D-H
graph.insertEdges(4, 7, 1); // E-H
// TODO 显示图对应的邻接矩阵
System.out.println("图的邻接矩阵");
graph.showAdjacencyMatrix();
// TODO 测试深度优先遍历
System.out.print("深度优先遍历(DFS):"); graph.dfs();
// TODO 测试广度优先遍历
System.out.print("\n广度优先遍历(BFS):"); graph.bfs();
}
}
运行结果