算法——图之无向图

本文介绍了图的概念和分类,重点讲述了无向图的表示方法,包括邻接矩阵、边的数组和邻接表数组,其中邻接表数组在空间效率和访问相邻节点方面更具优势。接着,文章讨论了图的搜索算法,提供了深度优先搜索(DFS)和广度优先搜索(BFS)的模板,并指出在寻找最短路径时应使用广搜。最后,提到在实际应用中,如何处理字符串类型的节点,可以使用符号表将其映射为整数进行处理。

图的概念

图是算法中是树的拓展,树是从上向下的数据结构,结点都有一个父结点(根结点除外),从上向下排列。而图没有了父子结点的概念,图中的结点都是平等关系,结果更加复杂。


图的分类

图可以分为无向图(简单连接),有向图(连接有方向),加权图(连接带权值),加权有向图(连接既有方向又有权值)。


这篇讨论无向图。

无向图的表示方法:

1.邻接矩阵
2.边的数组
3.邻接表数组


1.邻接矩阵

我们可以使用一个V*V的布尔矩阵graph来表示图。当顶点v和顶点w之间有边相连时,则graph[v][w]和graph[w][v]为true,否则为false。

但是这种方法需要占用的空间比较大,因为稀疏图更常见,这就导致了很多空间的浪费。V*V的矩阵很多时候我们是不能接受的。


2.边的数组

我们可以使用一个数组来存放所有的边,这样的话数组的大小仅有E。但是因为我们的操作总是需要访问某个顶点的相邻节点,对于这种数据类型,要访问相邻节点的话必须遍历整个数组,造成效率的低下,所以我们在这里也不使用这个数据结构。


3.邻接表数组

我们使用一个链表数组来表示,数组中每个元素都是链表表头,链表中存放对应下标的节点所连接的边。

这种数据结构使用的空间为V+E。并且可以相当方便的获取相邻节点。

如图:




实现如下:

import java.util.ArrayList;
import java.util.List;

public class Graph {
	private List<Integer>[] adj; // 邻接表
	private int V;	// 顶点数目
	private int E;	// 边的数目
	
	public Graph(int V) {
		this.V = V;
		adj = (List<Integer>[])new List[V];
		E = 0;
		for (int i = 0; i < V; i++) {
			adj[i] = new ArrayList<Integer>();
		}
	}
	
	public void addEdge(int v, int w) {
		adj[v].add(adj[v].size(), w);
		adj[w].add(adj[w].size(), v);
		E++;
	}
	
	public List<Integer> adj(int v) {
		return adj[v];
	}
	
	public int V() {
		return V;
	}
	
	public int E() {
		return E;
	}
	
	public String toString() {
		String s = V + " 个顶点, " + E + " 条边\n";
		for (int i = 0; i < V; i++) {
			s += i + ": ";
			for (Integer node : adj(i)) {
				s += node + " ";
			}
			s += "\n";
		}
		return s;
	}

}

到此为止,我们已经完成了图的表示。


有了表示,我们就需要使用图完成一些简单的应用。

例如,图的搜索,图的连通分量,图是否有环等等。


首先我们来实现图的搜索。

我们在这里实现一个模板。并不实际进行搜索。

目标:给定一个起点,在图中进行搜索。

方案:1.深度优先搜索 2.广度优先搜索。


深搜

原理:

这里打个比喻,搜索图中所有的节点,就像走迷宫一样,需要探索迷宫中所有的通道。探索迷宫所有的通道,我们需要什么呢?

1.我们需要选择一条没有标记的路,并且一边走一遍铺上一条绳子。

2.标记走过的路。

3.当走到一个标记的地方时,我们需要回退,根据绳子回退到上一个地方。

4.当回退的地方没有可以走的路了,就要继续回退。

也就是说,首先我们需要一直走下去,但是我们一边走就要一边做标记,如果走不下去了,就回退,回退到没有被标记的的路。循环往复,我们就能探索整个图了。


实现:

import java.util.Stack;

public class DepthFirstSearch {
	private boolean[] isMarked;
	private int begin;
	private int count;
	private Integer[] edgeTo; 
	
	public DepthFirstSearch(Graph g, int begin) {
		isMarked = new boolean[g.V()];
		edgeTo = new Integer[g.V()];
		count = 0;
		this.begin = begin;
		dfs(g, begin);
	}
	
	public void dfs(Graph g, int begin) {
		isMarked[begin] = true;
		for (Integer i : g.adj(begin)) {
			if (!isMarked[i]) {
				edgeTo[i] = begin;
				count++;
				dfs(g, i);
			}
		}
	}
	
	
	public boolean hasPath(int v) {
		return isMarked[v];
	}
	
	public int count() {
		return count;
	}
	
	public String pathTo(int v) {
		if (!hasPath(v)) return "";
		Stack<Integer> stack = new Stack<>();
		stack.push(v);
		for (int i = v; i != begin; i = edgeTo[i]) {
			stack.push(edgeTo[i]);
		}
		
		return stack.toString();
	}
	
	
}
我们需要一个数组来标记某个节点是否已经走过了,如果走过了,我们就不会再走了。
并且我们有一个数组去保存是从哪个节点到达当前节点。这样,我们往回追朔的时候,就可以找到一条路径了。

这是一个模板,并没有具体的搜索某个节点,而是将所有节点都搜索了一遍,在实际过程中,我们可以判断节点是否找到,找到就停止了。


对于无向图来说,深搜虽然可以找到一条从v到w的路径,但是这条路径是否是最优的并不是可靠的,往往都不是。

如果我们希望找到一条最短的路径,我们就应该使用广搜。

广搜

原理:

广搜并不是先一条路走到黑,而是慢慢的根据距离进行搜索。例如,一开始先根据距离是1进行搜索,先搜索所有距离为1的地方。如果没找到,再搜索距离为2的地方。以此类推。

如果说深搜是一个人在迷宫中搜索,那么广搜就是一组人在朝着各个方向进行搜索。当然不是效率比较高的意思,只是比喻而已。


实现:

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

public class BreadthFirstSearch {
	
	private boolean[] isMarked;
	private Integer[] edgeTo;
	private int begin;
	private int count; // 多少个点连通
	
	public BreadthFirstSearch(Graph g, int begin) {
		isMarked = new boolean[g.V()];
		edgeTo = new Integer[g.V()];
		this.begin = begin;
		count = 0;
		bfs(g, begin);
	}
	
	private void bfs(Graph g, int begin) {
		Queue<Integer> queue = new LinkedList<>();
		isMarked[begin] = true;
		queue.offer(begin);
		while (!queue.isEmpty()) {
			Integer temp = queue.poll();
			for (Integer i : g.adj(temp)) {
				if (!isMarked[i]) {
					isMarked[i] = true;
					count++;
					edgeTo[i] = temp;
					queue.offer(i);
				}
			}
		}
	}
	
	public boolean hasPath(int v) {
		return isMarked[v];
	}
	
	public int count() {
		return count;
	}
	
	public String pathTo(int v) {
		if (!hasPath(v)) return "";
		Stack<Integer> stack = new Stack<>();
		stack.push(v);
		for (int i = v; i != begin; i = edgeTo[i]) {
			stack.push(edgeTo[i]);
		}
		
		return stack.toString();
	}

}


其实广搜和深搜的不同就在于搜索规则的不同,深搜使用的是stack的LIFO(后进先出)的思想,总是搜索最新的节点。而广搜则是使用queue的FIFO(先进先出)的规则。

就如同上面的一样,节点进入队列的顺序是根据距离的,所以我们就可以实现慢慢范围的扩大搜索。

同样的,我们也标记了进入节点的前一个节点,用来追踪路径。因为我们是根据范围搜索的,所以得到的就是最短路径。


我们可以使用广搜和深搜来实现很多应用,例如是否有环,是否是二部图等等。这里我们就不展开了。


我们上面的图的节点都是以数字作为标记的,而对于实际应用来讲,图的节点一般都不会是数字,而是String类型的字符串等。

要实现这种符号图,我们只需要将我们的代码进行一些扩展,使用符号表的方法,将字符串映射到某个整数上就可以了。

例如:


我们只需要在将字符串映射得到一个数字,也就是使用散列表的方式,存储成键值对,就可以继续使用上面的代码了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值