[关闭]
@pastqing 2016-03-28T12:26:44.000000Z 字数 6066 阅读 5488

图的常见算法

java


一、图的基本知识

显然如果一个图有n个顶点,但是边小于n - 1个,则此图肯定是非连通图。

二、图的表示

图的表示方式基本有两种,邻接矩阵(adjacency matrix)与邻接表(adjacency list)

邻接矩阵(adjacency matrix)

下图是无向图和有向图邻接矩阵的表示示意图。
adjency-matrix.png-28kBadjency-matrix2.png-32.6kB
下面给出一个adjecncy matrix 的简单代码表示

  1. public class AdjencyMatrixGraph {
  2. private int vertic; //the nums of vertices
  3. private int edge; //the nums of edges
  4. private boolean[][] adjencyMatrix;
  5. public AdjencyMatrixGraph(int vertic) {
  6. if(vertic < 0)
  7. throw new RuntimeException("vertices can not be negative");
  8. this.vertic = vertic;
  9. edge = 0;
  10. adjencyMatrix = new boolean[vertic][vertic];
  11. }
  12. //add undirected edge v-w
  13. public void addEdge(int v, int w) {
  14. if(!adjencyMatrix[v][w]) {
  15. edge++;
  16. adjencyMatrix[v][w] = adjencyMatrix[w][v] = true;
  17. }
  18. }
  19. public boolean contain(int v, int w) {
  20. return adjencyMatrix[v][w];
  21. }
  22. }

邻接表(adjacency matrix)

下图是无向图和有向图邻接表的表示示意图。
adjency-list.png-33.1kB
adjency-list2.png-39kB

我们设计一个数据结构如下:
adjacency-lists.png-37.9kB

下面给出简单代码:

  1. //A single linkedList
  2. public class Bag<T> implements Iterable<T> {
  3. private class Node<T> {
  4. T element;
  5. Node<T> next;
  6. public Node(T element, Node<T> next) {
  7. this.element = element;
  8. this.next = next;
  9. }
  10. }
  11. private Node<T> first;
  12. private int num;
  13. public Bag() {
  14. first = null;
  15. num = 0;
  16. }
  17. public boolean contain(T element) {
  18. Node<T> cur = first;
  19. while(cur != null){
  20. if(element.equals(cur.element)) {
  21. return true;
  22. }
  23. cur = cur.next;
  24. }
  25. return false;
  26. }
  27. //first insert
  28. public void add(T element) {
  29. if(contain(element)) {
  30. return ;
  31. }
  32. Node<T> oldFirst = first;
  33. first = new Node<T>(element, oldFirst);
  34. num++;
  35. }
  36. public int size() {
  37. return num;
  38. }
  39. public boolean isEmpty() {
  40. return first == null;
  41. }
  42. public Iterator<T> iterator() {
  43. return new ListIterator<T>(first);
  44. }
  45. // an iterator, doesn't implement remove() since it's optional
  46. private class ListIterator<T> implements Iterator<T> {
  47. private Node<T> current;
  48. public ListIterator(Node<T> first) {
  49. current = first;
  50. }
  51. public boolean hasNext() { return current != null; }
  52. public void remove() { throw new UnsupportedOperationException(); }
  53. public T next() {
  54. if (!hasNext()) throw new NoSuchElementException();
  55. T item = current.element;
  56. current = current.next;
  57. return item;
  58. }
  59. }
  60. }
  1. public class AdjecncyListGraph {
  2. private int vertic; //the nums of vertices
  3. private int edge; //the nums of edges
  4. Bag<Integer>[] bags;
  5. public AdjecncyListGraph(int vertic) {
  6. if(vertic < 0)
  7. throw new RuntimeException("vertices can not be negative");
  8. this.vertic = vertic;
  9. edge = 0;
  10. bags = new Bag[vertic];
  11. for(int i = 0; i < vertic; i++)
  12. bags[i] = new Bag<Integer>();
  13. }
  14. public void addEdge(int v, int w) {
  15. if(v < 0 || v > vertic) {
  16. throw new IndexOutOfBoundsException();
  17. }
  18. if(w < 0 || w > vertic) {
  19. throw new IndexOutOfBoundsException();
  20. }
  21. edge++;
  22. bags[v].add(w);
  23. bags[w].add(v);
  24. }
  25. //求顶点的度
  26. public int degree(int v) {
  27. return bags[v].size();
  28. }
  29. public String toString() {
  30. StringBuilder s = new StringBuilder();
  31. s.append(vertic + " vertices, " + edge + " edges " + "\n");
  32. for (int v = 0; v < vertic; v++) {
  33. s.append(v + ": ");
  34. for (int w : bags[v]) {
  35. s.append(w + " ");
  36. }
  37. s.append("\n");
  38. }
  39. return s.toString();
  40. }
  41. }

二、图的遍历

广度优先搜索(Breadth-First-Search,BFS)

BFS类似于层序遍历,它的基本思想是:首先访问起始顶点v, 接着从v出发,依次访问各个未访问过的邻接顶点w1,w2....,然后再依次访问w1,w2...的所有未被访问过的邻接顶点。依次类推,直到图中所有的顶点都被访问过为止。如下例子:

图片来源:这里
bfs.png-7.3kB
bfs遍历的结果就是: 1->2->3->4->5->6->7->8

算法导论对bsf算法描述是这样的:
bfs_1.png-73.7kB
s.d 表示s到邻接结点的距离, s.π表示其前驱。
白色表示未访问过的结点,灰色表示发现了的结点,黑色表示已访问过的结点

算法描述:

  1. 访问给定源结点v,并标记该点v已被访问
  2. 将结点v进队
  3. 当队列非空时,继续执行,否则算法结束
  4. 出队,取队头结点u
  5. 访问结点u的第一个邻接点w
  6. 若w不存在,则转到第3步,若w存在则循环执行以下3步
    1. 结点w未被访问,将w标记为已访问
    2. 将结点w入队
    3. 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6

这里我只放上bsf的关键代码

  1. private void bfs(AdjecncyListGraph g, int s) {
  2. for(int v = 0; v < g.vNums(); v++) {
  3. distance[v] = Integer.MAX_VALUE;
  4. }
  5. LinkedList<Integer> queue = new LinkedList<Integer>();
  6. distance[s] = 0;
  7. marked[s] = true;
  8. queue.offer(s);
  9. while(!queue.isEmpty()) {
  10. int v = queue.poll();
  11. path.add(v);
  12. for(int w : g.getBags(v)) {
  13. if(!marked[w]) {
  14. edgeTo[w] = v;
  15. distance[w] = distance[v] + 1;
  16. marked[w] = true;
  17. queue.offer(w);
  18. }
  19. }
  20. }
  21. }

BFS需要一个辅助队列,n个顶点都要入队一次,空间复杂度为O(n)

当采用邻接表存储时,每个顶点都需要搜索一次,时间复杂度为O(n)。在搜索任一顶点时,每条边至少访问一次,时间复杂度为O(E)。因此总的时间复杂度为O(N + E)

当采用邻接矩阵存储时,查找每个邻接点的时间为O(N),因此时间复杂度为O(n^2)


深度优先搜索(Depth-First-Search,DFS)

DFS类似树的先序遍历。基本思想是:首先访问某个起始顶点v,然后从v出发,访问与v邻接且未被访问的任一顶点w1, 再访问与w1邻接且未被访问的任一顶点w2, ...重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点。若它还有其他的邻接点未被访问过,则从该点开始上面的过程。直到图中的所有点都被访问过为止。

图片来源: 这里

dfs.png-7.3kB
dfs遍历的结果就是: 1->2->4->8->5->3->6->7

算法描述:

  1. 访问初始结点v,并标记结点v为已访问
  2. 查找结点v的第一个邻接结点w
  3. 若w存在,则继续执行4,否则算法结束
  4. 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123
  5. 查找结点v的w邻接结点的下一个邻接结点,转到步骤3
    其实dfs就是一个递归过程
  1. public void dfs(AdjecncyListGraph g, int v){
  2. marked[v] = true;
  3. paths.add(v);
  4. for(int w : g.getBags(v)) {
  5. if(!marked[w]) {
  6. edgeTo[w] = v;
  7. marked[w] = true;
  8. dfs(g, w);
  9. }
  10. }
  11. }

当采用邻接矩阵存储时,查找每个顶点的邻接点的时间为O(V),因此总的时间复杂度为O(V^2)

当采用邻接表存储时,查找每个顶点的邻接点的时间为O(E),访问顶点的时间复杂度为O(V),因此总的时间复杂度为O(V + E)


三、图的常见算法

1.最小生成树(Minimum-Spanning-Tree)

关于生成树:一个连通图的极小连通子图,它包含了图的所有顶点,并且只含尽可能少的边。这意味着对于生成树来说,若砍去它的一条边,就会使生成树变成非连通图,增加一条边就会出现环。

对于一个带权的无向图G, 生成树不同,每颗树的权也不一定相同。其中权值最小的那颗,即为最小生成树。

  1. 最小生成树不是唯一的,即最小生成树的树形可能不同。当图G中的各边的权值互不相等时,它的最小生成树是唯一的。若无向连通图G的边比顶点数少1,即G本身就是一棵树,则最小生成树就是它本身。
  2. 最小生成树的边权值之和是唯一的。
    下面给出一个MST的通用算法伪代码:
  1. MST(G) {
  2. T = NULL;
  3. while T 未形成一颗生成树
  4. do 找到权值最小的边,并且该边加入T后不会产生回路
  5. T = T T
  6. }

Prim算法

Prim算法是用来从带权图中搜索最小生成树的一种算法。
算法过程如下:

  • 输入:一个加权连通图,其中顶点集合为V,边集合为E;
    • 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {};
    • 重复下列操作,直到Vnew = V:
    • 在集合E中选取权值最小的边(u, v),其中u为集合Vnew中的元素,而v则是V中没有加入Vnew的顶点(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
    • 将v加入集合Vnew中,将(u, v)加入集合Enew中;
      输出:使用集合Vnew和Enew来描述所得到的最小生成树。

算法伪代码:

  1. Prim(G, T) {
  2. T = NULL;
  3. U = {w}; //添加任一顶点w
  4. while((V - U)!=NULL) {
  5. 设(u, v)是u U v V - U),且权值最小的边
  6. T = T (u, v);
  7. U = U v ;
  8. }
  9. }

Prim算法时间复杂度为O(V^2),比较适合于求稠密图的最小生成树。


Kruskal算法

Kruskal算法是一种按权值的递增次序选择合适的边来构成最小生成树的方法。

  1. 新建图G,G中拥有原图中相同的节点,但没有边
  2. 将原图中所有的边按权值从小到大排序
  3. 从权值最小的边开始,如果这条边连接的两个节点于图G中不在同一个连通分量中,则添加这条边到图G中
  4. 重复3,直至图G中所有的节点都在同一个连通分量中

算法伪代码:

  1. KRUSKAL-FUNCTION(G, w)
  2. F := 空集合
  3. for each G 中的顶点 v
  4. do v 加入森林 F
  5. 所有的边(u, v) E依权重 w 递增排序
  6. for each 边(u, v) E
  7. do if u v 不在同一棵子树
  8. then F := F {(u, v)}
  9. u v 所在的子树合并

2.最短路径

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注