七、图算法

1. 图的定义

  • 把节点通过任意方式连接起来,就叫图。二叉树也是图的一种。

  • 图分为有向图(带箭头)无向图(也叫双向图,不带箭头)

  • 基本代码

class Vertex{
    private String key; // 键值
    Color color; // 颜色(状态)
    int d; // 离根节点的最小距离
    Vertex pi; // 父节点
}

在这里插入图片描述

2. 图的表示方法

2.1 邻接链表

由X节点相邻的节点,构成的链表,称为邻接链表。

上图中,的邻接链表:

  • 1的邻接链表为 2 -> 5
  • 2的邻接链表为 1-> 3 -> 5 ->4

因此,写成类似代码的形式,就是

adjacencyList.put(1, 链表(2 -> 5))
adjacencyList.put(2, 链表(1-> 3 -> 5 ->4))
或者
adjacencyList.get(1) == 链表(2 -> 5) is true
adjacencyList.get(2) == 链表(1-> 3 -> 5 ->4) is true

链表的实现么,在前面的资料中已经讲过了。

2.2 邻接矩阵

在后面最小生成树算法中介绍。

3. 两种图的遍历算法BFS、DFS(重点)

理解遍历的顺序即可,考试让你写代码不太可能。重在理解遍历的顺序,很有可能让你写出一个图的遍历顺序。

代码看个热闹就行了,重在理解思想。

  • 时间复杂度:由于所有的点和边都遍历了一次,因此时间复杂度是O(V+E)。(V是Vertex节点,E是Edge边)

3.1 BFS 广度优先遍历

是指从根节点开始,一定要把根节点的所有相邻节点都访问完了,再选下一个节点作为根节点,重复上述操作。具体流程看代码注解。

public static void BFS(Map<Vertex, List<Vertex>> adjacencyList, Vertex s) {
   /**
     * 每个节点共有三种状态(颜色): 
     * 白色:未被访问,一开始节点都是白的
     * 灰色:已被访问
     * 黑色:已被访问,且其所有相邻节点也都被访问了
     * 变黑操作 == 访问该节点的所有相邻节点
     */
   
   // 将根节点访问并标灰,表示根节点已被访问
   s.color = Color.GRAY;
   System.out.print(s + " -> ");

   // 根节点与根节点的距离当然是0辣
   s.d = 0;
   // 这个队列,存放灰色的节点,接下来要把它们依次变黑
   Queue<Vertex> queue = new LinkedList<>();
   // 根节点现在还是灰色的,接下来想把它变黑
   queue.add(s);
   while (!queue.isEmpty()){
       // 弄出一个节点u,接下来想办法把它变黑
       Vertex u = queue.poll();
       if (adjacencyList.get(u) != null) {
 			// 遍历u节点的所有相邻节点
           for (Vertex v : adjacencyList.get(u)) {
               // 如果这个相邻节点是白色的,就表示还没访问过。回答一下PPT里老师的问题:为什么需要这个if?因为相邻节点还可能是灰色的,而灰色的节点已经访问过了,是不需要访问的,所以这个if是有必要的。
               if (v.color.equals(Color.WHITE)) {
                   // 访问并灰化
                   System.out.print(v + " -> ");
                   v.color = Color.GRAY;
                   // 子节点离根节点的距离,比父节点离根节点的距离远1个单位
                   v.d = u.d + 1;
                   // U是v的父节点
                   v.pi = u;
                   // v是灰色的节点,存入队列
                   queue.add(v);
               }
           }
       }
       // u节点的所有相邻节点都变灰了,因此它可以变黑了
       u.color = Color.BLACK;
   }
}

3.2 DFS 深度优先遍历

用挖矿游戏举例子:

  • BFS:非要从地球表面开始一层一层往下挖。
  • DFS:找一个点,然后一路向下挖。挖到底了,再换一个点继续挖。本质上是前序遍历
public static void DFS(Map<Vertex, List<Vertex>> adjacencyList, Vertex s) {
    /**
      * 每个节点共有两种状态(颜色): 
      * 白色:没挖过,一开始所有节点都是白的
      * 黑色:挖过了
      */
    
    	// 这个栈存放有哪些点是可以往下挖的
        Stack<Vertex> stack = new Stack<>();
    	// 根节点肯定可以往下挖
        stack.add(s);
    	// 只要有节点可以往下挖,就继续干
        while (!stack.isEmpty()) {
            // 取出一个节点,准备开挖
            Vertex u = stack.pop();
            // 只有白色节点没挖过。黑色节点挖过了,不用再挖
            if (u.color.equals(Color.WHITE)){
                System.out.print(u+" -> ");
                // 标记一下u节点现在挖过了(因为下一步是挖u的相邻节点,相邻节点都挖了,u节点自然也就挖了)
                u.color = Color.BLACK;
                if (adjacencyList.get(u) != null) {
                    // 把u节点的相邻节点都放进stack,待会儿挖
                    Iterator<Vertex> iterator = adjacencyList.get(u).iterator();
                    while (iterator.hasNext()) {
                        stack.push(iterator.next());
                    }
                }
            }
        }
    }

递归写法(老师PPT里的):

private int time;
public static void DFS(Map<Vertex, List<Vertex>> adjacencyList){
    time = 0;
    //回答老师PPT里的问题:为什么需要一个循环?难道不是从一个根节点开始,发散式的挖,就可以了吗?因为可能存在孤立点或孤立结构,它们与根节点可能没有连接。所以需要让所有点都做一次根节点。
    for (Vertex u : adjacencyList.keySet()){
        if (u.color.equals(Color.WHITE)){
            DFS_VISIT(adjacencyList, u);
        }
    }
}
private DFS_VISIT(Map<Vertex, List<Vertex>> adjacencyList, Vertex u){
    time += 1;
    u.d = time;
    u.color = Color.GRAY;
    for (Vertex v : adjacencyList.get(u)){
        if (v.color == Color.WHITE){
            v.pi = u;
            DFS_VISIT(adjacencyList, v);
        }
    }
    u.color = Color.BLACK;
    time += 1;
    // u.f是挖完节点u的结束时刻
    u.f = time;
}

3.3拓扑排序、 括号结构、强连通分量

不可能考。拓扑排序深度优先搜索没有任何区别,考了深度优先搜索就不会考它了。

括号结构强连通分量甚至都超出了计算机科学考研大纲,都是离散数学的内容,要是期末考了,我就,我就。。。。。。。我也没办法好吧在这里插入图片描述

4. 最小生成树 MST

  • 定义:用最短的边,把图中所有的节点都连接起来。

4.1 邻接矩阵

邻接矩阵,用于记录节点与节点之间的两两距离。若有n个节点,那么邻接矩阵就是n阶方阵。邻接矩阵中的元素Matrix[i, j]代表从节点i到节点j的距离。

  • 我的代码选择使用构造edge对象,并在edge对象中存储距离。你完全可以选择用邻接矩阵。只不过考试不会考察这种代码的书写的,所以也就是理解思想,看个热闹就行了。
class Edge {
    Vertex v1;
    Vertex v2;
    int weight;
}

4.2 KRUSKAL算法

  • 思路:将所有的边从小到大考虑,如果这条边不是多余的,那么就add这条边。
  • 时间复杂度:等同于快速排序的时间复杂度O(E·logE)
public static Set<Edge> Kruskal(Vertex[] vertices, Edge[] edges) {
    // 存放MST的边
    Set<Edge> A = new HashSet<>();
    
    // 把自己放入到自己所在的集合
    for (Vertex vertex : vertices){
        vertex.set = new HashSet<>();
        vertex.set.add(vertex);
    }
    // 将边按边长升序排序
    Arrays.sort(edges, (e1,e2)->e1.weight - e2.weight);
	// 从小到大遍历每条边
    for(Edge e : edges) {
        // 如果这条边的两个端点不在同一个集合中,那么就合并这两个端点所在的集合,同时这条边加入MST。
        if (e.v1.set != e.v2.set) {
            A.add(e);
            union(e.v1,e.v2);
        }
    }
    return A;
}
// 合并两个端点所在的集合
private static void union(Vertex v1, Vertex v2) {
    for (Vertex v : v2.set) {
        v.set = v1.set;
        v1.set.add(v);
    }
}

4.3 Prim算法

  • 思路:将一个节点连接到图中,至少需要一条边,这个节点的键值key就是这条边的长度(key值也就是将这条边加入到图中,所需付出的成本。成本越小越好)。从根节点开遍历临接链表中的边u,如果该边通往的另一个节点v原本的key值大于u的长度,那么用u的长度取代v的key值,而放弃v原本较大的key值。
public static void Prim(Vertex[] vertices, Map<Vertex, List<Edge>> adj, Vertex r) {
    // 一开始,所有节点的key值无穷大,也就是节点还未在图中
    for (Vertex v : vertices){
        v.key = Integer.MAX_VALUE;
        v.p = null;
    }
    // 根节点key值为0
    r.key = 0;
    // 优先级队列: 按键值从小到大顺序存放节点,后面需要将节点按键值由小到大遍历
    PriorityQueue<Vertex> Q = new PriorityQueue<>(vertices.length, (v1, v2)-> v1.key - v2.key);
    for (Vertex v : vertices) {
        Q.add(v);
    }
    while(!Q.isEmpty()){
     	// 弄一个键值最小的节点,开始考察它的所有邻边
        Vertex u = Q.poll();
        for (Edge e : adj.get(u)) {
            // 取得邻边通往的节点
            Vertex v = e.v1.equals(u) ? e.v2 : e.v1;
            // 如果节点还在优先级队列中,说明节点的邻边还未遍历。并且当边长小于节点的键值时,取代其键值。
            if (Q.contains(v) && e.weight < v.key){
                v.p = u;
                v.key = e.weight;
                // 更新优先级队列(使之有序),优先级队列是二叉堆。这里相当于二叉堆的decreaseKey操作。
                Q.add(Q.poll());
            }
        }
    }
    // 将最小生成树输出。(确定了所有节点的父节点,也就确定了整棵树)
    for (Vertex v : vertices) {
        System.out.println(v+".parent="+v.p);
    }

}
  • 时间复杂度:优先级队列decreaseKey的复杂度是树的深度logV,并且循环执行相当于遍历的两次所有的边2E,因此总体的时间复杂度是O(E·logV)

5. 单源最短路径算法 Dijkstra

  • 所谓单源最短路径,就是从同一个起点出发,到达其他点的最短距离。

  • Dijkstra算法继承了Prim算法的思想,只不过把键值替换成了到根节点的距离。

public static void Dijkstra(Vertex[] vertices, Map<Vertex, List<Edge>> adj, Vertex r) {
    for (Vertex v : vertices){
        v.d = Integer.MAX_VALUE;
        v.p = null;
    }
    r.d = 0;
    PriorityQueue<Vertex> Q = new PriorityQueue<>(vertices.length, (v1, v2)-> v1.d - v2.d);
    for (Vertex v : vertices) {
        Q.add(v);
    }
    while(!Q.isEmpty()){
        Vertex u = Q.poll();
        for (Edge e : adj.get(u)) {
            Vertex v = e.v1.equals(u) ? e.v2 : e.v1;
            if (Q.contains(v) && e.weight + u.d < v.d){
                v.p = u;
                // v到根节点的距离 = u到根节点距离 + uv间距
                v.d = e.weight + u.d;
                Q.add(Q.poll());
            }
        }
    }
    for (Vertex v : vertices) {
        System.out.println(v+".parent="+v.p);
    }
}

6. 总结

害,终于结束了讨人厌的图论算法,到此为止,基本的数据结构部分已经结束(其实还有一种叫做串的数据结构,不在我们的考试范围内)。如果你已经裂开了,那么就到此为止吧,混个七八十分已经没问题了。下一部分是动态规划,考试作为压轴题,但是也不是那种很复杂的题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值