Python 每日一记274@Java加权无向图最小生成树实现(Kruskal算法)

Kruskal算法

在这里插入图片描述
初始状态下,每个顶点都是独立的树,将图所有的边装入最小优先队列,最小优先队列按照权重将边排序,每次从队列中弹出权重最小的边,判断这个边的两个顶点是否在一个树中,如果不在就合并分组,并且将这条边放入到最小生成树中。不断重复这个操作,知道所有的顶点在一棵树中,最小生成树就完成了。

java代码

package mypackage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Iterator;

//队列类,用链表实现,后面有用
class Queue<T> implements Iterable<T>{
    //    节点个数,头节点,尾节点
    private int N;
    private Node head;
    private Node last;
    //节点类
    public class Node {
        public T data;
        public Node next;

        public Node(T data, Node next) {
            this.data = data;
            this.next = next;
        }
    }
    //构造方法,初始化
    public Queue() {
        this.N = 0;
        this.head = new Node(null,null);
        this.last = null;
    }
    //队列长度
    public int size(){
        return N;
    }
    //队列是否为空
    public boolean isEmpty(){
        return N==0;
    }
    //入队列
    public void enqueue(T data){
//        如果队列为空,说明尾节点为空,让新节点为尾节点,头借点指向尾节点
        if (isEmpty()){
            last=new Node(data,null);
            head.next=last;
//            如果队列不为空,让新节点为尾节点,老的尾节点指向新尾节点
        }else {
            Node oldlast=last;
            last=new Node(data,null);
            oldlast.next=last;
        }
//        最后元素+1
        N++;
    }
    //出队列,注意先入先出,每次出的节点就是head指向的第一个节点,然后让head只想第二个节点即可
//    且注意,如果队列为空,要将last=null
    public T dequeue(){
//        如果为空,返回null
        if (isEmpty()){
            return null;
//            如果不为空,让head只想第二个节点,元素-1,且如果队列为空,要将last=null
        }else {
            Node oldfirst=head.next;
            head.next=oldfirst.next;
            N--;

            if (isEmpty()){
                last=null;
            }
//            返回弹出的元素
            return oldfirst.data;
        }
    }

    //    遍历
    @Override
    public Iterator iterator() {
        return new QIterator();
    }
    //    创建一个内部类实现Iterator接口
    public class QIterator implements Iterator {
        //        定义一个遍历的节点
        private Node n;

        public QIterator() {
//            初始化为0索引位置
            this.n = head;
        }

        //重写两个方法
        @Override
        public boolean hasNext() {
//            这个方法判断是否超出最大索引,如果超出会停止遍历
            return n.next != null;
        }

        @Override
        public Object next() {
//            这个方法会遍历得每个节点
            n = n.next;
            return n.data;
        }
    }
}

//堆类,最小优先队列
//T extends Comparable<T>,后续的数据才可进行比较
class MinPriorityQueue<T extends Comparable<T>>{
    //    实现基础为数组,元素个数N
    private T[] items;
    private int N;
    //    构造方法,注意capacity+1,因为我们第一个元素是从索引1开始的,0处的索引不装数据
//    因此虽然我们传入的容量为capacity,这个capacity表示的是堆的数量,capacity+1才是数组的大小
    public MinPriorityQueue(int capacity) {
        this.items = (T[]) new Comparable[capacity+1];
        N = 0;
    }
    //堆元素个数
    public int size(){
        return N;
    }
    //是否为空
    public boolean isEmpty(){
        return N==0;
    }
    //    判断数组元素的大小
    public boolean less(int i,int j){
        return items[i].compareTo(items[j])<0;
    }
    //    交换数组元素
    public void exchange(int i,int j){
        T temp=items[i];
        items[i]=items[j];
        items[j]=temp;
    }

    //    插入元素
    public void insert(T t){
//        先将元素个+1,然后将值放在数组的最后位置,然后使用上浮算法swim让堆的元素有序
//        最开始N=0,N++使得N=1,然后这个位置放插入的元素,以此类推,每次插入元素前都N++,再在新的位置N处插入元素
        N++;
        items[N]=t;
        swim(N);
    }
    //浮算法swim让堆的元素有序
//    k位置处的父节点位置为k/2,左子节点为2k,右子节点位置为2k+1
//    浮算法swim就是不断的循环比较,如果这个节点小于父节点,就交换
    public void swim(int k){
        while (k>1){
            if (less(k,k/2)){
                exchange(k,k/2);
            }else {
                break;
            }
//            每次k=k/2
            k=k/2;
        }
    }

    //    删除最小的元素并返回,其实就是删除根节点的元素
//    先交换第一个元素和最后一个元素,然后删除最后一个元素,这个就是最小的元素
//    因为此时顺序是乱的,然后采用下沉算法,使得堆有序
    public T delmin(){
        T min=items[1];
        exchange(1,N);
        items[N]=null;
        N--;
        sink(1);
        return min;
    }

    //    下沉算法,使得堆有序,这是比较麻烦的算法
//    思路就是,从某个节点开始,向下循环
//    如果有右节点,就比较左右节点的大小,选取较小的一个最为较小值,如果没有右节点,那么左节点作为较小值
//    如果当前节点比较小的节点大,就交换,且让当前索引设置为较小值得索引,继续下沉循环
    public void sink(int k) {
        while (2*k<=N){
            int min;
            if (2*k+1<=N){
                if (less(2*k,2*k+1)){
                    min=2*k;
                }else {
                    min=2*k+1;
                }
            }else {
                min=2*k;
            }

            if (less(k,min)){
                break;
            }
            exchange(k,min);
            k=min;
        }
    }
}

//边类,因为加权无向图的边比较麻烦,因此专门用一个类表示边
class Edge implements Comparable<Edge>{
    //两个顶点,一个权重
    private int v;
    private int w;
    private double weigth;

    public Edge(int v, int w, double weigth) {
        this.v = v;
        this.w = w;
        this.weigth = weigth;
    }

    //获取边的权重值
    public double getWeigth(){
        return weigth;
    }
    //获取边一个点
    public int either(){
        return v;
    }

    //获取边另一个点
    public int other(int x){
        if (x==v){
            return w;
        }else {
            return v;
        }
    }
    //重写compareTo方法,提供比较规则,以weigth比较边的大小
    @Override
    public int compareTo(Edge o) {
        int cmp;
        if (this.weigth>o.weigth){
            cmp= 1;
        }else if (this.weigth==o.weigth){
            cmp= 0;
        }else {
            cmp= -1;
        }
        return cmp;
    }
}


//并查集
class UF{
    //并查集数组,索引为元素,值为元素分组的父节点,根据父节点找根节点,根节点才是分组标志
    // 比如索引0对应的值为2,然后再找索引2的值为4,索引4的值为4,直到索引对应的值和索引相同,即停止,表示索引0对应的分组为4
    private int[] arr;
    private int count;//分组个数,多少个组

    //构造方法,传入分组的个数,默认每个元素单独一组
    public UF(int N) {
        this.count = N;
        this.arr=new int[N];
        //每个元素一组
        for (int i = 0; i < arr.length; i++) {
            arr[i]=i;
        }
    }
    //获取分组的个数
    public int getCount(){
        return count;
    }
    //查找某个元素所在的组,注意元素就是索引,所在的组就是数组的值
    public int find(int p){
        while (true){
            if (p==arr[p]){
                return p;
            }else {
                p=arr[p];
            }
        }
    }
    //判断是否在一个分组中,即查看是否这两个索引对应的值是否相等
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }
    //合并分组,即使得两个元素处于同一组,即使得两个索引处的值相等
    public void union(int p,int q){
        int pGroup=find(p);
        int qGroup=find(q);
        if (pGroup==qGroup){
            return;
        }else {
//            直接让根节点的值为qGroup,因为根节点才是组的标识
            arr[pGroup]=qGroup;
            count--;
        }
    }
}
//加权无向图,注意边是单独的一个类实现的
class WeigthGraph{
    private int V;//顶点数
    private int E;//边数
    //adj是一个数组,数组的类型的队列,队列里面装的数据类型是Edge边类
    private Queue<Edge>[] adj;//邻接表,注意索引是每个顶点
    //构造方法,传入顶点个数,初始化边数为0,
    public WeigthGraph(int v) {
        this.V = v;
        this.E=0;
        this.adj=new Queue[v];//初始化队列数组,大小为顶点的个数
        for (int i = 0; i <adj.length ; i++) {
            adj[i]=new Queue<Edge>();//初始化数组的每个队列
        }
    }

    //获得顶点的个数
    public int getV(){
        return V;
    }

    //添加边,注意这里的边是单独的类实现的,传入一个边参数,让这个边在两个顶点的邻接表中
    //真正的边是需要实现类的
    public void addEdge(Edge edge){
        int v=edge.either();
        int w=edge.other(v);
        adj[v].enqueue(edge);
        adj[w].enqueue(edge);
        E++;//边数+1
    }

    //    获取边的个数
    public int getE(){
        return E;
    }

    //获取某个顶点相邻的所有边,返回这个队列即可
    public Queue<Edge> getAdj(int v){
        return adj[v];
    }

    //获取图的所有边
    public Queue<Edge> getEdges(){
        Queue<Edge> allEdges=new Queue<>();
        for (int v = 0; v <V ; v++) {
            for (Edge e:adj[v]) {
              if (e.other(v)>v){
                  allEdges.enqueue(e);
              }
            }
        }
        return allEdges;
    }
}


//Kruskal算法,获得最小生成树
class Kruskal {
    private Queue<Edge> edges;//保存最小生成树的所有边
    private UF uf;//索引表示顶点,值表示顶点所在的分组,即所在的树
    // 存储图中的所有边,使用最小优先队列,可对边按照权重排序(边的排序规则之前设定的是weigh),每次删除并返回的权重最小的边
    private MinPriorityQueue<Edge> pq;

    //构造方法
    public Kruskal(WeigthGraph weigthGraph) {
        this.edges = new Queue<Edge>();
        this.uf = new UF(weigthGraph.getV());
        this.pq = new MinPriorityQueue<>(weigthGraph.getE());
        for (Edge e : weigthGraph.getEdges()) {
            pq.insert(e);//将边放入到最小优先队列中
        }

        //当pq不为空,且最小生成树中的边的数量小于图中顶点数-1时循环
        //当最小生成树中的边数等于顶点数-1时,所有顶点都已经在树中了,其实最小生成树就已经完成了
        while ((!pq.isEmpty()) && (edges.size() < weigthGraph.getV() - 1)) {
//            每次删除并返回一条权重最小的边,获取两个顶点,如果两个顶点在一个树中,继续下一次循环
//            如果没有在一个树中,将两者放入同一个树中
//            最后将这条边放入最小生成树中
            Edge e = pq.delmin();
            int v = e.either();
            int w = e.other(v);
            if (uf.connected(v, w)) {
                continue;
            }else {
                uf.union(v, w);
                edges.enqueue(e);
            }
        }
    }

    //获取最小生成树的所有边
    public Queue<Edge> getAllEdges() {
        return edges;
    }
}

//测试
public class MyJava {

    public static void main(String[] args) throws IOException {
        //读取road.txt文件,第一行表示城市数,第二行表示几个相通的城市,第三行表示哪些城市相连
        BufferedReader br=new BufferedReader(new InputStreamReader(MyJava.class.getClassLoader().getResourceAsStream("road.txt")));
        int citynumber=Integer.parseInt(br.readLine());
        //创建图
        WeigthGraph weigthGraphgraph=new WeigthGraph(citynumber);
        //添加边,先判断要循环多少次,每次把哪些顶点连接起来
        int roadmumber=Integer.parseInt(br.readLine());
        for (int i = 0; i <roadmumber ; i++) {
            String road=br.readLine();
            String[] str=road.split(" ");//分割后,表示为两个顶点
            int v=Integer.parseInt(str[0]);
            int w=Integer.parseInt(str[1]);
            double weigth=Double.parseDouble(str[2]);
            weigthGraphgraph.addEdge(new Edge(v,w,weigth));
        }

        Kruskal Kruskal=new Kruskal(weigthGraphgraph);
        Queue<Edge> edges =Kruskal.getAllEdges();

        System.out.println("最小生成树(左顶点+右顶点+长度)");
        for (Edge x:edges){
            System.out.print(x.either()+"--"+x.other(x.either())+"--"+x.getWeigth()+"  |  ");
        }
    }
}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值