目录
- 1 无向图介绍
- 1.1 特征
- 1.2 相关术语
- 1.3 图的存储结构
- 1.3.1 邻接矩阵
- 1.3.2 邻接表(使用)
- 2 无向图实现
- 3 深度优先搜索
- 4 广度优先搜索
- 5 案例:畅通工程
- 5.1 题目要求
- 5.2 解题思路(基于深度/广度优先搜索)
- 6 路径查找
- 6.1 问题分析
- 6.2 代码实现
1 无向图介绍
1.1 特征
无向图的边仅仅连接两个顶点,没有其他含义,区别于有向图(边不仅连接两个顶点,且具有方向)。
1.2 相关术语
- 相邻顶点:通过一条边相连的两个顶点,并且称这条边依附于这两个顶点;
- 度:某顶点的度指依附于该顶点的边的个数
- 子图:一幅图的所有边的子集(包含这些边依附的顶点)组成的图;
- 路径:由边顺序连接的一系列的顶点组成;
- 环:含有至少一条边且终点和起点相同的路径。
- 连通图:如果图中任意一个顶点都存在一条路径到达另外一个顶点,那么这幅图就称为连通图;
- 连通子图:一个非连通图由若干连通的部分组成,每一个连通的部分称为该图的连通子图。
1.3 图的存储结构
要表示图,只需表示清楚以下两部分内容:
- 图中所有的顶点;
- 所有连接顶点的边;
常见图的存储结构有两种:邻接矩阵和邻接表
1.3.1 邻接矩阵
- 使用一个V*V的二维数组int[V][V] adj,把索引看作顶点;
- 如果顶点v和顶点w相连,将adj[V][W]和adj[W][V]的值设置为1,否则设置为0;
- 显然,邻接矩阵的空间复杂度为O(N^2),不适合处理规模较大的问题;
1.3.2 邻接表(使用)
- 使用一个大小为V的数组Deque[V] adj,索引即顶点;
- 每个索引的adj[V]是队列,队列中存储的是所有与该顶点相邻的其他顶点;
2 无向图实现
public class Graph {
//顶点数目
private final int V;
//边的数目
private int E;
//邻接表
private Deque<Integer>[] adj;
public Graph(int V){
//初始化顶点数量
this.V = V;
//初始化边的数量
this.E = 0;
//初始化邻接表
this.adj = new Deque[V];
for (int i = 0; i < adj.length; i++) {
adj[i] = new LinkedList<>();
}
}
//获取顶点数目
public int V(){
return V;
}
//获取边的数目
public int E(){
return E;
}
//向图中添加一条边 v-w
public void addEdge(int v, int w) {
//在无向图中,边是没有方向的,所以该边既可以说是从v到w的边,又可以说是从w到v的边,因此,需要让w出现在v的邻接表中,并且还要让v出现在w的邻接表中
adj[v].offer(w);
adj[w].offer(v);
//边的数量+1
E++;
}
//获取和顶点v相邻的所有顶点
public Deque<Integer> adj(int v){
return adj[v];
}
}
3 深度优先搜索
- 深度优先搜索,是指在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找子结点,再找兄弟结点;
- 由于无向图的边是没有方向的,如果4和5顶点相连,4会出现在5的邻接表中,5也会出现在4的邻接表中,为了不对顶点重复搜索,用marked来标记当前顶点有没有被搜索过;
- 定义一个数组boolean[V] marked,索引代表顶点,值为true表示已经被搜索过,值为false表示还没有被搜索;
public class DepthFirstSearch {
//索引代表顶点,值表示当前顶点是否已经被搜索
private boolean[] marked;
//记录有多少个顶点与s顶点相通
private int count;
//构造深度优先搜索对象,使用深度优先搜索找出g图中s顶点的所有相通顶点
public DepthFirstSearch(Graph g,int s){
//初始化marked数组
this.marked = new boolean[g.V()];
//初始化跟顶点s相通的顶点数量
this.count=0;
dfs(g,s);
}
//使用深度优先搜索找出g图中v顶点的所有相通顶点
private void dfs(Graph g, int v){
//把v顶点标识为已搜索
marked[v] = true;
for (Integer w : g.adj(v)) {
//判断当前w顶点有没有被搜索过,如果没有被搜索过,则递归调用dfs方法进行深度搜索
if (!marked[w]){
//相通顶点数量+1
count++;
dfs(g,w);
}
}
}
//判断w顶点是否被搜索过
public boolean marked(int w){
return marked[w];
}
//获取与顶点s相通的所有顶点的总数
public int count(){
return count;
}
}
public class Test {
public static void main(String[] args) {
Graph g = new Graph(7);
g.addEdge(0,1);
g.addEdge(0,2);
g.addEdge(0,6);
g.addEdge(4,6);
g.addEdge(4,3);
g.addEdge(3,5);
DepthFirstSearch ds = new DepthFirstSearch(g,0);
System.out.println(ds.count());
System.out.println(g.adj(0));
}
}
6
[1, 2, 6]
4 广度优先搜索
广度优先搜索,是指在搜索时,如果遇到一个结点既有子结点又有兄弟结点,那么先找兄弟结点,再找子结点。
public class BreadthFirstSearch {
//索引代表顶点,值表示当前顶点是否已经被搜索
private boolean[] marked;
//记录有多少个顶点与s顶点相通
private int count;
//用来存储待搜索邻接表的点
private Deque<Integer> waitSearch;
//构造广度优先搜索对象,使用广度优先搜索找出g图中s顶点的所有相邻顶点
public BreadthFirstSearch(Graph g, int s) {
this.marked = new boolean[g.V()];
this.count=0;
this.waitSearch = new LinkedList<>();
bfs(g,s);
}
//使用广度优先搜索找出g图中v顶点的所有相通顶点
private void bfs(Graph g, int v) {
//把当前顶点v标识为已搜索
marked[v] = true;
//让顶点v进入队列,待搜索
waitSearch.offer(v);
//通过循环,如果队列不为空,则从队列中弹出一个待搜索的顶点进行搜索
while(!waitSearch.isEmpty()){
//弹出一个待搜索的顶点
Integer wait = waitSearch.poll();
//遍历wait顶点的邻接表
for (Integer w : g.adj(wait)) {
if (!marked[w]){
//把当前顶点w标识为已搜索
marked[w] = true;
//让相通的顶点+1
count++;
//把当前顶点w加入等待队列
waitSearch.offer(w);
}
}
}
}
//判断w顶点是否已经被搜索
public boolean marked(int w) {
return marked[w];
}
//获取与顶点s相通的所有顶点的总数
public int count() {
return count;
}
}
public class Test {
public static void main(String[] args) {
Graph g = new Graph(7);
g.addEdge(0,1);
g.addEdge(0,2);
g.addEdge(0,6);
g.addEdge(4,6);
g.addEdge(4,3);
g.addEdge(3,5);
BreadthFirstSearch ds = new BreadthFirstSearch(g,0);
System.out.println(ds.count());
System.out.println(g.adj(0));
}
}
6
[1, 2, 6]
5 案例:畅通工程
5.1 题目要求
- 某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接相连的城镇;
- 畅通工程的目标是使全省任何两个城镇间都可以实现交通(不一定直接相连,间接相连即可);
- 有文件traffic_project.txt,下面是文件内容及数据解释:
- 总共有20个城市,目前已经修改好了7条道路,问9号城市和10号城市是否相通?9号城市和8号城市是否相通?
5.2 解题思路(基于深度/广度优先搜索)
- 创建一个Graph对象,表示城市;
- 分别调用addEdge(0,1),addEdge(6,9),addEdge(3,8),addEdge(5,11),addEdge(2,12),addEdge(6,10),addEdge(4,8),表示修建道路将对应的城市连起来;
- 通过Graph对象和顶点9,构建DepthFirstSearch对象或BreadthFirstSearch对象;
- 调用搜索对象的marked(10)方法和marked(8)方法,即可得知9号城市和10号城市是否相通?9号城市和8号城市是否相通?
public class Traffic_Project {
public static void main(String[] args) throws IOException {
// 创建输入流
BufferedReader reader = new BufferedReader(new FileReader("src/traffic_project.txt"));
// 读取城市数目,初始化并查集
int number = Integer.parseInt(reader.readLine());
Graph g = new Graph(number);
// 读取已经修建好的道路数目
int roadNumber = Integer.parseInt(reader.readLine());
// 循环读取已经修建好的道路,并调用addEdge方法
for (int i=0; i<roadNumber; i++) {
String line = reader.readLine();
int p = Integer.parseInt(line.split(" ")[0]);
int q = Integer.parseInt(line.split(" ")[1]);
g.addEdge(p, q);
}
// 根据图g和顶点9构建图的搜索对象
// BreadthFirstSearch search = new BreadthFirstSearch(g,9);
DepthFirstSearch search = new DepthFirstSearch(g, 9);
// 调用搜索对象的marked(10)方法和marked(8)方法
boolean flag1 = search.marked(10);
boolean flag2 = search.marked(8);
System.out.println("9号城市和10号城市是否已相通:" + flag1);
System.out.println("9号城市和8号城市是否已相通:" + flag2);
}
}
9号城市和10号城市是否已相通:true
9号城市和8号城市是否已相通:false
6 路径查找
6.1 问题分析
- 要实现路径查找,首先要遍历并搜索图,这里我们基于深度优先搜索来完成;
- 添加edgeTo[]数组,记录从s到每个相通顶点的路径;
设s为0,其搜索如图所示:
根据最终edgeTo的结果,我们很容易能够找到从起点0到任意顶点的路径。
6.2 代码实现
public class DepthFirstPaths {
//索引代表顶点,值表示当前顶点是否已经被搜索
private boolean[] marked;
//起点
private int s;
//索引代表顶点,值代表从起点s到当前顶点路径上的最后一个顶点
private int[] edgeTo;
//构造深度优先搜索对象,使用深度优先搜索找出g图中起点为s的所有路径
public DepthFirstPaths(Graph g, int s){
//初始化marked数组
this.marked = new boolean[g.V()];
//初始化起点
this.s = s;
//初始化edgeTo数组
this.edgeTo = new int[g.V()];
dfs(g,s);
}
//使用深度优先搜索找出g图中s顶点的所有相通顶点
private void dfs(Graph g, int s){
//把s表示为已搜索
marked[s] = true;
//遍历顶点s的邻接表,拿到每一个相邻的顶点
for (Integer w : G.adj(s)) {
//如果顶点w没有被搜索,则继续递归搜索
if (!marked[w]){
edgeTo[w] = s;//到达顶点w的路径上的最后一个顶点是s
dfs(G,w);
}
}
}
//判断v顶点与s顶点是否相通
public boolean hasPathTo(int v){
return marked[v];
}
//找出从起点s到顶点v的路径
public Deque<Integer> pathTo(int v){
if (!hasPathTo(v)){
return null;
}
//创建栈对象,保存路径中的所有顶点
Deque<Integer> path = new LinkedList<>();
//通过循环,从顶点v开始,一直往前找,到找到起点为止
for (int x = v; x!=s;x = edgeTo[x]){
path.push(x);
}
//把起点s放到栈中
path.push(s);
return path;
}
}
public class Test {
public static void main(String[] args) {
Graph g = new Graph(6);
g.addEdge(0,2);
g.addEdge(0,1);
g.addEdge(0,5);
g.addEdge(1,2);
g.addEdge(2,3);
g.addEdge(3,4);
DepthFirstPaths paths = new DepthFirstPaths(g,0);
Deque<Integer> path = paths.pathTo(4);
StringBuilder builder = new StringBuilder();
while(!path.isEmpty()) {
builder.append(path.pop() + "-");
}
builder.deleteCharAt(builder.length()-1);
System.out.println(builder);
}
}
0-2-3-4