0

0

Java双向搜索路径算法实现与常见错误解析

霞舞

霞舞

发布时间:2025-10-10 13:47:44

|

513人浏览过

|

来源于php中文网

原创

Java双向搜索路径算法实现与常见错误解析

本文深入探讨了Java中双向路径搜索算法的实现,旨在通过两个方向同时搜索来提高效率。我们将分析一个常见的实现错误,即使用单个父子映射来记录双向路径,并提供一个修正后的策略,包括使用独立的父节点映射和正确的路径重构方法,以确保算法的正确性和完整性。

双向搜索算法概述

双向搜索(bidirectional search)是一种图搜索算法,旨在寻找两个节点(起点和终点)之间的最短路径。与传统的单向搜索(如bfs或dfs)不同,双向搜索同时从起点和终点开始进行搜索,当两个搜索前沿相遇时,算法终止。这种方法通常可以显著减少需要探索的节点数量,因为搜索空间以两个方向的“半球”形式扩展,其交点通常比单个“全球”的面积小得多,从而提高搜索效率。

其核心思想是:

  1. 从起点S开始一个前向搜索(Forward Search)。
  2. 从终点T开始一个后向搜索(Backward Search)。
  3. 当两个搜索过程在某个节点M相遇时,即M同时被前向搜索和后向搜索访问到,则找到了一条从S到T的路径。这条路径由S到M的前向路径段和M到T的后向路径段(反向)组成。

初始实现的问题分析

在提供的Java实现中,尝试使用双向搜索来构建路径。代码片段如下:

public BidirectionalSearch buildSearchTree(Vertex start, Vertex end) {
    // ... 参数校验 ...

    searchTreeParentByChild.clear(); // 单个Map

    Queue<Vertex> unvisitedVertexQueueFromStart = new ArrayDeque<>();
    Queue<Vertex> unvisitedVertexQueueFromEnd = new ArrayDeque<>();

    unvisitedVertexQueueFromStart.add(start);
    unvisitedVertexQueueFromEnd.add(end);

    searchTreeParentByChild.put(start, null); // 前向搜索的起点
    while (!unvisitedVertexQueueFromStart.isEmpty() && !unvisitedVertexQueueFromEnd.isEmpty()) {
        // 前向搜索部分
        var curStart = unvisitedVertexQueueFromStart.poll();
        for (var e : curStart.edges()) {
            if (!searchTreeParentByChild.containsKey(e.to())) {
                searchTreeParentByChild.put(e.to(), curStart); // 记录前向路径:e.to()的父节点是curStart
                unvisitedVertexQueueFromStart.add(e.to());
            }
        }
        var intersection = curStart.edges().stream()
                .map(Edge::to)
                .filter(unvisitedVertexQueueFromEnd::contains) // 检查交集
                .findAny();
        if (intersection.isPresent())
            return this;

        // 后向搜索部分
        var curEnd = unvisitedVertexQueueFromEnd.poll();
        for (var e : curEnd.edges()) {
            if (!searchTreeParentByChild.containsValue(e.to())) { // 错误:这里应该是containsKey
                searchTreeParentByChild.put(curEnd, e.to()); // 错误:记录后向路径的方式
                unvisitedVertexQueueFromEnd.add(e.to());
            }
        }
        intersection = curEnd.edges().stream()
                .map(Edge::to)
                .filter(unvisitedVertexQueueFromStart::contains) // 检查交集
                .findAny();
        if (intersection.isPresent())
            return this;
    }
    return this;
}

该实现存在两个主要问题:

  1. 单一父子映射的滥用: searchTreeParentByChild 被用于同时记录前向搜索和后向搜索的父子关系。

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

    • 前向搜索 searchTreeParentByChild.put(e.to(), curStart); 记录的是 子节点 -> 父节点 的关系(从start到e.to())。
    • 后向搜索 searchTreeParentByChild.put(curEnd, e.to()); 记录的是 curEnd -> e.to()。如果e.to()是curEnd的邻居,且我们希望构建从end到curEnd的路径,那么e.to()应该是curEnd的父节点。但这里的键值对是curEnd作为键,e.to()作为值,这实际上是在说curEnd是e.to()的父节点,与前向搜索的语义相同,但方向相反。更严重的是,它会覆盖或混淆前向搜索已经记录的父子关系,导致路径无法正确重构。
    • searchTreeParentByChild.containsValue(e.to()) 这里的检查也是错误的,应该检查containsKey(e.to())来判断e.to()是否已经被后向搜索访问过。
  2. 路径重构缺失: 即使找到了交点,当前代码也只是简单地 return this;,并没有提供任何机制来从 searchTreeParentByChild 中提取完整的路径。由于映射被混用,即便尝试提取,也只会得到不完整的或错误的结果。

    Imagine By Magic Studio
    Imagine By Magic Studio

    AI图片生成器,用文字制作图片

    下载

修正后的双向搜索实现策略

为了正确实现双向搜索并重构完整路径,我们需要:

  1. 使用两个独立的父节点映射:

    • Map<Vertex, Vertex> parentFromStart; 用于记录从start出发的前向搜索路径中,每个节点的前一个节点。
    • Map<Vertex, Vertex> parentFromEnd; 用于记录从end出发的后向搜索路径中,每个节点的前一个节点。
  2. 正确的交集判断: 在任一搜索步骤中,如果发现当前访问到的节点已经被另一个方向的搜索访问过,则找到了交点。

  3. 路径重构: 一旦找到交点,结合两个父节点映射来构建完整路径。

示例代码:修正后的 buildSearchTree 方法

import java.util.*;

// 假设 Vertex 和 Edge 类已定义如下:
class Vertex {
    String name;
    List<Edge> edges; // 出边列表,对于无向图,通常双向添加
    // 构造函数、equals、hashCode、toString 等

    public Vertex(String name) {
        this.name = name;
        this.edges = new ArrayList<>();
    }

    public List<Edge> edges() {
        return edges;
    }

    public void addEdge(Edge edge) {
        this.edges.add(edge);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Vertex vertex = (Vertex) o;
        return Objects.equals(name, vertex.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public String toString() {
        return name;
    }
}

class Edge {
    Vertex from;
    Vertex to;
    int weight;
    // 构造函数、getter等

    public Edge(Vertex from, Vertex to, int weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }

    public Vertex from() {
        return from;
    }

    public Vertex to() {
        return to;
    }

    public int weight() {
        return weight;
    }
}

// 假设 Graph 类已定义
class Graph {
    Set<Vertex> vertices;
    // 构造函数、添加节点、添加边等

    public Graph() {
        this.vertices = new HashSet<>();
    }

    public void addVertex(Vertex v) {
        vertices.add(v);
    }

    public Set<Vertex> vertices() {
        return vertices;
    }
}

public class BidirectionalSearcher {

    private final Graph graph;

    public BidirectionalSearcher(Graph graph) {
        this.graph = graph;
    }

    /**
     * 执行双向搜索并返回从起点到终点的路径。
     *
     * @param start 路径起点
     * @param end   路径终点
     * @return 包含路径上所有顶点的列表,如果不存在路径则返回空列表。
     * @throws IllegalArgumentException 如果起点或终点不在图中。
     */
    public List<Vertex> findPath(Vertex start, Vertex end) {
        if (!graph.vertices().containsAll(List.of(start, end))) {
            throw new IllegalArgumentException("起点或终点不在图中。");
        }
        if (start.equals(end)) {
            return List.of(start); // 起点和终点相同,路径就是自身
        }

        // 记录前向搜索的父节点:child -> parent
        Map<Vertex, Vertex> parentFromStart = new HashMap<>();
        // 记录后向搜索的父节点:child -> parent
        Map<Vertex, Vertex> parentFromEnd = new HashMap<>();

        // 前向搜索队列
        Queue<Vertex> queueFromStart = new ArrayDeque<>();
        // 后向搜索队列
        Queue<Vertex> queueFromEnd = new ArrayDeque<>();

        // 初始化前向搜索
        queueFromStart.add(start);
        parentFromStart.put(start, null); // 起点没有父节点

        // 初始化后向搜索
        queueFromEnd.add(end);
        parentFromEnd.put(end, null);     // 终点没有父节点

        Vertex meetingPoint = null; // 记录相遇点

        while (!queueFromStart.isEmpty() && !queueFromEnd.isEmpty() && meetingPoint == null) {
            // 执行前向搜索一步
            meetingPoint = searchStep(queueFromStart, parentFromStart, parentFromEnd);
            if (meetingPoint != null) break;

            // 执行后向搜索一步
            meetingPoint = searchStep(queueFromEnd, parentFromEnd, parentFromStart);
            if (meetingPoint != null) break;
        }

        if (meetingPoint == null) {
            return List.of(); // 未找到路径
        }

        // 重构路径
        return reconstructPath(start, end, meetingPoint, parentFromStart, parentFromEnd);
    }

    /**
     * 单步搜索逻辑,用于前向或后向搜索。
     *
     * @param currentQueue 当前搜索方向的队列
     * @param currentParents 当前搜索方向的父节点映射 (child -> parent)
     * @param otherParents 另一个搜索方向的父节点映射 (child -> parent)
     * @return 如果找到交点则返回交点Vertex,否则返回null
     */
    private Vertex searchStep(Queue<Vertex> currentQueue,
                              Map<Vertex, Vertex> currentParents,
                              Map<Vertex, Vertex> otherParents) {
        if (currentQueue.isEmpty()) {
            return null;
        }

        Vertex current = currentQueue.poll();

        // 遍历当前节点的邻居
        for (Edge edge : current.edges()) {
            Vertex neighbor = edge.to(); // 假设边是 from -> to

            // 如果邻居未被当前搜索方向访问过
            if (!currentParents.containsKey(neighbor)) {
                currentParents.put(neighbor, current); // 记录父节点
                currentQueue.add(neighbor);

                // 检查是否与另一个搜索方向相遇
                if (otherParents.containsKey(neighbor)) {
                    return neighbor; // 找到交点
                }
            }
        }
        return null;
    }

    /**
     * 从两个父节点映射和交点重构完整路径。
     *
     * @param start 起点
     * @param end 终点
     * @param meetingPoint 相遇点
     * @param parentFromStart 前向搜索的父节点映射
     * @param parentFromEnd 后向搜索的父节点映射
     * @return 完整的路径列表
     */
    private List<Vertex> reconstructPath(Vertex start, Vertex end, Vertex meetingPoint,
                                         Map<Vertex, Vertex> parentFromStart,
                                         Map<Vertex, Vertex> parentFromEnd) {
        List<Vertex> path = new LinkedList<>(); // 使用 LinkedList 便于在头部

热门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

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

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

497

2023.08.14

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

69

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

37

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

82

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

97

2026.03.06

热门下载

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

精品课程

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

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.2万人学习

Java 教程
Java 教程

共578课时 | 81.3万人学习

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

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