一、效果展示
1. 整个流程
这里只展示了整个流程中的部分功能,对程序感兴趣的可下载源码进行体验😋
2. Prim算法
3. Kruskal算法
二、程序的工程结构
下图是程序的工程结构截图,只展示了部分类。运行环境为IDEA
三、主要代码
- MyPanel 类:继承JPanel类,实现绘图的方法 paintGraph(),paintGraph() 在重写的 paint() 方法中被调用
package com.Key.GUI.AnimUI;
import com.Key.Algorithm.EdgeData;
import com.Key.Algorithm.Graph;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 自定义面板
* - 继承JPanel
* - 顶点集合 vex
* - 权值和顶点个数 weight,vexNum,edgeNum
* - 顶点值 vertex
* - 标记算法是否执行完 isEnd
* - 记录每次执行算法得到的顶点对应下标 adj
*
* @author Key
* @date 2021/06/16/14:49
**/
public class MyPanel extends JPanel {
private final List<String> vex;
private final int vexNum;
private final int edgeNum;
private final EdgeData[] edgeData;
public String pOrK;
public Boolean isEnd = false;
public List<Integer> adj = new ArrayList<>();
public int v1 = -1,v2 = -1;
public Color[] vexColors = new Color[6];
public Color[] edgeColors = new Color[15];
/**
* 构造方法
* - 传入图结构
* @param graph 图结构
*/
public MyPanel(Graph graph) {
super();
this.vex = graph.getVex();
this.vexNum = graph.getVexNum();
this.edgeNum = graph.getEdgeNum();
int[][] edges = graph.getEdges();
// 创建一个边实体类
edgeData = new EdgeData[edgeNum];
int index = 0;
// 获取边信息,存入边实体类中
for (int i = 0;i < vexNum;i++) {
for (int j = i + 1;j < vexNum;j++) {
if (edges[i][j] != 0 && edges[i][j] != Integer.MAX_VALUE) {
edgeData[index++] = new EdgeData(vex.get(i),vex.get(j), edges[i][j]);
}
}
}
Arrays.fill(vexColors, Color.BLACK);
Arrays.fill(edgeColors,Color.BLACK);
}
/**
* 重写paint方法,画图结构
* @param g 画笔
*/
@Override
public void paint(Graphics g) {
// 调用父类的paint,一定要写
super.paint(g);
// 进入画图
paintGraph(g);
}
/**
* 封装画图的具体方法
* @param g 画笔
*/
public void paintGraph(Graphics g) {
// 改变面板中字体样式
g.setFont(g.getFont().deriveFont(20f));
// 参数Map
GraphicalParam graphicalParam = new GraphicalParam();
// 用于判断是否改变画笔颜色
boolean is;
// 画顶点
for (int i = 0;i < vexNum;i++) {
// 获取每个顶点值
String vertex = vex.get(i);
// 获取每个顶点的位置参数
int[] vs = graphicalParam.vMap.get(i).clone();
// 加上P1是因为如果只有一个顶点的情况,则直接把该顶点画笔边红色
if ("K1".equals(pOrK) || "P1".equals(pOrK)) {
// 第一次开始画图时,就显示的文字
g.setColor(Color.BLACK);
// 只有Kruskal算法才展示
if ("K1".equals(pOrK)) {
g.drawString("选入全部顶点",655,95);
}
vexColors[i] = Color.RED;
}
// 使用对应画笔色(因为Kruskal算法一开始就选出全部顶点,故后面就不用再变顶点颜色)
if ("P".equals(pOrK)) {
// 把得到的最小边两个邻接顶点下标与当前顶点下标相比较,相等就把该顶点颜色变红
for (int j = 0;j < adj.size();j += 2) {
is = ((adj.get(j) == i) || (adj.get(j + 1) == i));
if (is) {
vexColors[i] = Color.RED;
break;
}
}
}
g.setColor(vexColors[i]);
// 画顶点
g.drawString(vertex,vs[0],vs[1]);
g.drawOval(vs[2],vs[3],40,40);
}
// 画边
for (int i = 0;i < edgeNum;i++) {
// v1,v2记录每条边的邻接顶点
int v1 = 0,v2 = 0;
// 获取当前边邻接顶点的位置下标
for (int k = 0;k < vex.size();k++) {
if (vex.get(k).equals(edgeData[i].start)) {
v1 = k;
}
if (vex.get(k).equals(edgeData[i].end)) {
v2 = k;
}
}
// 获取每条边的对应的位置参数
String str1 = "(" + v1 + "," + v2 +")";
String str2 = "(" + v2 + "," + v1 +")";
int[] es = (graphicalParam.eMap.get(str1) != null) ? graphicalParam.eMap.get(str1) : graphicalParam.eMap.get(str2);
// 根据每次得到的最小边改变画笔颜色
for (int k = 0;k < adj.size();k += 2) {
is = (adj.get(k) == v1 && adj.get(k + 1) == v2) || (adj.get(k) == v2 && adj.get(k + 1) == v1);
if (is) {
edgeColors[i] = Color.RED;
break;
}
}
g.setColor(edgeColors[i]);
// 画边
g.drawLine(es[0],es[1],es[2],es[3]);
g.drawString(String.valueOf(edgeData[i].weight),es[4],es[5]);
}
// 展示算法名字
g.setColor(Color.BLUE);
if ("P".equals(pOrK) || "P1".equals(pOrK)) {
g.drawString("Prim算法演示",650,60);
}
if ("K".equals(pOrK) || "K1".equals(pOrK)){
g.drawString("Kruskal算法演示",650,60);
}
// 画流程的文字
g.setColor(Color.BLACK);
for (int i = 0,k = 1;i < adj.size();i += 2,k++) {
g.drawString("第" + k + "次得到的最小边为:" + "(" + vex.get(adj.get(i)) + "," +
vex.get(adj.get(i + 1)) +")",590,125 + i * 15);
}
if (isEnd) {
g.drawString("算法执行完毕!",650,125 + adj.size() * 20);
}
}
}
- AlgoAnimFrame类:算法演示窗口的实现
package com.Key.GUI.AnimUI;
import com.Key.Algorithm.Graph;
import com.Key.GUI.StyleChange;
import com.Key.GUI.MyFrame;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
/**
* 放MyPanel算法演示面板的窗口
* - 算法实体类->algo
* - 算法名字->algoName
* - 是否点击暂停->isPause
* - 是否点击开始->isStart
* - Prim算法执行起始顶点下标->firstVex
*
* @author Key
* @date 2021/06/17/13:32
**/
public class AlgoAnimFrame {
private final GraphPainting paintG;
private final String algoName;
private boolean isPause = false;
private boolean isStart = false;
public int firstVex;
public AlgoAnimFrame(Graph graph, String name, long time) {
this.algoName = name;
final MyFrame jf = new MyFrame(name);
// 显示演示方式
String showText;
// 设置窗口的大小和布局
jf.setSize(1000, 700);
jf.setLayout(new GridLayout(2,1));
// +-------------------------------初始化第一个面板myPanel->用于显示图形和演示过程--------------------------------+
// 根据time值选择对应演示方式(手动演示time为-1)->初始化paintG
if (time == -1) {
this.paintG = new GraphPainting(graph,1000);
showText = "手动演示";
}else {
this.paintG = new GraphPainting(graph,time);
showText = "自动步演示";
}
// 创建有图结构的面板
paintG.myPanel = new MyPanel(graph);
// +----------------------------------------------------------------------------+
// +--------------------------------初始化第二个面板panel->用于显示按钮和文字标签----------------------------------+
// 创建第二个面板,用于显示按钮和标签文字
JPanel panel = new JPanel();
// 控制演示的按键
JButton startBtn = new JButton("开始演示");
JButton pauseBtn = new JButton("暂停/继续演示");
JButton nextBtn = new JButton("下一步");
JLabel label = new JLabel("<html><br><br><br>" +
"------------------------------------" +
showText +
"------------------------------------" +
"<html>");
JLabel[] labels = {label};
JButton[] buttons = {pauseBtn,startBtn,nextBtn};
new StyleChange().bestStyle(labels,buttons,null,null,null,150,30);
panel.add(label);
panel.add(startBtn);
// 根据传入的时间值选择对应的按钮组件
if (time == -1) {
panel.add(nextBtn);
}else {
panel.add(pauseBtn);
}
// +----------------------------------------------------------------------------+
// 在窗口中加入两个面板
jf.add(paintG.myPanel);
jf.add(panel);
// 根据算法名字显示对应的文字
if ("Prim算法演示".equals(name)) {
paintG.myPanel.pOrK = "P";
}else {
paintG.myPanel.pOrK = "K";
}
// +--------------------------------------------监听事件-----------------------------------------------+
// 建立两个线程,分别执行两个算法
Thread algo1 = new Thread() {
@Override
public void run() {
// 算法执行前先把原图画出来
paintG.drawGraph(null);
paintG.graphPainting("P",firstVex);
}
};
Thread algo2 = new Thread() {
@Override
public void run() {
// 开始执行前先把原图画出来
paintG.drawGraph(null);
paintG.graphPainting("K",-1);
}
};
/*
每次对子线程进行操作前都要先让主线程休眠一段时间
*/
// 开始演示按键
startBtn.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("开始演示算法");
try {
if ("Prim算法演示".equals(algoName)) {
algo1.start();
}else {
algo2.start();
}
isStart = true;
// 如果是手动演示,则需要开始演示后就暂停演示
if (time == -1) {
Thread.sleep(100);
// 暂停线程
paintG.pauseThread();
}
// 执行完开始按键后把按键置为不可点击
startBtn.setEnabled(false);
} catch(InterruptedException e1) {
e1.printStackTrace();
}
}
});
// 暂定/继续按键
pauseBtn.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("暂停/继续演示");
try {
if (!isPause) {
// 暂停线程
Thread.sleep(100);
paintG.pauseThread();
isPause = true;
}else {
// 恢复线程
Thread.sleep(100);
paintG.resumeThread();
isPause = false;
}
// 演示结束,关闭按键
if (paintG.myPanel.isEnd) {
pauseBtn.setEnabled(false);
}
} catch(InterruptedException e1) {
e1.printStackTrace();
}
}
});
// 点击下一步按键
nextBtn.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("下一步");
try {
// 演示开始后再进行操作
if (isStart) {
// 先恢复进程,过0.1秒后再暂停线程
Thread.sleep(100);
paintG.resumeThread();
Thread.sleep(100);
paintG.pauseThread();
Thread.sleep(100);
}
// 演示结束后关闭按键
if (paintG.myPanel.isEnd) {
nextBtn.setEnabled(false);
}
} catch(InterruptedException e1) {
e1.printStackTrace();
}
}
});
jf.setLocationRelativeTo(null);
jf.setVisible(true);
}
}
- GraphPainting类:实现图形的具体绘制和演示动画效果
package com.Key.GUI.AnimUI;
import com.Key.Algorithm.Algorithm;
import com.Key.Algorithm.EdgeData;
import com.Key.Algorithm.Graph;
import java.util.List;
/**
* 具体画图类
*
* @author Key
* @date 2021/06/24/23:12
**/
public class GraphPainting {
public MyPanel myPanel;
private final long sleepTime;
private final List<String> vex;
private final Graph graph;
private final Algorithm algo;
/**
* 用于控制线程(绘图)暂停或继续
*/
private final Object lock = new Object();
private volatile boolean pause = false;
/**
* 构造器,传入图结构和算法名字
* @param graph 图结构
*/
public GraphPainting(Graph graph, long time) {
this.graph = graph;
this.vex = graph.getVex();
this.sleepTime = time;
// 把图结构传入算法实现类中
this.algo = new Algorithm(graph);
}
/**
* 执行面板中的paint方法,使用repaint()重复画图
* @param vs 执行Prim算法每次得到的边(两个顶点)
*/
public void drawGraph(String[] vs) {
if (vs != null) {
for (int i = 0;i < vex.size();i++) {
if (vs[0].equals(vex.get(i))) {
myPanel.v1 = i;
myPanel.adj.add(i);
}
if (vs[1].equals(vex.get(i))) {
myPanel.v2 = i;
myPanel.adj.add(i);
}
}
}
myPanel.repaint();
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 绘制相应算法的图形
* @param algoName 算法名字(P/K)
* @param firstVex Prim算法起始顶点
*/
public void graphPainting(String algoName, int firstVex) {
String[] content = new String[2];
// 获取最小边数组
EdgeData[] edgeData;
System.out.println("最小生成树为");
// 根据算法名字调用对应算法具体实现方法
if ("P".equals(algoName)) {
edgeData = algo.MST_Prim(graph, firstVex);
// 如果只有一个顶点(也是连通图),则直接把该顶点变红色,然后演示结束
if (graph.getEdgeNum() == 0) {
myPanel.pOrK = "P1";
drawGraph((null));
}
}else {
edgeData = algo.MST_Kruskal(graph);
// 先把全部顶点选入
myPanel.pOrK = "K1";
drawGraph((null));
}
// 获取最小边的个数
int minEdgeNum = algo.index;
// 根据得到的最小边数组绘图
for(int j = 0;j < minEdgeNum;j++) {
content[0] = edgeData[j].start;
content[1] = edgeData[j].end;
// 判断是否暂停线程
if (pause) {
onPause();
}
// 具体绘制图形
drawGraph(content);
}
// 最后再画一次图,把最小生成树显示出来
myPanel.isEnd = true;
myPanel.repaint();
}
/**
* 暂停线程
* - 这个方法只能在run 方法中实现,不然会阻塞主线程,导致页面无响应
*/
public void onPause() {
// 创建一把锁对象lock,调用wait()会释放锁,同时暂停线程;调用notify()函数会唤醒锁,从而重新获取这把锁
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 调用该方法实现线程的暂停
*/
public void pauseThread() {
pause = true;
}
/**
* 调用该方法实现恢复线程的运行
*/
public void resumeThread() {
pause = false;
// 调用notify()函数唤醒锁,恢复线程
synchronized (lock){
lock.notify();
}
}
}