图的概念
图是算法中是树的拓展,树是从上向下的数据结构,结点都有一个父结点(根结点除外),从上向下排列。而图没有了父子结点的概念,图中的结点都是平等关系,结果更加复杂。
图的分类
图可以分为无向图(简单连接),有向图(连接有方向),加权图(连接带权值),加权有向图(连接既有方向又有权值)。
这篇讨论无向图。
无向图的表示方法:
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类型的字符串等。
要实现这种符号图,我们只需要将我们的代码进行一些扩展,使用符号表的方法,将字符串映射到某个整数上就可以了。
例如:
我们只需要在将字符串映射得到一个数字,也就是使用散列表的方式,存储成键值对,就可以继续使用上面的代码了。
本文介绍了图的概念和分类,重点讲述了无向图的表示方法,包括邻接矩阵、边的数组和邻接表数组,其中邻接表数组在空间效率和访问相邻节点方面更具优势。接着,文章讨论了图的搜索算法,提供了深度优先搜索(DFS)和广度优先搜索(BFS)的模板,并指出在寻找最短路径时应使用广搜。最后,提到在实际应用中,如何处理字符串类型的节点,可以使用符号表将其映射为整数进行处理。
992

被折叠的 条评论
为什么被折叠?



