一、效果展示

1. 整个流程

这里只展示了整个流程中的部分功能,对程序感兴趣的可下载源码进行体验😋

java区域树状图如何写 java画树_Graph

2. Prim算法

java区域树状图如何写 java画树_java区域树状图如何写_02

3. Kruskal算法

java区域树状图如何写 java画树_java_03


二、程序的工程结构

下图是程序的工程结构截图,只展示了部分类。运行环境为IDEA

java区域树状图如何写 java画树_java_04


三、主要代码

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