0

0

深入理解与实现:Java中BFS算法计算最短路径的正确姿势

花韻仙語

花韻仙語

发布时间:2025-11-07 16:54:12

|

905人浏览过

|

来源于php中文网

原创

深入理解与实现:java中bfs算法计算最短路径的正确姿势

本文旨在详细阐述如何使用广度优先搜索(BFS)算法在Java中正确计算非加权图的最短路径。我们将分析常见实现中的陷阱,特别是路径重建逻辑的错误,并提供一套健壮的解决方案,包括使用父节点映射进行路径追踪、优化队列选择以及正确实现equals()和hashCode()方法,以确保算法的准确性和效率。

广度优先搜索(BFS)与最短路径

广度优先搜索(BFS)是一种图遍历算法,它从起始节点开始,逐层地访问所有相邻节点。由于其“层级”遍历的特性,BFS非常适合用于在非加权图中查找从源节点到目标节点的最短路径(即,经过最少边数的路径)。然而,一个常见的实现错误可能导致路径重建失败,无法得到真正的最短路径。

常见陷阱:不正确的路径重建

在BFS中,为了重建路径,通常需要记录每个节点是如何被发现的。一种直观但容易出错的方法是使用一个映射,例如Map<Node, Node> nextNodeMap,来存储当前节点 -> 下一个节点的关系。然而,这种方法存在一个核心问题:

考虑以下图结构:A -> B和A -> C。如果先探索到B,nextNodeMap中会记录A -> B。如果之后从A又探索到C,并且在某个循环中再次处理A的邻居时,如果C被处理,A -> C可能会覆盖掉A -> B。更常见的情况是,当一个节点有多个邻居时,例如节点4连接到5和6,如果nextNodeMap.put(currentNode, nextNode)被用于记录currentNode到nextNode的映射,那么4 -> 5可能会被4 -> 6覆盖,反之亦然,取决于遍历顺序。这会导致在路径重建时,从一个父节点只能追溯到它“最后”被记录的子节点,而非“第一个”被发现的子节点,从而无法保证最短路径。

立即学习Java免费学习笔记(深入)”;

例如,原始代码中的路径重建逻辑:

// ...
nextNodeMap.put(currentNode, nextNode); // 问题所在:可能覆盖
// ...
for (Node node = sourceNode; node != null; node = nextNodeMap.get(node)) {
    directions.add(node);
}

这种方式在currentNode有多个siblingNodes时,nextNodeMap.put(currentNode, nextNode)会不断更新currentNode对应的nextNode,最终只保留了currentNode的最后一个被访问的邻居。这导致路径重建时无法正确回溯到导致目标节点被发现的路径。

解决方案:基于父节点映射的路径追踪

为了正确地重建最短路径,我们应该记录每个节点是“通过哪个父节点”被发现的。这可以通过一个Map<Node, Node> paths来实现,其中键是子节点,值是其父节点。当一个节点sibling首次被发现时,我们将其父节点current记录下来:paths.put(sibling, current)。由于BFS的特性,第一个发现sibling的current节点必然是到达sibling的最短路径上的前一个节点。

此外,这个paths映射也可以兼作已访问节点的集合,因为如果一个节点存在于paths的键中,就意味着它已经被访问过。这消除了单独维护一个visitedNodes集合的必要性。

听脑AI
听脑AI

听脑AI语音,一款专注于音视频内容的工作学习助手,为用户提供便捷的音视频内容记录、整理与分析功能。

下载

优化队列选择

在Java中,ArrayDeque作为队列实现通常比LinkedList更高效,因为它在内部使用数组,避免了链表操作的额外开销,尤其是在大规模数据操作时性能优势更明显。

修正 Node 类的 equals() 和 hashCode() 方法

在使用基于哈希的集合(如HashSet或HashMap)存储自定义对象时,正确地重写equals()和hashCode()方法至关重要。如果equals()方法被重写而hashCode()没有,或者两者实现不一致,将导致对象在集合中行为异常,例如无法正确查找或存储。

原始Node类的equals(Node compareTo)方法并没有正确覆盖java.lang.Object.equals(Object o),因为它接受的参数类型是Node而不是Object。此外,hashCode()方法也缺失。这可能导致在HashSet或HashMap中,即使两个Node对象的值相同,它们仍被视为不同的对象。

正确的Node类实现应如下:

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class Node {
    private final int value;
    private final Set<Node> siblingNodes = new HashSet<>();

    public Node(int value) {
        this.value = value;
    }

    public Set<Node> getSiblingNodes() {
        return siblingNodes;
    }

    public int getValue() {
        return value;
    }

    public void addSiblingNode(Node node) {
        siblingNodes.add(node);
    }

    @Override
    public boolean equals(Object o) {
        // 检查对象是否为自身
        if (this == o) return true;
        // 检查对象是否为null或类型不匹配
        if (o == null || getClass() != o.getClass()) return false;
        // 类型转换并比较关键字段
        Node other = (Node) o;
        return value == other.value;
    }

    @Override
    public int hashCode() {
        // 使用Objects.hash()生成哈希码,确保与equals方法一致
        return Objects.hash(value);
    }
}

完整的BFS最短路径实现

下面是基于上述改进的BFS最短路径算法实现:

import java.util.*;
import java.util.stream.Collectors;

public class BFSShortestPath {

    /**
     * 使用BFS算法查找从源节点到目标节点的最短路径。
     *
     * @param source      起始节点。
     * @param destination 目标节点。
     * @return 包含最短路径节点的列表,如果不存在路径则返回空列表。
     */
    public static List<Node> getDirections(Node source, Node destination) {
        // paths 映射:键是子节点,值是父节点。它也用于追踪已访问节点。
        Map<Node, Node> paths = new HashMap<>();
        // 使用 ArrayDeque 作为队列,性能优于 LinkedList。
        Queue<Node> queue = new ArrayDeque<>();

        // 初始化:将源节点加入队列,并记录其父节点为null(路径的起点)。
        queue.add(source);
        paths.put(source, null);

        boolean isFound = false; // 标记是否找到目标节点

        // BFS 搜索循环
        while (!isFound && !queue.isEmpty()) {
            Node current = queue.remove(); // 出队当前节点

            // 遍历当前节点的所有邻居
            for (Node sibling : current.getSiblingNodes()) {
                // 如果邻居节点未被访问过(即不在 paths 映射中)
                if (!paths.containsKey(sibling)) {
                    // 记录邻居节点的父节点为当前节点
                    paths.put(sibling, current);
                    // 如果邻居节点就是目标节点,则找到路径
                    if (sibling.equals(destination)) {
                        isFound = true; // 设置标记,终止搜索
                        break;
                    }
                    queue.add(sibling); // 将邻居节点加入队列
                }
            }
        }
        // 调用辅助方法重建路径
        return getPath(source, destination, paths);
    }

    /**
     * 辅助方法:根据父节点映射重建路径。
     *
     * @param start 起始节点。
     * @param end   目标节点。
     * @param paths 父节点映射 (child -> parent)。
     * @return 从起始到目标节点的路径列表,如果不存在路径则返回空列表。
     */
    public static List<Node> getPath(Node start, Node end, Map<Node, Node> paths) {
        List<Node> path = new ArrayList<>();
        Node current = end;

        // 从目标节点开始,通过父节点映射回溯到起始节点
        // 如果 paths 不包含 end 节点,说明 end 不可达
        if (!paths.containsKey(end)) {
            return Collections.emptyList();
        }

        while (current != null) {
            path.add(current);
            // 如果回溯到起始节点,或者路径断裂 (current 无法找到父节点)
            if (current.equals(start)) {
                break;
            }
            current = paths.get(current);
        }

        // 如果回溯未能到达起始节点,说明没有完整路径
        if (current == null || !current.equals(start)) {
            return Collections.emptyList();
        }

        Collections.reverse(path); // 反转列表,使其从起始节点到目标节点
        return path;
    }

    public static void main(String[] args) {
        // 构造示例图
        //        1             -> 5
        // 0 ->           -> 4
        //        2 -> 3        -> 6    -> 7

        Node node0 = new Node(0);
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        node0.addSiblingNode(node1);
        node0.addSiblingNode(node2);

        Node node3 = new Node(3);
        node2.addSiblingNode(node3);

        Node node4 = new Node(4);
        node3.addSiblingNode(node4);

        Node node5 = new Node(5);
        Node node6 = new Node(6);
        node4.addSiblingNode(node5);
        node4.addSiblingNode(node6);

        Node node7 = new Node(7);
        node6.addSiblingNode(node7);

        // 查找从节点0到节点7的最短路径
        List<Node> shortestPath = getDirections(node0, node7);

        // 打印路径
        if (!shortestPath.isEmpty()) {
            String path = shortestPath.stream()
                .map(Node::getValue)
                .map(String::valueOf)
                .collect(Collectors.joining(" -> "));
            System.out.println("最短路径: " + path);
        } else {
            System.out.println("未找到从节点0到节点7的路径。");
        }

        // 查找从节点0到节点5的路径
        List<Node> pathTo5 = getDirections(node0, node5);
        if (!pathTo5.isEmpty()) {
            String path = pathTo5.stream()
                .map(Node::getValue)
                .map(String::valueOf)
                .collect(Collectors.joining(" -> "));
            System.out.println("最短路径 (0 -> 5): " + path);
        } else {
            System.out.println("未找到从节点0到节点5的路径。");
        }
    }
}

输出结果:

最短路径: 0 -> 2 -> 3 -> 4 -> 6 -> 7
最短路径 (0 -> 5): 0 -> 2 -> 3 -> 4 -> 5

总结

正确实现BFS算法以计算非加权图的最短路径,关键在于以下几点:

  1. 路径重建策略: 使用Map<Node, Node>来存储子节点 -> 父节点的映射关系。这确保了每个节点只记录其第一次被发现时的父节点,从而保证了路径的最短性。
  2. 避免冗余: 这个父节点映射本身可以兼作已访问节点的记录,无需单独的visitedNodes集合。
  3. 性能优化: 在Java中,优先使用ArrayDeque作为队列实现,而非LinkedList,以获得更好的性能。
  4. 对象契约: 对于自定义类,如果要在哈希集合或映射中使用,务必正确覆盖equals(Object o)和hashCode()方法,并确保它们之间的一致性,以避免潜在的运行时错误和逻辑问题。

遵循这些原则,可以构建一个健壮且高效的BFS最短路径查找算法。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

77

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

40

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

67

2025.11.17

java判断map相关教程
java判断map相关教程

本专题整合了java判断map相关教程,阅读专题下面的文章了解更多详细内容。

47

2025.11.27

页面置换算法
页面置换算法

页面置换算法是操作系统中用来决定在内存中哪些页面应该被换出以便为新的页面提供空间的算法。本专题为大家提供页面置换算法的相关文章,大家可以免费体验。

500

2023.08.14

PHP 高并发与性能优化
PHP 高并发与性能优化

本专题聚焦 PHP 在高并发场景下的性能优化与系统调优,内容涵盖 Nginx 与 PHP-FPM 优化、Opcode 缓存、Redis/Memcached 应用、异步任务队列、数据库优化、代码性能分析与瓶颈排查。通过实战案例(如高并发接口优化、缓存系统设计、秒杀活动实现),帮助学习者掌握 构建高性能PHP后端系统的核心能力。

114

2025.10.16

PHP 数据库操作与性能优化
PHP 数据库操作与性能优化

本专题聚焦于PHP在数据库开发中的核心应用,详细讲解PDO与MySQLi的使用方法、预处理语句、事务控制与安全防注入策略。同时深入分析SQL查询优化、索引设计、慢查询排查等性能提升手段。通过实战案例帮助开发者构建高效、安全、可扩展的PHP数据库应用系统。

99

2025.11.13

JavaScript 性能优化与前端调优
JavaScript 性能优化与前端调优

本专题系统讲解 JavaScript 性能优化的核心技术,涵盖页面加载优化、异步编程、内存管理、事件代理、代码分割、懒加载、浏览器缓存机制等。通过多个实际项目示例,帮助开发者掌握 如何通过前端调优提升网站性能,减少加载时间,提高用户体验与页面响应速度。

36

2025.12.30

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.3万人学习

Java 教程
Java 教程

共578课时 | 82.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号