算法——图之无向图

来源:互联网 发布:2083欧洲独立宣言 知乎 编辑:程序博客网 时间:2024/05/18 19:21

图的概念

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


图的分类

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


这篇讨论无向图。

无向图的表示方法:

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类型的字符串等。

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

例如:


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

原创粉丝点击