【算法学习笔记】图

本文介绍了图的三种存储方法:邻接表、邻接矩阵和一种通用的存储结构,并提供了对应的Java代码实现。接着,详细阐述了图的宽度优先遍历和深度优先遍历算法。最后,讨论了拓扑排序、Kruskal算法、Prim算法以及Dijkstra算法的原理和代码实现,这些都是图论中的重要算法。
摘要由CSDN通过智能技术生成

由点集和边集构成的数据结构,不同点之间通过边互相连接,分为有向图和无向图。有向图的边带有方向,只能从一个点走到另一个点,不能反向;无向图的边则没有限制,点之间互相连通。

图的存储方式

以下图为例:
一个简单的图

图的存储方式多种多样,这里只介绍一些比较常用的存储方式。

1.邻接表法

用一个Map存储,Key为图中的某个节点,Value为与这个节点直接相邻的所有节点组成的列表(如果带权,可以将节点与权值封装为一个数据结构,同样用列表存储即可)。

2.邻接矩阵法

用一个n*n的矩阵存储,每一行的行号代表图中的某个节点,这一行的所有列存储这个图到其他直接相邻节点的距离(不相邻的距离为+∞,到自己的距离为0),即矩阵的每一个元素都相当于一条边。

3.本文所用的图的通用存储方式及代码(适用于大部分图的算法题,给定的如果不是这种结构可以先转换为该结构再写算法)

图类:

public class Graph {
    // 点集,Key为点编号
    public HashMap<Integer, Node> nodes;
    // 边集
    public HashSet<Edge> edges;

    public Graph() {
        nodes = new HashMap<>();
        edges = new HashSet<>();
    }
}

点类:

public class Node {
    // 节点值
    public int value;
    // 入度
    public int in;
    // 出度
    public int out;
    // 从该点能直接到达的邻居
    public ArrayList<Node> nexts;
    // 以该点为起点的边
    public ArrayList<Edge> edges;

    public Node(int value) {
        this.value = value;
        in = 0;
        out = 0;
        nexts = new ArrayList<>();
        edges = new ArrayList<>();
    }
}

边类:

public class Edge {
    // 权值
    public int weight;
    // 起点
    public Node from;
    // 终点
    public Node to;

    public Edge(int weight, Node from, Node to) {
        this.weight = weight;
        this.from = from;
        this.to = to;
    }
}

图的遍历

宽度优先遍历

思路:

  1. 利用队列实现;
  2. 从源节点开始依次按照宽度进队列,然后弹出;
  3. 每弹出一个节点,把该节点所有没有进过队列的节点进队列;
  4. 重复操作直到队列为空

代码实现如下:


// 宽度优先遍历
public static void bfs(Node node) {
    if (node == null) {
        return null;
    }
    Queue<Node> queue = new LinkedList<>();
    // 记录已经访问过的节点
    Set<Node> vis = new HashSet<>();
    queue.offer(node);
    vis.add(node);
    while (!queue.isEmpty()) {
        Node cur = queue.poll();
        System.out.println(cur.value);
        for (Node next : cur.nexts) {
            if (!vis.contains(next)) {
                set.add(next);
                queue.offer(next);
            }
        }
    }
}

深度优先遍历

思路:

  1. 利用栈实现;
  2. 从源节点开始把节点按照深度放入栈,然后弹出;
  3. 每弹出一个节点,把该节点和它的下一个没有进过栈的邻接点放入栈(如果还有未访问的邻接节点);
  4. 重复操作直到栈为空。

代码实现如下:

// 深度优先遍历
public static void dfs(Node node) {
    if (node == null) {
        return null;
    }
    // 栈中保存的是当前深度访问的路径
    Stack<Node> stack = new Stack<>();
    Set<Node> vis = new HashSet<>();
    stack.push(node);
    vis.add(node);
    System.out.println(node.value);
    while (!stack.isEmpty()) {
        Node cur = stack.pop();
        for (Node next : node.nexts) {
            // for循环寻找当前节点第一个未被访问到的邻接节点
            if (!set.contains(next)) {
                // 将当前节点重新压回栈中(用于回溯)
                stack.push(cur);
                stack.push(next);
                set.add(next);
                System.out.println(next.value);
                // 已经到下一个深度了,直接break退出for循环
                break;
            }
        }
    }
}

图的常用算法

拓扑排序

适用的范围:有向图,存在入度为0的节点,不存在环。
步骤:

  1. 从入度为0的点开始,将其的直接邻接节点的入度减一;
  2. 寻找下一个入度为0的节点,重复操作。

代码实现如下:

// 拓扑排序
public static List<Node> sortedTopology(Graph graph) {
    // Key:某一个node
    // Value:这个节点剩余的入度
    HashMap<Node, Integer> inMap = new HashMap<>();
    // 入度为0的节点才进入队列
    Queue<Node> zeroInQueue = new LinkedList<>();
    for (Node node : graph.nodes.values()) {
        inMap.put(node, node.in);
        if (node.in == 0) {
            zeroInQueue.offer(node);
        }
    }
    // 拓扑排序的结果
    List<Node> result = new ArrayList<>();
    while (!zeroInQueue.isEmpty()) {
        Node cur = zeroInQueue.poll();
        result.add(cur);
        for (Node next : node.nexts) {
            inMap.put(next, inMap.get(next) - 1);
            if (inMap.get(next) == 0) {
                zeroInQueue.offer(next);
            }
        }
    }
    return result;
}

Kruskal算法

适用范围:无向图,用于寻找最小生成树。
最小生成树:连通所有节点的加权和最小的边集。
算法思路:

  1. 对所有边按权值从小到大排序;
  2. 遍历排序后的边集,如果这条边添加后不会形成环,则添加,加入结果集;否则跳过
  3. 遍历完毕后得到的结果就是最小生成树。

如何判断加入边后是否形成环?

  1. 初始化时让每个节点各自位于一个节点集合中;
  2. 每添加一条边,将这条边的两个节点所属的集合合并;
  3. 如果一条边的两个节点在同一个集合中,则会形成环,否则不会。

代码实现如下:

// Kruskal算法(寻找最小生成树)
// 辅助类,可以用并查集替代(之后补充)
public static class MySets {

    public HashMap<Node, List<Node>> setMap;

    public MySets(List<Node> nodes) {
        // 初始化,每个节点单独形成一个集合
        for (Node node : nodes) {
            List<Node> set = new ArrayList<>();
            set.add(node);
            setMap.put(node, set);
        }
    }

    // 判断两个节点是否在同一个集合中
    public boolean isSameSet(Node from, Node to) {
        return setMap.get(from) == setMap.get(to);
    }

    // 将两个节点所在的集合合并
    public void union(Node from, Node to) {
        List<Node> fromSet = setMap.get(from);
        List<Node> toSet = setMap.get(to);
        for (Node toNode : toSet) {
            fromSet.add(toNode);
            setMap.put(toNode, fromSet);
        }
    }
}
// 算法部分
public static class EdgeComparator implements Comparator<Edge> {
    @override
    public int compare(Edge e1, Edge e2) {
        return e1.weight - e2.weight;
    }
}
public static Set<Edge> kruskalMST(Graph graph) {
    MySets mySets = new MySets(graph.nodes.values());
    // 优先队列,用于对边按权排序
    PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
    for (Edge edge : graph.edges) {
        priorityQueue.add(edge);
    }
    Set<Edge> result = new HashSet<>();
    while (!priorityQueue.isEmpty()) {
        Edge edge = priorityQueue.poll();
        if (!mySets.isSameSet(edge.from, edge.to)) {
            result.add(edge);
            mySets.union(edge.from, edge.to);
        }
    }
    return result;
}

Prim算法

适用范围:无向图,同样用于寻找最小生成树。
算法思路:

  1. 以任一个点为起点,将它加入点集,将以它作为端点的边全部加入边集;
  2. 对于当前点,选取边集中权值最小的那条边,判断其两端的点是否都在点集中,若不在,将这条边加入结果集;否则选次小的边,再次判断直到有符合条件的边;
  3. 若边均不符合条件(不是连通图,最小生成森林),再选取一个未加入的点作为起点,重复操作;
  4. 将新加入结果集的边新连通的那个节点加入点集,并将新连通节点带来的新边加入边集;
  5. 重复操作直到所有点都被连通,此时的结果集就是最小生成树。

代码实现如下:

// prim算法
public static Set<Edge> primMST(Graph graph) {
    // 新边加入优先队列
    PriorityQueue<Edge> p = new PriorityQueue<>(new EdgeComparator());
    // 已连通的节点
    HashSet<Node> set = new HashSet<>();
    // 结果集
    Set<Edge> result = new HashSet<>();

    // for循环用于处理非连通图,连通图可直接去掉
    for (Node node : graph.nodes.values()) { // 随便挑一个未连通节点
        if (!set.contains(node)) {
            // 开始节点
            set.add(node);
            // 将它的边加入优先级队列
            for (Edge edge : node.edges) {
                p.add(edge);
            }
            while (!p.isEmpty()) {
                // 当前权值最小的边
                Edge edge = p.poll();
                Node to = edge.to;
                if (!set.contains(to)) {
                    // 新点,加入集合
                    set.add(to);
                    result.add(edge);
                    for (Edge nextEdge : to.edges) {
                        // 不去重不影响结果,仅常数时间变大
                        p.add(nextEdge);
                    }
                }
            }
        }
    }
}

Dijkstra算法

适用范围:没有权值为负的边,用于寻找单源最短路径。
算法思路:

  1. 初始化一个距离Map,Key为节点,Value为起点到该点的最短距离,初始时节点到自己的距离为0,到其他节点的距离为+∞;
  2. 从起始节点开始,更新所有节点的最短距离;
  3. 取当前最近的节点,用它作为起点更新所有节点的最短距离(加上真正起点到这个节点的距离),该节点标记为已使用;
  4. 重复操作直到所有节点都被使用。

代码实现如下:

// Dijkstra算法
public static HashMap<Node, Integer> dijkstra(Node head) {
    // 从head出发到所有点的最小距离
    HashMap<Node, Integer> distanceMap = new HashMap<>();
    distanceMap.put(head, 0);
    // 已经求过距离的节点,存储在vis中,不再使用
    HashSet<Node> vis = new HashSet<>();
    // 取当前未使用的最近的节点
    Node minNode = getMinDistanceAndUnvisNode(distanceMap, vis);
    while (minNode != null) {
        // 源点到当前节点的距离
        int distance = distanceMap.get(minNode);
        for (Edge edge : minNode.edges) {
            Node to = edge.to;
            if (!distanceMap.containsKey(to)) {
                // 未访问过的节点,加入
                distanceMap.put(to, distance + edge.weight);
            } else {
                // 已访问的节点,更新
                distanceMap.put(to, Math.min(distance + edge.weight, distanceMap.get(to)));
            }
            
        }
        // 将当前节点设置为已使用
        vis.add(minNode);
        minNode = getMinDistanceAndUnvisNode(distanceMap, vis);
    }
    return distanceMap;
}

public static Node getMinDistanceAndUnvisNode(HashMap<Node, Integer> distanceMap, HashSet<Node> vis) {
    Node minNode = null;
    int minDistance = Integer.MAX_VALUE;
    for (Entry<Node, Integer> entry : distanceMap.entrySet()) {
        Node node = entry.getKey();
        int distance = entry.getValue();
        if (!vis.contains(node) && distance < minDistance) {
            minNode = node;
            minDistance = distance;
        }
    }
    return minNode;
}
用自建堆优化Dijkstra

前面我们使用哈希表来存储源点到各个节点的距离,这样在取最近节点时需要遍历哈希表,效率不高。那么我们可以用小根堆来加速这一获取过程,但是要注意,Dijkstra在运算过程中节点与源点的距离值会改变,也即堆中的值会变化,我们需要在节点值变化时自动重排小根堆维持其堆顶始终为最小值,而原生的小根堆没有这个功能,所以我们需要自建堆实现相应功能。代码如下:

// 堆弹出的信息类
public static class NodeRecord {
    public Node node;
    public int distance;

    public NodeRecord(Node node, int distance) {
        this.node = node;
        this.distance = distance;
    }
}

// 自建小根堆
public static class NodeHeap {
    // 堆的底层结构
    private Node[] nodes;
    // 记录每个节点在数组中的下标
    private HashMap<Node, Integer> heapIndexMap;
    // 每个节点离源点的距离
    private HashMap<Node, Integer> distanceMap;
    // 堆的大小
    private int size;

    public NodeHeap(int size) {
        nodes = new Node[size];
        heapIndexMap = new HashMap<>();
        distanceMap = new HashMap<>();
        this.size = 0;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public void addOrUpdateOrIgnore(Node node, int distance) {
        if (inHeap(node)) {
            // 在堆上,更新最小距离
            distanceMap.put(node, Math.min(distanceMap.get(node), distance));
            // 上浮堆化
            insertHeapify(node, heapIndexMap.get(node));
        }
        if (!isEntered(node)) {
            // 未进入过堆的新节点
            nodes[size] = node;
            heapIndexMap.put(node, size);
            distanceMap.put(node, ditance);
            insertHeapify(node, size++);
        }
        // 进过堆已经不在的节点直接忽略(已经得到最短路的节点)
    }

    // 弹出堆顶
    public NodeRecord pop() {
        NodeRecord record = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));
        swap(0, size - 1);
        heapIndexMap.put(nodes[size - 1], -1);
        distanceMap.remove(nodes[size - 1]);
        nodes[size - 1] = null;
        // 下沉堆化
        heapify(0, --size);
        return nodeRecord;
    }

    // 插入元素时或某个节点值变小时维护小根堆(上浮)
    private void insertHeapify(Node node, int index) {
        while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {
            swap(index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    // 下沉堆化(弹出节点后将最后一个节点交换到顶部了,将其下沉到合适位置)
    private void heapify(int index, int size) {
        int left = index * 2 + 1;
        while (left < size) {
            // 左右子节点中较小的那一个的下标
            int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left]) ? left + 1 : left;
            // 较小的子节点与当前节点比较
            smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;
            if (smallest == index) {
                // 如果当前节点已经比两个子节点都要小了,退出
                break;
            }
            swap(smallest, index);
            index = smallest;
            left = index * 2 + 1;
        }
    }

    // node 是否进入过堆
    public boolean isEntered(Node node) {
        return heapIndexMap.containsKey(node);
    }

    // node 当前是否在堆中
    public boolean inHeap(Node node) {
        return isEntered(node) && heapIndexMap.get(node) != -1;
    }

    // 交换堆中两个节点的位置
    private void swap(int index1, int index2) {
        heapIndexMap.put(nodes[index1], index2);
        heapIndexMap.put(nodes[index2], index1);
        Node tmp = nodes[index1];
        nodes[index1] = nodes[index2];
        nodes[index2] = tmp;
    }
}
// 用自建堆优化的Dijkstra算法,size为图中节点数量
// 从head出发,所有能到达的节点,生成到达每个节点的最短路径并返回
public static HashMap<Node, Integer> dijkstra(Node head, int size) {
    NodeHeap nodeHeap = new  NodeHeap(size);
    nodeHeap.addOrUpdateOrIgnore(head, 0);
    HashMap<Node, Integer> result = new HashMap<>();
    while (!nodeHeap.isEmpty()) {
        NodeRecord record = nodeHeap.pop();
        Node cur = record.node;
        int distance = record.distance;
        for (Edge edge : cur.edges) {
            nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
        }
        result.put(cur, distance);
    }
    return result;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值