图(Graph)也是一种数据结构,在计算机科学中除了线性表和树结构,还有一种图结构!这种结构节点可以具有零个或多个相邻的元素,非常适合表示多对多的关系!
我们已经学过了线性表和树结构,它们都有各自的应用场景。
然而线性表局限于一个直接前驱和一个直接后继的关系,也就是说集合中必存在唯一的一个"第一个元素",必存在唯一的一个"最后元素",除了最后一个元素之外,均有唯一的后继。除了第一个元素之外,均有唯一的前驱。
而树(Tree)的局限就是只能有一个直接的前驱(父节点),因此我们想要在计算机中表示多对多的关系时,就不得不使用图(Graph)这种数据结构来表示!
图(Graph)是一种非线性的数据结构,是一些"顶点"的集合,这些顶点通过一系列"边"结对连接。其中节点可以具有零个或多个相邻的元素。两个节点之间的连接称为边。节点也可以称为顶点。如图
所有的顶点构成一个顶点集合,所有的边构成边集合,一个完整的图结构就是由顶点集合和边集合组成。另外图结构在交通运输网、地铁网络、社交网络等都可以抽象成图结构,从而进行存储与计算!
1、顶点(Vertex):顶点也称节点!顶点可以存储图结构的数据元素
2、边(Edge):连接顶点的线,也可保存权值!例如某两点之间的距离
3、路径:比如3->4
的路径有①3->1->4
②3->2->1->4
4、无向图(Undirected Graph):顶点之间的连接没有方向(如上图),比如1-2
,既可以1->2
也可以2->1
。
5、有向图(Directed Graph):顶点之间的连接或边具有方向性!描述两个顶的顺序就有要求,比如2-1
,只能是2->1
不能是1->2
,如下图所示
6、带权图:边带有权值的图,如下图
图在计算机中表示方式有两种:
1、邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是通过row和col来表示1…n。
例如上图row={0, 1, 2, 3, 4, 5},col={0, 1, 2, 3, 4, 5}。row和col共同组成一个二维数组!arry[row][col] = 0
表示没有直连关系,arry[row][col] = 1
表示有直连关系,例如array[0][1] = 1
就表示0和1顶点是相连的。而如果值等于0说明不相连(不互通)。
2、邻接表
1、邻接矩阵(二维数组)需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在,会造成空间的一定损失
2、邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
3、举例说明
前面已经介绍了图结构的基本概念与表示方式,现在我们来快速使用代码创建一个图结构,体验图结构的表示方式。
要求代码实现(上图)图结构。备注:通过二维数组表示时规定 1 有直连关系,0 没有直连关系。
思路分析:
Java代码实现:
package com.laizhenghua.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @description: 图结构的创建
* @date: 2021/11/20 13:34
*/
public class Graph {
private List<String> vertexList; // 存储顶点
private int[][] edges; // 存储邻接矩阵
private int numberOfEdges; // 边的个数
private boolean[] isVisited; // 记录某个顶点是否被访问过(后面遍历需要用到)
// 构造器
public Graph(int n) {
this.vertexList = new ArrayList<>(n);
this.edges = new int[n][n];
this.numberOfEdges = 0;
this.isVisited = new boolean[n];
}
// 插入节点
public void insertVertex(String vertex) {
this.vertexList.add(vertex);
}
/**
* 添加边
* @param v1 横向坐标(row的下标)
* @param v2 纵向坐标(col的下标)
* @param weight 权值
*/
public void insertEdge(int v1, int v2, int weight) {
this.edges[v1][v2] = weight;
this.edges[v2][v1] = weight;
this.numberOfEdges++;
}
// 获取顶点个数
public int getNumberOfVertex() {
return this.vertexList.size();
}
// 获取边的个数
public int getNumberOfEdges() {
return this.numberOfEdges;
}
// 通过索引从集合里面获取顶点
public String getVertex(int index) {
return this.vertexList.get(index);
}
// 获取v1和v2的权值
public int getWeight(int v1, int v2) {
return this.edges[v1][v2];
}
// 显示图对应的矩阵
public void showGraph() {
for (int[] array : this.edges) {
System.out.println(Arrays.toString(array));
}
}
}
main
方法中测试
/**
* @description:
* @date: 2021/11/20 13:33
*/
public class GraphTest {
public static void main(String[] args) {
int n = 5; // 顶点个数
String[] vertexArray = new String[]{"A", "B", "C", "D", "E"};
// 创建图对象
Graph graph = new Graph(n);
// 循环添加顶点
for (String vertex : vertexArray) {
graph.insertVertex(vertex);
}
// 添加边。A-B A-C B-C B-D B-E
graph.insertEdge(0, 1, 1); // A-B
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
// 输出矩阵
graph.showGraph();
}
}
# !usr/bin/env python
# -*- coding:utf-8 -*-
# __author__ = "laizhenghua"
import numpy as np
class Graph:
def __init__(self, n):
self.vertex_list = []
self.edges = np.zeros((n, n), dtype=int) # 5 * 5 的矩阵
self.number_of_edges = 0
self.is_visited = [] # 存放布尔值记录某个顶点是否被访问过(后面遍历需要用到)
# 添加顶点
def inset_vertex(self, vertex):
self.vertex_list.append(vertex)
# 添加边
def insert_edge(self, v1, v2, weight):
self.edges[v1][v2] = weight
self.edges[v2][v1] = weight
self.number_of_edges += 1
# 获取顶点个数
def get_number_of_vertex(self):
return len(self.vertex_list)
# 获取边个数
def get_number_of_edges(self):
return self.number_of_edges
# 通过索引获取列表里面的顶点
def get_vertex_by_index(self, index):
return self.vertex_list[index]
# 获取v1和v2对应的权值
def get_weight(self, v1, v2):
return self.edges[v1][v2]
# 显示图对应的矩阵
def show_graph(self):
for i in range(len(self.edges)):
for j in range(len(self.edges[i])):
print(self.edges[i][j], end=" ")
print()
if __name__ == "__main__":
n = 5 # 顶点个数
graph = Graph(n) # 创建图对象
vertex_list = ["A", "B", "C", "D", "E"]
# 循环添加顶点
for vertex in vertex_list:
graph.inset_vertex(vertex)
# 添加边
graph.insert_edge(0, 1, 1)
graph.insert_edge(0, 2, 1)
graph.insert_edge(1, 2, 1)
graph.insert_edge(1, 3, 1)
graph.insert_edge(1, 4, 1)
# 输出邻接矩阵
graph.show_graph()
所谓图的遍历,即是对图顶点的访问!一个图有那么多个顶点,如何遍历这些顶点,需要特定策略(如二叉树的遍历有前序、中序、后序)。
而对于图的遍历一般由两种访问策略:
深度优先,从字面上理解就是图的深度优先查找!其遍历的基本思想如下:
根据遍历的基本思想,我们得出深度优先遍历的算法步骤(假设初始节点为v):
案例(我们还是以刚才创建的图结构进行深度优先遍历):
深度优先算法Java代码实现(Graph类新增方法):
// 获取第一个邻接节点的下标
public int getFirstNeighbor(int index) {
for (int i = 0; i < this.vertexList.size(); i++) {
if (this.edges[index][i] > 0) {
return i;
}
}
return -1; // 如果不存在则返回-1
}
// 根据前一个邻接节点的下标来获取下一个邻接节点
public int getNextNeighbor(int v1, int v2) {
for (int i = v2 + 1; i < this.vertexList.size(); i++) {
if (this.edges[v1][i] > 0) {
return i;
}
}
return -1;
}
/**
* 深度优先遍历算法
* @param isVisited 记录节点被访问过的boolean数组
* @param index 第一次就是0
*/
private void dfs(boolean[] isVisited, int index) {
// 首先我们访问该节点(输出)
System.out.print(getVertexByIndex(index) + "->");
// 将节点标记为已经访问过
isVisited[index] = true;
// 查找节点下标为index的第一个邻接节点w
int w = getFirstNeighbor(index);
while (w != -1) {
if (!isVisited[w]) {
dfs(isVisited, w);
}
// 如果w节点已经被访问过了
w = getNextNeighbor(index, w);
}
}
// 对dfs进行一个重载,遍历所有的节点,并进行dfs
public void dfs() {
// 遍历所有节点,进行dfs[类似回溯]
for (int i = 0; i < getNumberOfVertex(); i++) {
if (!isVisited[i]) {
dfs(isVisited, i);
}
}
isVisited = new boolean[getNumberOfVertex()];
}
在main
方法中测试
// 图的深度优先遍历
graph.dfs(); // A->B->C->D->E->
深度优先算法Python代码实现(Graph类新增方法):
# 获取第一个邻接节点的下标
def get_first_neighbor(self, index):
for i in range(len(self.vertex_list)):
if self.edges[index][i] > 0:
return i
return -1
# 根据前一个邻接节点的下标来获取下一个邻接节点
def get_next_neighbor(self, v1, v2):
for i in range(v2 + 1, len(self.vertex_list), 1):
if self.edges[v1][i] > 0:
return i
return -1
# 深度优先遍历算法
def dfs(self, *args):
if len(args) == 0:
for i in range(self.get_number_of_vertex()):
if self.is_visited[i] is not True:
self.dfs(self.is_visited, i)
self.is_visited = [False] * len(self.vertex_list)
if len(args) == 2:
# 首先我们访问该节点(输出)
print(self.get_vertex_by_index(args[1]) + "->", end="")
# 将节点标记为已经访问过
args[0][args[1]] = True
w = self.get_first_neighbor(args[1])
while w != -1:
if self.is_visited[w] is not True:
self.dfs(args[0], w)
w = self.get_next_neighbor(args[1], w)
图的广度优先搜索/遍历(Broad First Search):类似于一个分层搜索的,广度优先遍历需要使用一个队列以保持访问过的节点顺序,以便按这个顺序来访问这些节点的邻接节点。
其算法步骤如下:
深度优先算法Java代码实现(Graph类新增方法):
// 对一个节点进行广度优先遍历
private void bfs(boolean[] isVisited, int index) {
int u; // 队列头节点对应的下标
int w; // 邻接节点w
LinkedList<Integer> queue = new LinkedList<>(); // 队列,记录节点访问顺序
// 访问节点/输出节点信息
System.out.print(getVertexByIndex(index) + "->");
// 将节点标记为已访问
isVisited[index] = true;
// 将节点加入队列
queue.addLast(index);
while (!queue.isEmpty()) {
u = queue.removeFirst(); // 取出队列头结点下标
w = getFirstNeighbor(u); // 获取第一个邻节点的下标
if (w != -1) {
if (!isVisited[w]) {
System.out.print(getVertexByIndex(w) + "->");
isVisited[w] = true; // 将节点标记为已访问
queue.addLast(w); // 入队
}
// 以u为前驱点,找w后面的下一个邻节点
w = getNextNeighbor(u, w); // 体现出广度优先
}
}
}
// 对所有节点进行广度优先遍历
public void bfs() {
for (int i = 0; i < getNumberOfVertex(); i++) {
if (!isVisited[i]) {
bfs(isVisited, i);
}
}
isVisited = new boolean[getNumberOfVertex()];
}
深度优先算法Python代码实现(Graph类新增方法):
# 广度优先遍历算法
def bfs(self, *args):
if len(args) == 0:
for i in range(len(self.vertex_list)):
if self.is_visited[i] is not True:
self.bfs(self.is_visited, i)
self.is_visited = [False] * len(self.vertex_list)
if len(args) == 2:
# 对一个节点进行广度优先遍历
u = 0 # 队列头结点对应的下标
w = 0 # 邻接节点对应的下标
bfs_queue = Queue() # 记录节点的访问顺序
print(self.get_vertex_by_index(args[1]) + "->", end="")
args[0][args[1]] = True
bfs_queue.put(args[1]) # 入队
while bfs_queue.empty() is not True:
u = bfs_queue.get()
w = self.get_first_neighbor(u)
if w != -1:
if args[0][w] is not True:
print(self.get_vertex_by_index(w) + "->", end="")
args[0][w] = True
bfs_queue.put(w) # 入队
w = self.get_next_neighbor(u, w)
测试
if __name__ == "__main__":
n = 5 # 顶点个数
graph = Graph(n) # 创建图对象
vertex_list = ["A", "B", "C", "D", "E"]
# 循环添加顶点
for vertex in vertex_list:
graph.inset_vertex(vertex)
# 添加边
graph.insert_edge(0, 1, 1)
graph.insert_edge(0, 2, 1)
graph.insert_edge(1, 2, 1)
graph.insert_edge(1, 3, 1)
graph.insert_edge(1, 4, 1)
graph.show_graph()
# 图的深度优先遍历
graph.dfs()
print()
# 图的广度优先遍历
graph.bfs()
输出结果
我们发现深度优先和广度优先输出的顺序是一样的,并没有看出他们之间的区别。所以我们再来看个例子~
1、深度优先遍历的顺序是:1->2->4->8->5->3->6->7
2、广度优先遍历的顺序是:1->2->3->4->5->6->7->8
代码验证:
if __name__ == "__main__":
n = 8 # 顶点个数
graph = Graph(n) # 创建图对象
vertex_list = ["1", "2", "3", "4", "5", "6", "7", "8"]
for vertex in vertex_list:
graph.inset_vertex(vertex)
# 添加边
graph.insert_edge(0, 1, 1) # 1 - 2
graph.insert_edge(0, 2, 1) # 1 - 3
graph.insert_edge(1, 3, 1) # 2 - 4
graph.insert_edge(1, 4, 1) # 2 - 5
graph.insert_edge(2, 5, 1) # 3 - 6
graph.insert_edge(2, 6, 1) # 3 - 7
graph.insert_edge(3, 7, 1) # 4 - 8
graph.insert_edge(4, 7, 1) # 5 - 8
graph.insert_edge(5, 6, 1) # 6 - 7
graph.show_graph()
# 深度优先遍历
graph.dfs()
print()
# 广度优先遍历
graph.bfs()