如何在 Java 中实现无向图
基本概念
图的定义
一个图是由点集 \(V=\{v_i\}\) 和 \(V\) 中元素的无序对的一个集合 \(E=\{e_k\}\) 所构成的二元组,记为 \(G=(V,E)\),\(V\) 中的元素 \(v_i\) 叫做顶点,\(E\) 中的元素 \(e_k\) 叫做边。
对于 \(V\) 中的两个点 \(u, v\),如果边 \((u, v)\) 属于 \(E\),则称 \(u,v\) 两点相邻,\(u,v\) 称为边 \((u, v)\) 的端点。
我们可以用 \(m(G)=|E|\) 表示图 \(G\) 中的边数,用 \(n(G)=|V|\) 表示图 \(G\) 中的顶点个数。
无向图的定义
对于 \(E\) 中的任意一条边 \((v_i, v_j)\),如果边 \((v_i, v_j)\) 端点无序,则它是无向边,此时图 \(G\) 称为无向图。无向图是最简单的图模型,下图显示了同一幅无向图,顶点使用圆圈表示,边则是顶点之间的连线,没有箭头(图片来自于《算法第四版》):
无向图的 API
对于一幅无向图,我们关心图的顶点数、边数、每个顶点的相邻顶点和边的添加操作,所以接口如下所示:
package com.zhiyiyo.graph;
/**
* 无向图
*/
public interface Graph {
/**
* 返回图中的顶点数
*/
int V();
/**
* 返回图中的边数
*/
int E();
/**
* 向图中添加一条边
* @param v 顶点 v
* @param w 顶点 w
*/
void addEdge(int v, int w);
/**
* 返回所有相邻顶点
* @param v 顶点 v
* @return 所有相邻顶点
*/
Iterable<Integer> adj(int v);
}
无向图的实现方式
邻接矩阵
用矩阵表示图对研究图的性质及应用常常是比较方便的,对于各种图有各种矩阵表示方式,比如权矩阵和邻接矩阵,这里我们只关注邻接矩阵。它的定义为:
对于图 \(G=(V,E)\),\(|V|=n\),构造一个矩阵 \(\boldsymbol A=(a_{ij})_{n\times n}\),其中:
a_{ij} = \left\{
\begin{aligned}
1& \quad (v_i,v_j)\in E\\
0& \quad 其他
\end{aligned}
\right.
\end{equation}
\]
则称矩阵 \(\boldsymbol{A}\) 为图 \(G\) 的邻接矩阵。
由定义可知,我们可以使用一个二维的布尔数组 A
来实现邻接矩阵,当 A[i][j] = true
时说明顶点 i
和 j
相邻。
对于 \(n\) 个顶点的图 \(G\),邻接矩阵需要消耗的空间为 \(n^2\) 个布尔值的大小,对于稀疏图来说会造成很大的浪费,当顶点数很大时所消耗的空间会是个天文数字。同时当图比较特殊,存在自环以及平行边时,邻接矩阵的表示方式是无能为力的。《算法》中给出了存在这两种情况的图:
边的数组
对于无向图,我们可以实现一个类 Edge
,里面只用两个实例变量用来存储两个顶点 \(u\) 和 \(v\),接着在一个数组里面保存所有 Edge
即可。这样做有一个很大的问题,就是在获取顶点 \(v\) 的所有相邻顶点时必须遍历整个数组才能得到,时间复杂度是 \(O(|E|)\),由于获取相邻顶点是很常用的操作,所以这种表示方式也不太行。
邻接表数组
如果我们把顶点表示为一个整数,取值范围为 \(0\sim |V|-1\),那么就可以用一个长度为 \(|V|\) 的数组的索引表示每一个顶点,然后将每一个数组元素设置为一个链表,上面挂载着索引所代表的的顶点相邻的其他顶点。图一所示的无向图可以用下图所示的邻接表数组表示出来:
使用邻接表实现无向图的代码如下所示,由于邻接表数组中的每个链表都会保存与顶点相邻的顶点,所以将边添加到图中时需要对数组中的两个链表进行添加节点的操作:
package com.zhiyiyo.graph;
import com.zhiyiyo.collection.stack.LinkStack;
/**
* 使用邻接表实现的无向图
*/
public class LinkGraph implements Graph {
private final int V;
private int E;
private LinkStack<Integer>[] adj;
public LinkGraph(int V) {
this.V = V;
adj = (LinkStack<Integer>[]) new LinkStack[V];
for (int i = 0; i < V; i++) {
adj[i] = new LinkStack<>();
}
}
@Override
public int V() {
return V;
}
@Override
public int E() {
return E;
}
@Override
public void addEdge(int v, int w) {
adj[v].push(w);
adj[w].push(v);
E++;
}
@Override
public Iterable<Integer> adj(int v) {
return adj[v];
}
}
这里用到的栈代码如下所示,栈的实现不是这篇博客的重点,所以这里不做过多解释:
package com.zhiyiyo.collection.stack;
import java.util.EmptyStackException;
import java.util.Iterator;
/**
* 使用链表实现的堆栈
*/
public class LinkStack<T> {
private int N;
private Node first;
public void push(T item) {
first = new Node(item, first);
N++;
}
public T pop() throws EmptyStackException {
if (N == 0) {
throw new EmptyStackException();
}
T item = first.item;
first = first.next;
N--;
return item;
}
public int size() {
return N;
}
public boolean isEmpty() {
return N == 0;
}
public Iterator<T> iterator() {
return new ReverseIterator();
}
private class Node {
T item;
Node next;
public Node() {
}
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
private class ReverseIterator implements Iterator<T> {
private Node node = first;
@Override
public boolean hasNext() {
return node != null;
}
@Override
public T next() {
T item = node.item;
node = node.next;
return item;
}
@Override
public void remove() {
}
}
}
无向图的遍历
给定下面一幅图,现在要求找到每个顶点到顶点 0 的路径,该如何实现?或者简单点,给定顶点 0 和 4,要求判断从顶点 0 开始走,能否到达顶点 4,该如何实现?这就要用到两种图的遍历方式:深度优先搜索和广度优先搜索。
在介绍这两种遍历方式之前,先给出解决上述问题需要实现的 API:
package com.zhiyiyo.graph;
public interface Search {
/**
* 起点 s 和 顶点 v 之间是否连通
* @param v 顶点 v
* @return 是否连通
*/
boolean connected(int v);
/**
* 返回与顶点 s 相连通的顶点个数(包括 s)
*/
int count();
/**
* 是否存在从起点 s 到顶点 v 的路径
* @param v 顶点 v
* @return 是否存在路径
*/
boolean hasPathTo(int v);
/**
* 从起点 s 到顶点 v 的路径,不存在则返回 null
* @param v 顶点 v
* @return 路径
*/
Iterable<Integer> pathTo(int v);
}
深度优先搜索
深度优先搜索的思想类似树的先序遍历。我们从顶点 0 开始,将它的相邻顶点 2、1、5 加到栈中。接着弹出栈顶的顶点 2,将它相邻的顶点 0、1、3、4 添加到栈中,但是写到这你就会发现一个问题:顶点 0 和 1明明已经在栈中了,如果还把他们加到栈中,那这个栈岂不是永远不会变回空。所以还需要维护一个数组 boolean[] marked
,当我们将一个顶点 i
添加到栈中时,就将 marked[i]
置为 true
,这样下次要想将顶点 i
加入栈中时,就得先检查一个 marked[i]
是否为 true
,如果为 true
就不用再添加了。重复栈顶节点的弹出和节点相邻节点的入栈操作,直到栈为空,我们就完成了顶点 0 可达的所有顶点的遍历。
为了记录每个顶点到顶点 0 的路径,我们还需要一个数组 int[] edgeTo
。每当我们访问到顶点 u
并将其一个相邻顶点 i
压入栈中时,就将 edgeTo[i]
设置为 u
,说明要想从顶点i
到达顶点 0,需要先回退顶点 u
,接着再从顶点 edgeTo[u]
处获取下一步要回退的顶点直至找到顶点 0。
package com.zhiyiyo.graph;
import com.zhiyiyo.collection.stack.LinkStack;
import com.zhiyiyo.collection.stack.Stack;
public class DepthFirstSearch implements Search {
private boolean[] marked;
private int[] edgeTo;
private Graph graph;
private int s;
private int N;
public DepthFirstSearch(Graph graph, int s) {
this.graph = graph;
this.s = s;
marked = new boolean[graph.V()];
edgeTo = new int[graph.V()];
dfs();
}
/**
* 递归实现的深度优先搜索
*
* @param v 顶点 v
*/
private void dfs(int v) {
marked[v] = true;
N++;
for (int i : graph.adj(v)) {
if (!marked[i]) {
edgeTo[i] = v;
dfs(i);
}
}
}
/**
* 堆栈实现的深度优先搜索
*/
private void dfs() {
Stack<Integer> vertexes = new LinkStack<>();
vertexes.push(s);
marked[s] = true;
while (!vertexes.isEmpty()) {
Integer v = vertexes.pop();
N++;
// 将所有相邻顶点加到堆栈中
for (Integer i : graph.adj(v)) {
if (!marked[i]) {
edgeTo[i] = v;
marked[i] = true;
vertexes.push(i);
}
}
}
}
@Override
public boolean connected(int v) {
return marked[v];
}
@Override
public int count() {
return N;
}
@Override
public boolean hasPathTo(int v) {
return connected(v);
}
@Override
public Iterable<Integer> pathTo(int v) {
if (!hasPathTo(v)) return null;
Stack<Integer> path = new LinkStack<>();
int vertex = v;
while (vertex != s) {
path.push(vertex);
vertex = edgeTo[vertex];
}
path.push(s);
return path;
}
}
广度优先搜索
广度优先搜索的思想类似树的层序遍历。与深度优先搜索不同,从顶点 0 出发,广度优先搜索会先处理完所有与顶点 0 相邻的顶点 2、1、5 后,才会接着处理顶点 2、1、5 的相邻顶点。这个搜索过程就是一圈一圈往外扩展、越走越远的过程,所以可以用来获取顶点 0 到其他节点的最短路径。只要将深度优先搜索中的堆换成队列,就能实现广度优先搜索:
package com.zhiyiyo.graph;
import com.zhiyiyo.collection.queue.LinkQueue;
public class BreadthFirstSearch implements Search {
private boolean[] marked;
private int[] edgeTo;
private Graph graph;
private int s;
private int N;
public BreadthFirstSearch(Graph graph, int s) {
this.graph = graph;
this.s = s;
marked = new boolean[graph.V()];
edgeTo = new int[graph.V()];
bfs();
}
private void bfs() {
LinkQueue<Integer> queue = new LinkQueue<>();
marked[s] = true;
queue.enqueue(s);
while (!queue.isEmpty()) {
int v = queue.dequeue();
N++;
for (Integer i : graph.adj(v)) {
if (!marked[i]) {
edgeTo[i] = v;
marked[i] = true;
queue.enqueue(i);
}
}
}
}
}
队列的实现代码如下:
package com.zhiyiyo.collection.queue;
import java.util.EmptyStackException;
public class LinkQueue<T> {
private int N;
private Node first;
private Node last;
public void enqueue(T item) {
Node node = new Node(item, null);
if (++N == 1) {
first = node;
} else {
last.next = node;
}
last = node;
}
public T dequeue() throws EmptyStackException {
if (N == 0) {
throw new EmptyStackException();
}
T item = first.item;
first = first.next;
if (--N == 0) {
last = null;
}
return item;
}
public int size() {
return N;
}
public boolean isEmpty() {
return N == 0;
}
private class Node {
T item;
Node next;
public Node() {
}
public Node(T item, Node next) {
this.item = item;
this.next = next;
}
}
}
后记
这样就简要介绍完了无向图的实现及遍历方式,对于无向图的更多操作,比如寻找环和判断是否为二分图可以参见《算法第四版》,以上~~
如何在 Java 中实现无向图的更多相关文章
- 如何在 Java 中实现无向环和有向环的检测
无向环 一个含有环的无向图如下所示,其中有两个环,分别是 0-2-1-0 和 2-3-4-2: 要检测无向图中的环,可以使用深度优先搜索.假设从顶点 0 出发,再走到相邻的顶点 2,接着走到顶点 2 ...
- 如何在 Java 中实现最小生成树算法
定义 在一幅无向图 \(G=(V,E)\) 中,\((u, v)\) 为连接顶点 \(u\) 和顶点 \(v\) 的边,\(w(u,v)\) 为边的权重,若存在边的子集 \(T\subseteq E\ ...
- 如何在 Java 中实现 Dijkstra 最短路算法
定义 最短路问题的定义为:设 \(G=(V,E)\) 为连通图,图中各边 \((v_i,v_j)\) 有权 \(l_{ij}\) (\(l_{ij}=\infty\) 表示 \(v_i,v_j\) 间 ...
- 如何在JAVA中实现一个固定最大size的hashMap
如何在JAVA中实现一个固定最大size的hashMap 利用LinkedHashMap的removeEldestEntry方法,重载此方法使得这个map可以增长到最大size,之后每插入一条新的记录 ...
- 如何在java中使用sikuli进行自动化测试
很早之前写过一篇介绍sikuli的文章.本文简单介绍如何在java中使用sikuli进自动化测试. 图形脚本语言sikuli sikuli IDE可以完成常见的单击.右击.移动到.拖动等鼠标操作,ja ...
- 如何在Java中调用Python代码
有时候,我们会碰到这样的问题:与A同学合作写代码,A同学只会写Python,而不会Java, 而你只会写Java并不擅长Python,并且发现难以用Java来重写对方的代码,这时,就不得不想方设法“调 ...
- 如何在java中跳出当前多重嵌套循环?有几种方法?
如何在java中跳出当前多重嵌套循环?有几种方法? - 两种方法 - 1.在外层循环定义标记 ok: for(int i=0;i<100;i++){ ...
- 用代码说话:如何在Java中实现线程
并发编程是Java语言的重要特性之一,"如何在Java中实现线程"是学习并发编程的入门知识,也是Java工程师面试必备的基础知识.本文从线程说起,然后用代码说明如何在Java中实现 ...
- 如何在Java中测试类是否是线程安全的
通过优锐课的java核心笔记中,我们可以看到关于如何在java中测试类是否线程安全的一些知识点汇总,分享给大家学习参考. 线程安全性测试与典型的单线程测试不同.为了测试一个方法是否是线程安全的,我们需 ...
随机推荐
- BBS 项目(四)
目录 BBS 项目(四) 首页布局 个人头像显示 个人站点路由设计 个人站点页面设计 base.html site.html 左侧过滤功能 404.html BBS 项目(四) 首页布局 <!D ...
- CF549G题解
变菜了,一年前做这种题10min出结论,现在对着样例胡半天都没结果 首先考虑从判断无解入手. 定义两个位置 \((i,j)\),若 \(a[i]=a[j]+(j-i)\),则 \(i\) 和 \(j\ ...
- .Net6 设置环境变量
开发过程中经常要发布一个版本到测试服上给测试人员测试,可能需要直接输出错误信息,需要设置环境变量为Development,如果是IIS可以通过设置IIS环境变量,见下面这篇文章 https://blo ...
- vscode中使用git
vscode中使用git 使用vscode打开git的文件时,会自动的跟踪文件的改动, 如果要提交修改,首先点击+,这个相当于git add, 这样会暂时保存,然后点击上面的√,然后在输入栏中输入修改 ...
- YCCMS 3.3 CSRF漏洞--代码执行
一. 启动环境 1.双击运行桌面phpstudy.exe软件 2.点击启动按钮,启动服务器环境 二.代码审计 1.双击启动桌面Seay源代码审计系统软件 2.点击新建项目按钮,弹出对画框中选择(C:\ ...
- Ubuntu- 彻底干净卸载MySQL、Apache2、Php的方法
一.卸载删除 mysql 1 sudo apt-get autoremove --purge mysql-server-5.0 2 sudo apt-get remove mysql-server 3 ...
- Python_Learn,Python背景的介绍
一.计算机程序的运行方式 机器语言编写的程序可以在计算机上直接运行,而汇编语言和高级余语言写的程序(通常称为源程序)则需要"翻译"成机器语言才能运行.源程序"翻译&quo ...
- AS3 setChildIndex的bug
AS3 setChildIndex的bug 小伙伴们注意了,在timelines使用 MovieClip 的 setChildIndex方法要注意,因为它有一个bug,调整显示对象的深度之后,会导致显 ...
- Sting -- byte[]互转
1.String -->byte[] String str = "中国"; byte[] bys = str.getBytes(); Arrays.toString(bys) ...
- kafka 保证消息被消费和消息只消费一次
1. 保证消息被消费 即使消息发送到了消息队列,消息也不会万无一失,还是会面临丢失的风险. 我们以 Kafka 为例,消息在Kafka 中是存储在本地磁盘上的, 为了减少消息存储对磁盘的随机 I/O, ...