认识数据结构之——图 构建图与应用

图算法雷达扫描可视化实现

一、Edge类(边模型)

用于存储图中"顶点-目标顶点"的关联关系,包含目标顶点名称和两点间距离
java 复制代码
import java.util.*;

/**
 * 边模型类:存储图中边的目标顶点名称与两点间距离
 * 无向图中,一条边会对应两个顶点的双向关联(如A到B的边,同时存在于A的边列表和B的边列表)
 */
class Edge {
    // 目标顶点的名称(如"X""A""B")
    String name;
    // 当前顶点到目标顶点的距离(整数型,简化距离计算结果)
    int dis;

    /**
     * 构造方法:初始化边的目标顶点和距离
     * @param name 目标顶点名称
     * @param dis 两点间距离
     */
    public Edge(String name, int dis) {
        this.dis = dis; // 赋值距离
        this.name = name; // 赋值目标顶点名称
    }
}

关键注意点

  1. Edge类是图的"边"数据载体,仅存储单方向的关联关系(如从A指向B),无向图的双向性需在Graph类中通过逻辑实现;
  2. 距离用int类型而非double,是为了简化后续排序和展示,避免小数带来的冗余计算。

二、MPoint类(顶点坐标模型)

记录图中顶点的名称与屏幕坐标,为距离计算(勾股定理)提供基础坐标数据

java 复制代码
/**
 * 顶点坐标模型类:关联顶点名称与屏幕像素坐标
 * 用于存储所有顶点的位置信息,支持鼠标交互时计算新顶点与历史顶点的距离
 */
class MPoint {
    // 顶点的唯一标识名称(如初始顶点"X",后续添加的"A""B"等)
    String name;
    // 顶点在Swing窗口中的X轴像素坐标
    int x;
    // 顶点在Swing窗口中的Y轴像素坐标
    int y;

    /**
     * 构造方法:初始化顶点名称与坐标
     * @param name 顶点名称
     * @param x X轴坐标
     * @param y Y轴坐标
     */
    public MPoint(String name, int x, int y) {
        this.name = name; // 赋值顶点名称
        this.x = x; // 赋值X轴坐标
        this.y = y; // 赋值Y轴坐标
    }
}

关键注意点

  1. MPoint的核心作用是"坐标存档",每次鼠标点击添加新目标时,会生成一个MPoint对象存入列表,后续计算距离时直接从列表中获取历史顶点坐标;
  2. 坐标与Swing窗口的像素对应(如(200,700)是初始顶点"X"的固定位置),确保距离计算结果与可视化位置一致。

三、Graph类(图核心逻辑类)

实现无向图的构建、边添加、距离排序与信息展示,是整个图算法的核心

java 复制代码
/**
 * 图核心逻辑类:管理顶点与边的关系,实现无向图的构建、排序与信息展示
 * 核心数据结构为HashMap,键是顶点名称,值是该顶点关联的所有边列表
 */
public class Graph {
    // 无向图的存储结构:key=顶点名称,value=该顶点的所有边(Edge列表)
    Map<String, List<Edge>> graph = new HashMap<>();

    /**
     * 向图中添加顶点(若顶点已存在则不重复添加)
     * @param name 要添加的顶点名称
     */
    public void add(String name) {
        // 1. 先判断顶点是否已存在(避免重复创建空边列表)
        if (!graph.containsKey(name)) {
            // 2. 为新顶点初始化空的边列表(后续通过scan方法添加边)
            List<Edge> edgeList = new ArrayList<>();
            // 3. 将顶点与边列表存入HashMap
            graph.put(name, edgeList);
        }
    }

    /**
     * 构建无向边:为两个顶点添加双向关联的边(体现无向图特性)
     * @param name 起点顶点名称
     * @param to 终点顶点名称
     * @param dis 两点间距离
     */
    public void scan(String name, String to, int dis) {
        // 1. 确保起点和终点都已存在于图中(若不存在则调用add方法创建)
        if (!graph.containsKey(name)) {
            add(name);
        }
        if (!graph.containsKey(to)) {
            add(to);
        }

        // 2. 为起点添加"指向终点"的边
        // 2.1 获取起点的边列表
        List<Edge> edgeList = graph.get(name);
        // 2.2 创建起点到终点的边对象
        Edge edge = new Edge(to, dis);
        // 2.3 将边添加到起点的边列表
        edgeList.add(edge);

        // 3. 为终点添加"指向起点"的边(无向图的双向性关键步骤)
        // 3.1 获取终点的边列表
        List<Edge> edgeList1 = graph.get(to);
        // 3.2 创建终点到起点的边对象(距离与起点到终点一致)
        Edge edge1 = new Edge(name, dis);
        // 3.3 将边添加到终点的边列表
        edgeList1.add(edge1);
    }

    /**
     * 展示图中所有顶点的关联关系:按距离升序排序后打印
     * 便于直观查看每个顶点到其他目标的距离,支持"优先攻击最近目标"的业务需求
     */
    public void showInfo() {
        // 1. 获取所有顶点的名称集合(通过HashMap的keySet方法)
        Set<String> keySet = graph.keySet();
        // 2. 打印标题,区分每次扫描结果
        System.out.println("=== 当前雷达扫描图 ===");
        
        // 3. 遍历每个顶点,处理并展示其边列表
        for (String name : keySet) {
            // 3.1 获取当前顶点的边列表
            List<Edge> edgeList = graph.get(name);
            // 3.2 打印当前顶点名称
            System.out.println("顶点:" + name);

            // 3.3 冒泡排序:将边列表按距离升序排序(最近目标在前)
            // 外层循环:控制排序轮次(每轮确定一个最大距离的边到末尾)
            for (int i = 0; i < edgeList.size(); i++) {
                // 内层循环:比较相邻边的距离,交换顺序(大距离后移)
                for (int j = 0; j < edgeList.size() - 1; j++) {
                    Edge e1 = edgeList.get(j); // 当前边
                    Edge e2 = edgeList.get(j + 1); // 下一条边
                    // 若当前边距离 > 下一条边距离,交换两者位置
                    if (e1.dis > e2.dis) {
                        edgeList.set(j, e2);
                        edgeList.set(j + 1, e1);
                    }
                }
            }

            // 3.4 打印排序后的目标与距离
            for (int i = 0; i < edgeList.size(); i++) {
                Edge edge = edgeList.get(i);
                System.out.println("目标:" + edge.name + "- 距离:" + edge.dis);
            }
            // 3.5 打印分隔线,区分不同顶点的信息
            System.out.println("-----");
        }
        System.out.println("------");
    }
}

关键注意点

  1. add方法的防御性判断:避免重复添加同一顶点导致的边列表覆盖,确保每个顶点只对应一个边列表;
  2. scan方法的双向性:每次调用会同时为两个顶点添加边,是无向图"双向可达"的核心逻辑,若遗漏则会变成有向图;
  3. 冒泡排序的作用:将每个顶点的边列表按距离升序排列,直接为"优先攻击最近Alien"提供数据支持,无需额外处理排序逻辑;
  4. 信息展示格式:通过"顶点→目标-距离"的结构打印,清晰区分每个顶点的关联关系,便于调试和业务使用。

四、RobotListener类(鼠标交互与绘制逻辑)

处理鼠标点击事件,实现新目标的绘制、距离计算与图数据更新,是"可视化"与"图算法"的桥梁

java 复制代码
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;

/**
 * 鼠标监听器类:处理鼠标交互(点击添加目标)、图形绘制与图数据同步
 * 关联Graph类和MPoint类,实现"点击→绘制→计算距离→更新图"的完整流程
 */
public class RobotListener implements MouseListener {
    // 画笔对象:用于在Swing窗口中绘制目标点(圆形)和初始顶点(矩形)
    Graphics g;
    // 初始顶点"X"的固定坐标(Swing窗口中的位置)
    final int sx = 200, sy = 700;
    // 新添加目标的名称标识(从"A"开始自动递增,如"A""B""C")
    char name = 'A';
    // 关联的图实例:用于存储新目标与历史顶点的距离关系
    Graph scanGraph = new Graph();
    // 顶点坐标列表:存储所有顶点的MPoint对象,用于批量计算新目标与历史顶点的距离
    ArrayList<MPoint> points = new ArrayList<>();

    /**
     * 构造方法:初始化初始顶点"X"
     * 程序启动时自动创建"X"顶点,作为雷达扫描的起点
     */
    public RobotListener() {
        scanGraph.add("X"); // 向图中添加初始顶点"X"
        // 向顶点列表添加"X"的坐标(与sx、sy一致)
        points.add(new MPoint("X", 200, 700));
    }

    // 鼠标点击事件(本项目未使用,留空)
    @Override
    public void mouseClicked(MouseEvent e) {}

    /**
     * 鼠标按下事件:核心交互逻辑
     * 鼠标按下时触发:绘制新目标、计算距离、更新图数据、展示扫描结果
     */
    @Override
    public void mousePressed(MouseEvent e) {
        System.out.println("按下"); // 调试用:打印鼠标按下状态
        // 1. 获取鼠标按下的坐标(即新目标的坐标)
        int x = e.getX(); // 鼠标按下位置的X轴坐标
        int y = e.getY(); // 鼠标按下位置的Y轴坐标

        // 2. 绘制新目标:红色圆形(直径50像素,视觉上区分初始顶点)
        g.setColor(Color.RED); // 设置画笔颜色为红色
        // 绘制圆形:参数依次为(X轴坐标、Y轴坐标、宽度、高度),宽高一致即为圆形
        g.fillOval(x, y, 50, 50);

        // 3. 计算新目标与所有历史顶点的距离,并更新图数据
        // 遍历顶点列表(包含"X"及已添加的目标)
        for (int i = 0; i < points.size(); i++) {
            MPoint p = points.get(i); // 获取历史顶点的MPoint对象
            // 3.1 勾股定理计算距离:sqrt((新X-历史X)² + (新Y-历史Y)²),转为int简化
            int dis = (int) Math.sqrt((x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
            // 3.2 调用scan方法:为新目标与历史顶点添加双向边
            scanGraph.scan(name + "", p.name, dis);
        }

        // 4. 将新目标添加到顶点列表(供后续新目标计算距离使用)
        MPoint mPoint = new MPoint(name + "", x, y); // 创建新目标的MPoint对象
        points.add(mPoint); // 加入列表

        // 5. 更新目标名称标识(下一次点击将使用下一个字母,如"A"→"B")
        name++;

        // 6. 展示当前图的所有扫描信息(按距离排序后的结果)
        scanGraph.showInfo();
    }

    // 鼠标释放事件(本项目未使用,留空)
    @Override
    public void mouseReleased(MouseEvent e) {}

    // 鼠标进入窗口事件(本项目未使用,留空)
    @Override
    public void mouseEntered(MouseEvent e) {}

    // 鼠标离开窗口事件(本项目未使用,留空)
    @Override
    public void mouseExited(MouseEvent e) {}
}

关键注意点

  1. 初始顶点"X"的初始化:在构造方法中完成,确保程序启动时就有扫描起点,避免后续计算空指针;
  2. 鼠标坐标与绘制的关系:e.getX()e.getY()获取的是鼠标在Swing窗口中的相对坐标,直接用于绘制圆形,确保视觉位置与坐标计算一致;
  3. 批量距离计算:通过遍历points列表,一次性计算新目标与所有历史顶点的距离,避免遗漏关联关系;
  4. 目标名称自动递增:char name='A'通过name++实现自动命名,确保每个新目标的名称唯一,避免顶点名称冲突。

五、GraphRobot类(窗口容器类)

创建Swing窗口,初始化界面布局与画笔,绑定鼠标监听器,是可视化的载体

java 复制代码
import javax.swing.*;
import java.awt.*;

/**
 * 窗口容器类:创建Swing窗口,初始化界面配置,关联鼠标监听器
 * 是整个程序的入口,负责将图算法与可视化界面结合
 */
public class GraphRobot {

    /**
     * 初始化并展示Swing窗口
     * 配置窗口标题、大小、关闭策略、布局,绑定监听器并绘制初始顶点
     */
    public void showUI() {
        // 1. 创建Swing窗口对象(JFrame)
        JFrame jf = new JFrame();
        // 2. 设置窗口标题(显示在窗口顶部)
        jf.setTitle("图算法应用");
        // 3. 设置窗口大小(宽度800像素,高度1000像素)
        jf.setSize(800, 1000);
        // 4. 设置窗口关闭策略:关闭窗口时退出整个程序
        jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        // 5. 设置窗口居中显示(打开时位于屏幕正中央)
        jf.setLocationRelativeTo(null);
        // 6. 设置窗口布局:流式布局(组件按添加顺序排列)
        FlowLayout flow = new FlowLayout();
        jf.setLayout(flow);

        // 7. 显示窗口(必须在配置完成后调用,否则界面不显示)
        jf.setVisible(true);
        // 8. 获取窗口的画笔对象(用于后续绘制图形)
        Graphics g = jf.getGraphics();

        // 9. 创建鼠标监听器对象,并绑定画笔
        RobotListener rl = new RobotListener();
        rl.g = g; // 将窗口画笔传递给监听器,确保监听器能绘制图形
        // 10. 为窗口绑定鼠标监听器(监听鼠标按下等事件)
        jf.addMouseListener(rl);

        // 11. 延迟500毫秒后绘制初始顶点"X"(矩形)
        try {
            Thread.sleep(500); // 延迟500ms:确保窗口完全加载后再绘制,避免图形不显示
        } catch (InterruptedException e) {
            // 捕获中断异常,转为运行时异常抛出(简化异常处理)
            throw new RuntimeException(e);
        }
        // 绘制初始顶点"X":黑色矩形(坐标200,700,宽高100像素)
        g.setColor(Color.BLACK); // 设置画笔颜色为黑色
        g.fillRect(200, 700, 100, 100); // 绘制矩形:(X,Y,宽,高)
    }

    /**
     * 程序入口(main方法)
     * 启动程序,调用showUI方法展示窗口
     */
    public static void main(String[] args) {
        // 创建GraphRobot对象
        GraphRobot ger = new GraphRobot();
        // 调用showUI方法初始化并显示窗口
        ger.showUI();
    }
}

关键注意点

  1. 窗口显示顺序:jf.setVisible(true)必须在窗口配置(大小、标题等)之后调用,否则配置不生效;
  2. 画笔获取时机:jf.getGraphics()必须在jf.setVisible(true)之后调用,否则无法获取有效画笔,导致绘制失败;
  3. 延迟绘制初始顶点:Thread.sleep(500)是为了等待窗口完全加载,避免因窗口未初始化完成导致初始矩形不显示;
  4. 关闭策略设置:WindowConstants.EXIT_ON_CLOSE确保关闭窗口时程序完全退出,避免后台进程残留;
  5. 布局选择:流式布局(FlowLayout)简化了界面管理,适合本项目的简单可视化需求,若需复杂布局可替换为其他布局(如BorderLayout)。
相关推荐
陌上丨2 小时前
Redis常用的数据类型有哪些?Zset的底层数据结构是什么?
数据结构·数据库·redis
FMRbpm2 小时前
邻接矩阵练习1--------LCP 07.传递信息
数据结构·c++·算法·leetcode·深度优先·新手入门
啊阿狸不会拉杆2 小时前
《数字信号处理》第 1 章 离散时间信号与系统
人工智能·算法·机器学习·信号处理·数字信号处理·dsp
ʚB҉L҉A҉C҉K҉.҉基҉德҉^҉大2 小时前
C++安全编程指南
开发语言·c++·算法
tianyuanwo2 小时前
Python RPM打包的基石:深入理解 python3.x-rpm-macros 组件
开发语言·python·xx-rpm-macros
啊阿狸不会拉杆2 小时前
《数字信号处理》第 2 章 - z 变换与离散时间傅里叶变换(DTFT)
人工智能·算法·机器学习·信号处理·数字信号处理·dsp
小北方城市网2 小时前
Spring Boot Actuator+Prometheus+Grafana 生产级监控体系搭建
java·spring boot·python·rabbitmq·java-rabbitmq·grafana·prometheus
老鼠只爱大米2 小时前
LeetCode经典算法面试题 #146:LRU 缓存(双向链表、线程安全等多种实现方案详细解析)
算法·leetcode·lru·lru缓存·双向链表
胖墩会武术2 小时前
【PyTorch项目实战】FastSAM(快速分割一切)
人工智能·pytorch·python