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()+" | ");
}
}
}