详解图论算法 图的宽度优先遍历 图的深度优先遍历 图的拓扑排序算法 kruskal算法 prim算法 Dijkstra算法

文章介绍了图的基础类、节点类和边类的定义,并提供了邻接表和邻接矩阵的表示方法。接着,文章详细讲解了如何将二维矩阵转换为邻接链表,并实现了Kruskal和Prim算法求解最小生成树,以及Dijkstra算法求解单源最短路径。此外,还给出了拓扑排序的算法实现。
摘要由CSDN通过智能技术生成

目录

 图的基础类:Graph类:

Node结点类:

 Edge边类:

 (1)邻接表:

 (2)邻接矩阵:

用数组表示图:

二维矩阵;

 由于图论算法,可以用多种图结构方法表示,所以我们可以做一个转换函数,把题中的图结构转化为自己的图结构,然后用自己已经写好的算法得出结果

将二维矩阵转为邻接链表算法:

 kruskal算法查找最小生成树的方法是:将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。

最小生成树


 图的基础类:Graph类:

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;

/**
 * @ProjectName: study3
 * @FileName: Graph
 * @author:HWJ
 * @Data: 2023/6/7 15:18
 */
public class Graph<V> {
    public HashMap<Integer, Node<V>> nodes;  // Integer编号  Node<V> 实际的结点    存放这个图里所有的结点
    public HashSet<Edge<V>> edges;            //存放这个图里的所有边

    public Graph() {
        this.nodes = new LinkedHashMap<>();
        this.edges = new HashSet<>();
    }
}

Node结点类:

import java.util.ArrayList;

/**
 * @ProjectName: study3
 * @FileName: Node
 * @author:HWJ
 * @Data: 2023/6/7 15:21
 */
public class Node <V>{
    public V value;        //这个结点的值
    public int in;        // 进入边的个数 入度
    public int out;       //出去边的个数  出度
    public ArrayList<Node<V>> nexts; //此结点的直接相邻的结点
    public ArrayList<Edge<V>> edges; //他与直接相邻结点的边

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

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                ", in=" + in +
                ", out=" + out +
                ", nexts=" + nexts +
                ", edges=" + edges +
                '}';
    }
}

 Edge边类:

/**
 * @ProjectName: study3
 * @FileName: Edge
 * @author:HWJ
 * @Data: 2023/6/7 15:27
 */
public class Edge<V> {//有向边
    public int weight;  //边的权重
    public Node<V> to;  //边的终点
    public Node<V> from;   //边的起点

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

}

 (1)邻接表:

 (2)邻接矩阵:

用数组表示图:

 数组arr[0] = 5 表示0这个结点指向5这个结点,但是这种表示方式,不能表示所有的图论模型

二维矩阵;

[3,0,2]  表示0结点指向2结点,边长度为3;

 由于图论算法,可以用多种图结构方法表示,所以我们可以做一个转换函数,把题中的图结构转化为自己的图结构,然后用自己已经写好的算法得出结果

将二维矩阵转为邻接链表算法:

// matrix  所有的边
    // N*3的矩阵
    // [weight, from, to]
    public static Graph<Integer> createGraph(Integer[][] matrix) { //将二维矩阵的图转化为邻接矩阵格式的图
        Graph<Integer> graph = new Graph<Integer>();
        for (int i = 0; i < matrix.length; i++) {
            Integer from = matrix[i][1];
            Integer to = matrix[i][2];
            Integer weight = matrix[i][0];
            if (!graph.nodes.containsKey(from)) {
                graph.nodes.put(from, new Node<Integer>(from)); //判断图是否有这个结点,没有就加入
            }
            if (!graph.nodes.containsKey(to)) {
                graph.nodes.put(to, new Node<Integer>(to));
            }
            Node<Integer> fromNode = graph.nodes.get(from);
            Node<Integer> toNode = graph.nodes.get(to);
            Edge<Integer> edge = new Edge<Integer>(weight, fromNode, toNode); //构建有向边
            fromNode.nexts.add(toNode);  // 将这个结点的下一个结点加入
            fromNode.out++;   //出数++
            toNode.in++;      //入数++
            graph.edges.add(edge);    //图论的edges加入这个边
            fromNode.edges.add(edge);   //这个结点,加入这个边
        }
        return graph;
    }

图的宽度优先遍历和广度优先遍历算法: 

public static void bfs(Node<Integer> head) { // 对一个图进行深度优先遍历
        // 这里的哈希表可以改为数组结构,这样可以节约常数时间
        // 这样的改法 适用于题目明确指出了(图论结点个数的范围不会超过某个常数值) 且他的value值为某个基础类型
        if (head == null) {
            return;
        }
        LinkedList<Node<Integer>> nodes = new LinkedList<>();   // 进行深度优先遍历 一定要用队列,先进入的先弹出
        HashSet<Node<Integer>> set = new HashSet<>();   //创建一个hashset, 防止进入环中就一直循环加入已经加过的结点
        // int[] set = new int[N]; 这里的表示那个最大常数值
        nodes.add(head);
        set.add(head);
        while (!nodes.isEmpty()) { // nodes不为空进入循环
            Node<Integer> node = nodes.pop();
            System.out.println(node);  //打印结点,可以根据题目定制处理
            for (Node<Integer> n : node.nexts) { // 循环当前结点的所有邻近结点
                if (!set.contains(n)) { //如果还没处理过此节点,就加入结点
                    set.add(n);
                    nodes.add(n);
                }
                // if (set[n.value] == 0){
                //     set[n.value] = 1;
                //     nodes.add(n);
                // }
            }
        }
    }


    public static void dfs(Node<Integer> head) { // 看不懂 就用纸和debug结合来研究
        if (head == null) {
            return;
        }
        Stack<Node<Integer>> nodes = new Stack<>();
        HashSet<Node<Integer>> set = new HashSet<>();
        nodes.add(head);
        set.add(head);
        System.out.println(head);
        while (!nodes.isEmpty()) {
            Node<Integer> node = nodes.pop();
            System.out.println(node);  //打印结点,可以根据题目定制处理
            for (Node<Integer> n : node.nexts) { // 循环当前结点的所有邻近结点
                if (!set.contains(n)) { //如果还没处理过此节点,就加入结点
                    nodes.push(node); //将其重新压入栈内
                    nodes.push(n);  //将其压入栈内
                    set.add(n);
                    System.out.println(n);  //打印结点,可以根据题目定制处理
                    break;  //省略当前结点的其他邻近结点
                }
            }
        }

    }

拓扑排序算法:

//有向图的无环   拓扑排序
    public static List<Node<Integer>> sortedTopology(Graph<Integer> graph){
        LinkedList<Node<Integer>> nodes = new LinkedList<>();  // 将所有入度为0的结点加入队列,弹出时再加入结果队列
        HashMap<Node<Integer>, Integer> nodeIntegerHashMap = new HashMap<>(); // 用来存放结点 和 入度的关系
        for (Node<Integer> node:graph.nodes.values()) {
            nodeIntegerHashMap.put(node, node.in);
            if (node.in == 0){
                nodes.add(node);
            }
        }
        LinkedList<Node<Integer>> result = new LinkedList<>();
        while (!nodes.isEmpty()){
            Node<Integer> node = nodes.pop(); // 弹出已知入度为0的结点,然后将其与其他结点的关系删除
            result.add(node);                 // 将这个结点加入结果列表中
            for (Node<Integer> n : node.nexts) {
                nodeIntegerHashMap.put(n, nodeIntegerHashMap.get(n)-1);
                if (nodeIntegerHashMap.get(n) == 0){  // 如果关系删除后,入度为0了 就加入此节点
                    nodes.add(n);
                }
            }

        }
        return result;
    }

kruskal算法: 

 

 kruskal算法查找最小生成树的方法是:将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。

算法实现:如果不形成环的边就加入此边,开始时,将所有结点独立生成一个集合,然后选择权值小的边,看to和from结点是否在一个集合,不在一个集合就合并且保留这个边,如果在一个集合就不要此边

最小生成树

所谓一个 带权图 的最小生成树,就是原图中边的权值最小的生成树 ,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和。

构建的Mysets类 

public class MySets<V> {
    public HashMap<Node<V>, List<Node<V>>> setMap;
    public MySets(List<Node<V>> nodes){
        for (Node<V> node : nodes) {
            List<Node<V>> set = new ArrayList<>();
            set.add(node);
            setMap.put(node, set);
        }
    }

    public boolean isSameSet(Node<V> from, Node<V> to){
        List<Node<V>> fromSet = setMap.get(from);
        List<Node<V>> toSet = setMap.get(to);
        return fromSet == toSet;
    }

    public void union(Node<V> from, Node<V> to){
        List<Node<V>> fromSet = setMap.get(from);
        List<Node<V>> toSet = setMap.get(to);
        for (Node<V> node : toSet) { // 将toSet集合里面的所有结点加入fromSet,并将结点指向fromSet
            fromSet.add(node);
            setMap.put(node, fromSet);
        }
    }
}

 kruskal算法:

public static Set<Edge<Integer>> kruskalMST(Graph<Integer> graph){
        MySets<Integer> integerMySets = new MySets<Integer>(new ArrayList<>(graph.nodes.values()));
        // 传入一个比较器, 使权值较小的边排在前边,完成小根堆
        PriorityQueue<Edge<Integer>> queue = new PriorityQueue<>(new Comparator<Edge<Integer>>() {

            @Override
            public int compare(Edge o1, Edge o2) {
                return o1.weight - o2.weight;
            }
        });
        //在小根堆里加入所有边
        queue.addAll(graph.edges);

        Set<Edge<Integer>> result = new HashSet<>();
        while (!queue.isEmpty()){
            //取出权值最小的边
            Edge<Integer> edge = queue.poll();
            // 判断fromNode 和 toNode 结点是否在同一个集合
            if (!integerMySets.isSameSet(edge.from, edge.to)){
                // 如果不在同一个集合
                // 加入这条边
                result.add(edge);
                // 并将这两个集合合并
                integerMySets.union(edge.from, edge.to);
            }
        }
        return result;
    }

prim算法: 

 

Prim算法是一种基于贪心策略的最小生成树算法,主要用于解决无向图的最小生成树问题,即在一个联通的加权无向图中找到一棵生成树,使得树上所有边的权值之和最小。

算法流程:

  1. 选定任意一个起始节点,将其加入集合S中。
  2. 从S中的所有节点出发,选择一条连接到未加入S中的节点的最小边,并将其加入生成树中。
  3. 将该节点加入集合S中,重复执行步骤2,直到集合S中包含所有节点。
public static Set<Edge<Integer>> primMST(Graph<Integer> graph){
        // 将解锁结点的所有边加入小根堆
        PriorityQueue<Edge<Integer>> queue = new PriorityQueue<>(new Comparator<Edge<Integer>>() {

            @Override
            public int compare(Edge o1, Edge o2) {
                return o1.weight - o2.weight;
            }
        });
        //
        HashSet<Node<Integer>> set = new HashSet<>();

        // 每次挑选的边都放入result中
        HashSet<Edge<Integer>> result = new HashSet<>();
        for (Node<Integer> node  : graph.nodes.values()) { // 随机选取某个结点   // 具有处理深林的能力

            if (!set.contains(node)){ // 如果不存在这个结点就加入,并解锁他的所有边
                set.add(node);
                queue.addAll(node.edges);
                while (!queue.isEmpty()){
                    // 取出最小的边,参看他的终点我们是否参看过,没有就加入并解锁他的所有边
                    Edge<Integer> edge = queue.poll();
                    if (!set.contains(edge.to)){
                        result.add(edge);
                        set.add(edge.to);
                        queue.addAll(edge.to.edges);
                    }
                }
            }
        }
        return result;
    }

 Dijkstra算法:

Dijkstra算法是一种解决单源最短路径问题的贪心算法。

该算法的基本原理是:从源节点开始,每次选择距离源节点最近的一个节点,然后以该节点为中心进行扩展,最终得到源节点到其他所有节点的最短路径。

具体实现步骤如下:

  1. 创建一个集合S,用来存储已经确定最短路径的节点。
  2. 初始化距离数组dist,表示源节点到其他节点的距离,将源节点的距离设置为0,其他节点的距离设置为无穷大。
  3. 从dist数组中选取距离源节点最近的一个节点V,并将其加入到集合S中。
  4. 对于节点V的所有邻居节点W,如果源节点到W的距离大于源节点到V再加上V到W的距离,就更新dist数组中W的距离。
  5. 重复步骤3和步骤4,直到集合S包含了所有节点。

最终得到的dist数组就是源节点到其他所有节点的最短路径长度。同时,可以通过记录每个节点的前驱节点,来还原出最短路径。

Dijkstra算法未优化版本:

public static HashMap<Node<Integer>, Integer> dijkstra1(Node<Integer> head){
        // 从头结点出发到其余所有结点的最小距离
        // key:头结点到所有结点
        // value:头结点到所有结点的最短距离
        HashMap<Node<Integer>, Integer> distances = new HashMap<>();
        distances.put(head, 0);
        // 如果在表中没有这条记录,就默认头结点到他的距离为无穷大
        HashSet<Node<Integer>> selectNodes = new HashSet<>();
        // 这里一定返回头结点, 这里返回离头结点最近的结点, 返回的结点不在已经锁存的结点内
        Node<Integer> cur = getMinDistanceAndUnselectedNode(distances, selectNodes);
        while (cur != null){
            int distance = distances.get(cur); //得到离原点的距离
            for (Edge<Integer>  edge: cur.edges) { //遍历这个结点的所有边
                Node<Integer> toNode = edge.to;
                if (!distances.containsKey(toNode)){ //如果这个结点还没到达过就直接添加这个结点和他的距离
                    distances.put(toNode, distance + edge.weight);
                }
                // 否则更新最短距离
                distances.put(toNode, Math.min(distances.get(toNode), distance + edge.weight));
            }
            // 运行后将这个结点锁存
            selectNodes.add(cur);
            cur = getMinDistanceAndUnselectedNode(distances, selectNodes);
        }
        return distances;

    }

    public static Node<Integer> getMinDistanceAndUnselectedNode(
            HashMap<Node<Integer>, Integer> distances,
            HashSet<Node<Integer>> selectNodes){
        Node<Integer> minNode = null;
        int minDistance = Integer.MAX_VALUE;
        for (Map.Entry<Node<Integer>, Integer> entry : distances.entrySet()) {
            Node<Integer> node = entry.getKey();
            int distance = entry.getValue();
            if (!selectNodes.contains(node) && distance < minDistance){ // 要求他不是已经锁存的结点且distance较小
                minNode = node;
                minDistance = distance;
            }
        }
        return  minNode;
    }

}

Dijkstra算法优化版本:

import java.util.HashMap;

/**
 * @ProjectName: study3
 * @FileName: OptimizeDijkstra
 * @author:HWJ
 * @Data: 2023/6/11 22:45
 * Dijkstra算法的优化
 */
public class OptimizeDijkstra {
    public static void main(String[] args) {

    }

    public static class MyRecord {
        public Node node; // 数组不能使用泛型
        public int distance;

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

    public static class NodeHeap {
        public Node[] nodes;
        public HashMap<Node, Integer> heapIndexMap;
        public HashMap<Node, Integer> distanceMap;
        public int size;

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

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

        public boolean isEntered(Node node) {
            return heapIndexMap.containsKey(node);
        }

        public boolean inHeap(Node node) {
            return isEntered(node) && heapIndexMap.get(node) != -1;
        }

        public void addOrUpdateOrIgnore(Node node, int distance) {
            if (inHeap(node)) { // 有这个结点的条件是,他加入过,且没有得到他独自的最小路径
                // 如果有这个结点,就选择最小值更新,更新后,因为是变小了所有只考虑往上做insertHeapify,使其变为小根堆
                heapIndexMap.put(node, Math.min(distanceMap.get(node), distance));
                insertHeapify(node, heapIndexMap.get(node));
            }
            // 如果没有这个结点,就新加入这个结点,然后往上做insertHeapify,使其变为小根堆
            if (isEntered(node)) {
                nodes[size] = node;
                heapIndexMap.put(node, size);
                distanceMap.put(node, distance);
                insertHeapify(node, size++);
            }
        }

        public MyRecord pop() {
            MyRecord myRecord = new MyRecord(nodes[0], distanceMap.get(nodes[0])); // 返回一个记录结点,方便得到distance
            swap(0, size - 1); // 将最后一个结点放在头部,然后开始往下做小根堆
            heapIndexMap.put(nodes[size - 1], -1);
            distanceMap.remove(nodes[size - 1]);
            nodes[size - 1] = null; // 此时这个弹出的头结点,置为null
            heapify(0, --size);
            return myRecord;
        }

        public void swap(int index1, int index2) { //交换两个结点
            heapIndexMap.put(nodes[index1], index2);
            heapIndexMap.put(nodes[index2], index1);
            Node temp = nodes[index1];
            nodes[index1] = nodes[index2];
            nodes[index2] = temp;
        }

        public void insertHeapify(Node node, int size) {
            int index = heapIndexMap.get(node);
            // 如果他比他的父结点小,就往上冒
            while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {

                swap(index, (index - 1) / 2);
                index = (index - 1) / 2;
            }
        }

        public void heapify(int index, int size) {
            int left = 2 * index + 1;
            while (left < size) {
                // 如果有右节点,就找到左右结点中小的那一个的索引
                int small = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left]) ? left + 1 : left;
                // 判断小的那个结点和父结点,那个小得到小的那个
                small = distanceMap.get(nodes[small]) < distanceMap.get(nodes[index]) ? small : index;
                if (small == index) { // 如果父结点index更小,说明不需要往下更新小根堆
                    return;
                }
                swap(small, index); //否则更新小根堆
                index = small;
                left = 2 * index + 1;
            }
        }
    }

    // 改进后的dijkstra算法
    // 从head出发,所有能到达的结点,生成到达每个结点的最小路径
    public static HashMap<Node, Integer> dijkstra(Node head, int size) {
        NodeHeap nodeHeap = new NodeHeap(size);
        // 先加入头结点head,路径为0
        nodeHeap.addOrUpdateOrIgnore(head, 0);
        HashMap<Node, Integer> result = new HashMap<>();
        while (!nodeHeap.isEmpty()) {//如果不为空,说明还有结点的路径没有更新
            MyRecord record = nodeHeap.pop(); // 弹出最小路径的结点,遍历他所有的边,更新其他结点的路径
            Node cur = record.node;
            int distance = record.distance;
            for (Edge edge : cur.edges) {
                nodeHeap.addOrUpdateOrIgnore(edge.to, distance + edge.weight);
            }
            result.put(cur, distance); // 然后在结果中加入这个结点
        }
        return result; // 返回结果
    }


}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Studying~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值