章节目录:
一、图的相关概念
1.1 基本介绍
为什么要有图?
- 线性表局限于一个直接前驱和一个直接后继的关系;
- 树也只能有一个直接前驱也就是父节点;
- 当我们需要表示多对多的关系时, 这里我们就用到了图。
1.2 举例说明
- 图是一种数据结构,其中节点可以具有零个或多个相邻元素。两个节点之间的连接称为边,节点也可以称为顶点。
- 示意图:
1.3 常用概念
- 顶点(vertex)
- 边(edge)
- 路径
- 无向图
- 有向图
- 带权图
二、图的表示方式
图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)。
2.1 邻接矩阵
-
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于 n 个顶点的图而言,矩阵是的
row
和col
表示的是 1…n 个点。 -
示意图:
2.2 邻接表
- 邻接矩阵需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在的,会造成空间的一定损失。
- 邻接表的实现只关心存在的边,不关心不存在的边,因此没有空间浪费,邻接表由数组+链表组成 。
- 示意图:
三、入门案例
需求:实现如下图的结构。
- 示意图:
- 思路分析:
- 使用集合存储顶点;
- 通过二维数组保存矩阵。
- 代码示例:
public class GraphDemo {
public static void main(String[] args) {
// 顶点元素。
String[] vertexes = {"A", "B", "C", "D", "E"};
// 5个顶点的图。
Graph graph = new Graph(5);
// 循环的添加顶点。
for (String vertex : vertexes) {
graph.insertVertex(vertex);
}
// 手动添加边:
// A-B A-C B-C B-D B-E
graph.insertEdge(0, 1, 1); // A-B(A下标0;B下标1;有连接则权值1)
graph.insertEdge(0, 2, 1); // A-C(A下标0;C下标2;有连接则权值1)
graph.insertEdge(1, 2, 1); // 其它同理....
graph.insertEdge(1, 3, 1); //
graph.insertEdge(1, 4, 1); //
// 显示添加结果。
graph.showGraph();
// [0, 1, 1, 0, 0]
// [1, 0, 1, 1, 1]
// [1, 1, 0, 0, 0]
// [0, 1, 0, 0, 0]
// [0, 1, 0, 0, 0]
}
}
class Graph {
/**
* 存储顶点集合。
*/
List<String> vertexes;
/**
* 存储图对应的邻结矩阵。
*/
int[][] edges;
/**
* 表示边的数目。
*/
int numOfEdges;
/**
* 图的构造器。
*
* @param n 表示 n 个顶点。
*/
public Graph(int n) {
this.edges = new int[n][n];
this.vertexes = new ArrayList<>();
this.numOfEdges = 0;
}
/**
* 显示图对应的矩阵。
*/
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
/**
* 插入顶点。
*
* @param vertex 顶点
*/
public void insertVertex(String vertex) {
vertexes.add(vertex);
}
/**
* 添加边。
*
* @param v1 表示点的下标即是第几个顶点 举例:假设要表示"A"、"B",则元素"A"对应的下标是0;元素"B"对应的下标是1
* @param v2 第二个顶点对应的下标
* @param weight 表示权值 (这里使用0表示无连接;1表示有直线连接。)
*/
public void insertEdge(int v1, int v2, int weight) {
// 因为是无向图,则需要进行双向添加权值。
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
}
四、深度优先遍历
4.1 基本介绍
-
所谓图的遍历,即是对节点的访问,一个图有那么多个节点,如何遍历这些节点,需要特定策略。
-
一般有两种访问策略:它们分别是深度优先遍历、广度优先遍历 。
4.2 遍历思路
图的深度优先搜索(Depth First Search)。
- 深度优先遍历,从初始访问节点出发,初始访问节点可能有多个邻接节点,深度优先遍历的策略就是首先访问第一个邻接节点,然后再以这个被访问的邻接节点作为初始节点,访问它的第一个邻接节点(可以这样理解:每次都在访问完当前节点后,首先访问当前节点的第一个邻接节点)。
- 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个节点的所有邻接节点进行横向访问。
- 显然,深度优先搜索是一个递归的过程。
4.3 算法步骤
- 访问初始节点,并标记该节点为已访问;
- 查找该节点的第一个邻接节点;
- 若存在邻接节点,则继续执行下面步骤,如果不存在,则回到第 1 步,将从初始节点的下一个节点继续;
- 若邻接节点未被访问,对该节点进行深度优先遍历递归(即把当前邻接节点当做另一个初始节点,然后进行步骤 123);
- 查找已访问邻接节点的下一个邻接节点,转到步骤 3。
- 示意图:
4.4 代码示例
/**
* 深度优先遍历。
*
* @param isVisited 标记是否被访问的数组
* @param i 下标
*/
private void dfs(boolean[] isVisited, int i) {
// 访问并输出节点。
System.out.print(vertexes.get(i) + "->");
// 设置为已访问。
isVisited[i] = true;
// 查找节点 i 的第一个邻接节点 n 。
int n = getFirstNeighbor(i);
// 存在这样一个邻接节点。
while (-1 != n) {
if (!isVisited[n]) {
dfs(isVisited, n);
}
// 如果 n 节点已经被访问过。
n = getNextNeighbor(i, n);
}
}
/**
* 重载 dfs 方法。
* 目的:对所有节点进行 dfs 。
*/
public void dfs() {
isVisited = new boolean[vertexes.size()];
for (int i = 0; i < vertexes.size(); i++) {
if (!isVisited[i]) {
// 回溯。
dfs(isVisited, i);
}
}
}
五、广度优先遍历
5.1 遍历思路
图的广度优先搜索(Broad First Search)。
- 类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的节点的顺序,以便按这个顺序来访问这些节点的邻接节点。
5.2 算法步骤
- 访问初始节点,并标记该节点为已访问;
- 将已访问的初始节点加入队列(记录访问顺序);
- 当队列非空时,继续执行,否则对于该节点执行的算法结束;
- 出队列,取出队头节点;
- 查找队头节点的第一个邻接节点;
- 若队头节点的邻接节点不存在,则转到步骤 3;否则循环执行以下三个步骤:
- 若邻接节点尚未被访问,则访问该节点并标记为已访问;
- 该邻接节点入队列(记录访问顺序);
- 查找该邻接节点的下一个邻接节点,转到步骤 6。
- 示意图:
5.3 代码示例
/**
* 广度优先遍历。
*
* @param isVisited 标记是否被访问的数组
* @param i 下标
*/
private void bfs(boolean[] isVisited, int i) {
// 访问并标记为已访问。
System.out.print(vertexes.get(i) + "->");
isVisited[i] = true;
// 入队列,用于记录顶点访问的顺序。
LinkedList<Integer> queue = new LinkedList<>();
queue.addLast(i);
// 表示队列的头顶点对应下标。
int h;
// 邻接顶点。
int n;
while (!queue.isEmpty()) {
// 取出队列的头顶点下标。
h = queue.removeFirst();
// 得到第一个邻接顶点的下标。
n = getFirstNeighbor(h);
// 找到。
while (-1 != n) {
//是否访问过。
if (!isVisited[n]) {
System.out.print(vertexes.get(n) + "->");
//标记已经访问。
isVisited[n] = true;
//入队。
queue.addLast(n);
}
// 以头顶点为前驱点,找邻接顶点后面的下一个邻接顶点。
n = getNextNeighbor(h, n);
}
}
}
/**
* 重载 bfs 方法。
* 目的:遍历所有的顶点,并进行 bfs 。
*/
public void bfs() {
isVisited = new boolean[vertexes.size()];
for (int i = 0; i < vertexes.size(); i++) {
if (!isVisited[i]) {
// 回溯。
bfs(isVisited, i);
}
}
}
六、完整示例代码
需求:通过深度优先遍历和广度优先遍历两种方式,对由元素{“1”, “2”, “3”, “4”, “5”, “6”, “7”, “8”}组成的图,进行遍历。
- 示意图:
- 代码示例:
public class GraphDemo {
public static void main(String[] args) {
String[] vertexes = {"1", "2", "3", "4", "5", "6", "7", "8"};
// 8个顶点的图。
Graph graph = new Graph(8);
// 循环的添加顶点。
for (String vertex : vertexes) {
graph.insertVertex(vertex);
}
// 手动更新边的关系。
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.insertEdge(3, 7, 1);
graph.insertEdge(4, 7, 1);
graph.insertEdge(2, 5, 1);
graph.insertEdge(2, 6, 1);
graph.insertEdge(5, 6, 1);
// 显示添加结果。
graph.showGraph();
// [0, 1, 1, 0, 0, 0, 0, 0]
// [1, 0, 0, 1, 1, 0, 0, 0]
// [1, 0, 0, 0, 0, 1, 1, 0]
// [0, 1, 0, 0, 0, 0, 0, 1]
// [0, 1, 0, 0, 0, 0, 0, 1]
// [0, 0, 1, 0, 0, 0, 1, 0]
// [0, 0, 1, 0, 0, 1, 0, 0]
// [0, 0, 0, 1, 1, 0, 0, 0]
System.out.println("深度遍历:");
graph.dfs();
// 深度遍历:
// 1->2->4->8->5->3->6->7->
System.out.println();
System.out.println("广度优先:");
graph.bfs();
// 广度优先:
// 1->2->3->4->5->6->7->8->
}
}
class Graph {
/**
* 存储顶点集合。
*/
List<String> vertexes;
/**
* 存储图对应的邻结矩阵。
*/
int[][] edges;
/**
* 表示边的数目。
*/
int numOfEdges;
/**
* 记录某个顶点是否被访问。
*/
boolean[] isVisited;
/**
* 图的构造器。
*
* @param n 表示 n 个顶点。
*/
public Graph(int n) {
this.edges = new int[n][n];
this.vertexes = new ArrayList<>();
this.numOfEdges = 0;
}
/**
* 显示图对应的矩阵。
*/
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
/**
* 插入顶点。
*
* @param vertex 顶点
*/
public void insertVertex(String vertex) {
vertexes.add(vertex);
}
/**
* 添加边。
*
* @param v1 表示点的下标即是第几个顶点 举例:假设要表示"A"、"B",则元素"A"对应的下标是0;元素"B"对应的下标是1
* @param v2 第二个顶点对应的下标
* @param weight 表示权值 (这里使用1表示有直线连接;0表示无连接。)
*/
public void insertEdge(int v1, int v2, int weight) {
// 因为是无向图,则需要进行双向添加权值。
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
/**
* 得到第一个邻接顶点的下标。
*
* @param index 下标
* @return int 如果存在就返回对应的下标,否则返回-1
*/
public int getFirstNeighbor(int index) {
for (int j = 0; j < vertexes.size(); j++) {
if (edges[index][j] > 0) {
return j;
}
}
return -1;
}
/**
* 根据前一个邻接顶点的下标来获取下一个邻接顶点。
*
* @param v1 下标1
* @param v2 下标2
* @return int 如果存在就返回对应的下标,否则返回-1
*/
public int getNextNeighbor(int v1, int v2) {
for (int j = (v2 + 1); j < vertexes.size(); j++) {
if (edges[v1][j] > 0) {
return j;
}
}
return -1;
}
/**
* 深度优先遍历。
*
* @param isVisited 标记是否被访问的数组
* @param i 下标
*/
private void dfs(boolean[] isVisited, int i) {
// 访问并输出顶点。
System.out.print(vertexes.get(i) + "->");
// 设置为已访问。
isVisited[i] = true;
// 查找顶点 i 的第一个邻接顶点 n 。
int n = getFirstNeighbor(i);
// 存在这样一个邻接顶点。
while (-1 != n) {
if (!isVisited[n]) {
dfs(isVisited, n);
}
// 如果 n 顶点已经被访问过。
n = getNextNeighbor(i, n);
}
}
/**
* 重载 dfs 方法。
* 目的:遍历所有的顶点,并进行 dfs 。
*/
public void dfs() {
isVisited = new boolean[vertexes.size()];
for (int i = 0; i < vertexes.size(); i++) {
if (!isVisited[i]) {
// 回溯。
dfs(isVisited, i);
}
}
}
/**
* 广度优先遍历。
*
* @param isVisited 标记是否被访问的数组
* @param i 下标
*/
private void bfs(boolean[] isVisited, int i) {
// 访问并标记为已访问。
System.out.print(vertexes.get(i) + "->");
isVisited[i] = true;
// 入队列,用于记录顶点访问的顺序。
LinkedList<Integer> queue = new LinkedList<>();
queue.addLast(i);
// 表示队列的头顶点对应下标。
int h;
// 邻接顶点。
int n;
while (!queue.isEmpty()) {
// 取出队列的头顶点下标。
h = queue.removeFirst();
// 得到第一个邻接顶点的下标。
n = getFirstNeighbor(h);
// 找到。
while (-1 != n) {
//是否访问过。
if (!isVisited[n]) {
System.out.print(vertexes.get(n) + "->");
//标记已经访问。
isVisited[n] = true;
//入队。
queue.addLast(n);
}
// 以头顶点为前驱点,找邻接顶点后面的下一个邻接顶点。
n = getNextNeighbor(h, n);
}
}
}
/**
* 重载 bfs 方法。
* 目的:遍历所有的顶点,并进行 bfs 。
*/
public void bfs() {
isVisited = new boolean[vertexes.size()];
for (int i = 0; i < vertexes.size(); i++) {
if (!isVisited[i]) {
// 回溯。
bfs(isVisited, i);
}
}
}
}
七、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。