概述
图是由有穷非空集合的顶点和顶点之间的边组成的集合,通常表示为 G(V,E)。G 表示一个图,V 是 图 G 顶点的集合,E 是图 G 边的集合。在图形结构中,数据之间具有任意联系,任意两个数据之间都可能相关,可用于表示多对多的数据结构
如果从顶点 V1 到 V2 的边没有方向,则称这条边为无向边,顶点和无向边组成图称为无向图。如果从顶点 V1 到 V2 的边有方向,则称这条边为有向边,由顶点和有向边组成的图称为有向图
图的存储结构:邻接矩阵
图的邻接矩阵的存储方式是基于两个数组来表示图的数据结构并存储图中的数据。一个一维数组存储图中的顶点信息,一个二维数组(也叫作邻接矩阵)存储图中的边信息,这里用 /<v1, v2/> 表示顶点 v1 和 v2 的边信息
在无向图的邻接矩阵中,如果 /<v1, v2/> 值为 1,则表示对应的顶点 v1 和 v2 连通,为 0 则不连通。在无向图的邻接矩阵中,主对角元素都为 0,即顶点自身没有连通关系
在有向图的邻接矩阵中,如果 /<v1, v2/> 值为 1,则表示存在从 v1 到 v2 的有向边,为 0 则表示不存在。同样,在有向图的邻接矩阵中,主对角元素都为 0,也就是说从顶点到自身没有连通关系。要注的是,有向图的边是有方向的,v1 的 出度为 2,表示从 v1 顶点出发的边有两条,v2 的出度为 0,表示没有从 v2 出发的边
某些图的每条边都带有权重,如果要将这些权值保存下来。则可以采用权值代替邻接矩阵中的 0、1。有权值代表存在边,无权值则用其他特殊符号表示
图的存储结构:邻接表
邻接表采用了数组与链表相结合的存储方式,是图的一种链式存储结构,主要用于解决邻接矩阵中顶点多边少时存在空间浪费的问题,具体的处理方法如下:
- 将图中的顶点信息存储在一个一维数组中,同时在顶点信息中存储用于指向第 1 个邻节点的指针,以便查找该顶点的边信息
- 图中每个顶点 V 的所有邻节点构成一个线性表,由于邻节点的个数不定,所以用单向链表存储。在单向链表中,每一个邻节点都保存有自身在一维数组中的下标,以及指向下一个邻节点的指针
对于带权值的图,在节点定义中再增加一个权重值 weight 的数据域,存储权值信息即可
图的遍历
图的遍历指从图中某一顶点出发遍访图中的每个顶点,且使每个原点仅被访问一次。图的遍历分为广度优先遍历和深度优先遍历,且对无向图和有向图都适用
广度优先遍历:假设从图中某个顶点 V 出发,在访问 V 之后依次访问 V 的各个未曾访问的邻节点,然后分别从这些邻节点出发依次访问它们的邻节点;如果此时图中尚有节点未被访问,则另选一个未曾访问的节点作为起点重复上述过程,直至图中所有节点均被访问
深度优先遍历:假设从图中某个顶点 V 出发,在访问 V 之后再访问它的第一个邻节点,再以这个邻节点为起点,重复上述步骤;如果此时图中尚有节点未被访问,则另选一个未曾访问的节点作为起点重复上述过程,直至图中所有节点都被访问
代码实现如下:
import java.util.*;
public class GraphLoopTest {
// 使用邻接表来存储图数据
private Map<String, List<String>> graph = new HashMap<String, List<String>>();
public void initGraphData() {
// 初始化图数据,图结构如下
// 1
// / \
// 2 3
// / \ / \
// 4 5 6 7
// \ | / \ /
// 8 9
graph.put("1", Arrays.asList("2", "3"));
graph.put("2", Arrays.asList("1", "4", "5"));
graph.put("3", Arrays.asList("1", "6", "7"));
graph.put("4", Arrays.asList("2", "8"));
graph.put("5", Arrays.asList("2", "8"));
graph.put("6", Arrays.asList("3", "8", "9"));
graph.put("7", Arrays.asList("3", "9"));
graph.put("8", Arrays.asList("4", "5", "6"));
graph.put("9", Arrays.asList("6", "7"));
}
private Queue<String> queue = new LinkedList<String>();
private Map<String, Boolean> status = new HashMap<String, Boolean>();
/**
* 广度优先遍历
*/
public void BFSSearch(String startPoint) {
queue.add(startPoint);
status.put(startPoint, false);
bfsLoop();
}
private void bfsLoop() {
// 从 queue 取出队列头的点,更新状态为已经遍历。
String currentQueueHeader = queue.poll();
status.put(currentQueueHeader, true);
System.out.println(currentQueueHeader);
// 找出与此点邻接且尚未遍历的点,进行标记,然后全部放入queue中
List<String> neighborPoints = graph.get(currentQueueHeader);
for (String poinit : neighborPoints) {
if (!status.getOrDefault(poinit, false)) {
if (queue.contains(poinit)) continue;
queue.add(poinit);
status.put(poinit, false);
}
}
// 如果队列不为空继续遍历
if (!queue.isEmpty()) {
bfsLoop();
}
}
private Stack<String> stack = new Stack<String>();
/**
* 深度优先遍历
*/
public void DFSSearch(String startPoint) {
stack.push(startPoint);
status.put(startPoint, true);
dfsLoop();
}
private void dfsLoop() {
if(stack.empty()){
return;
}
// 查看栈顶元素,但并不出栈
String stackTopPoint = stack.peek();
// 找出与此点邻接的且尚未遍历的点,进行标记
List<String> neighborPoints = graph.get(stackTopPoint);
for (String point : neighborPoints) {
if (!status.getOrDefault(point, false)) {
stack.push(point);
status.put(point, true);
dfsLoop();
}
}
String popPoint = stack.pop();
System.out.println(popPoint);
}
public static void main(String[] args) {
GraphLoopTest test = new GraphLoopTest();
test.initGraphData();
test.BFSSearch("1");
test.DFSSearch("1");
}
}