图算法路径查找器实战案例
场景描述
通过构建一个交互式网页应用,让用户能在自定义地图上直观地观察和比较BFS、DFS、Dijkstra和A*这四种路径查找算法的工作原理、效率差异以及适用场景。
你将学到
- 掌握BFS、DFS、Dijkstra、A*四种核心图算法的原理与JavaScript实现
- 使用HTML5 Canvas进行2D网格地图的绘制与交互开发
- 设计算法的可视化动画与状态同步机制
- 理解并比较不同算法在路径查找问题上的时空复杂度特性
- 构建模块化、可扩展的前端应用程序架构
前置知识
在开始本案例前,请确保你已掌握以下知识:
- JavaScript ES6+语法基础
- HTML5 Canvas基础操作
- 基本的数据结构知识(栈、队列、堆)
- 事件处理与DOM操作基础
相关课程章节:
架构设计
本项目采用分层架构设计,各组件职责清晰:
+-------------------+ +---------------------+ +-------------------+
| 用户界面 | --> | 算法调度器 | --> | 画布渲染器 |
| (HTML/CSS/Controls)| | (PathfindingWorker)| |(CanvasRenderer) |
+-------------------+ +---------------------+ +-------------------+
| | |
v v v
+-------------------+ +---------------------+ +-------------------+
| 事件处理器 | <--> | 网格地图管理器 | <--> | 可视化状态 |
| (EventHandler) | | (GridManager) | | (VisualizationState)|
+-------------------+ +---------------------+ +-------------------+
核心组件说明:
- GridManager:管理网格地图数据,包括创建、修改墙壁、起点终点设置
- PathfindingWorker:在Web Worker中运行算法,避免阻塞UI线程
- CanvasRenderer:负责网格地图的绘制与动画效果
- EventHandler:处理用户交互事件(点击、拖拽、算法选择等)
- VisualizationState:管理算法执行过程中的可视化状态
实现步骤
第1步:搭建基础HTML结构与CSS样式
首先创建基础的HTML页面结构,包含控制面板、画布和状态信息区域。
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图算法路径查找器</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<h1>图算法路径查找器</h1>
<p class="subtitle">交互式可视化学习平台</p>
</header>
<div class="main-content">
<!-- 控制面板 -->
<aside class="control-panel">
<div class="panel-section">
<h3>地图控制</h3>
<button id="clear-map">清除地图</button>
<button id="generate-maze">生成迷宫</button>
<button id="clear-path">清除路径</button>
</div>
<div class="panel-section">
<h3>绘制模式</h3>
<div class="mode-buttons">
<button class="mode-btn active" data-mode="wall">墙壁</button>
<button class="mode-btn" data-mode="start">起点</button>
<button class="mode-btn" data-mode="end">终点</button>
</div>
</div>
<div class="panel-section">
<h3>算法选择</h3>
<select id="algorithm-select">
<option value="bfs">广度优先搜索 (BFS)</option>
<option value="dfs">深度优先搜索 (DFS)</option>
<option value="dijkstra">Dijkstra算法</option>
<option value="astar">A*算法</option>
</select>
</div>
<div class="panel-section">
<h3>动画控制</h3>
<div class="speed-control">
<label>动画速度:</label>
<input type="range" id="animation-speed" min="1" max="100" value="50">
<span id="speed-value">50</span>
</div>
<div class="button-group">
<button id="start-btn">开始搜索</button>
<button id="stop-btn">停止搜索</button>
</div>
</div>
<div class="panel-section stats">
<h3>算法统计</h3>
<div id="stats-container">
<div class="stat-item">
<span class="stat-label">探索节点:</span>
<span id="nodes-explored">0</span>
</div>
<div class="stat-item">
<span class="stat-label">路径长度:</span>
<span id="path-length">0</span>
</div>
<div class="stat-item">
<span class="stat-label">执行时间:</span>
<span id="execution-time">0ms</span>
</div>
</div>
</div>
</aside>
<!-- 主画布区域 -->
<main class="canvas-container">
<div class="canvas-wrapper">
<canvas id="pathfinding-canvas"></canvas>
<div class="canvas-info">
<span>画布大小:<span id="canvas-size">30x20</span></span>
<span>格子大小:<span id="cell-size">20px</span></span>
</div>
</div>
<!-- 算法说明 -->
<div class="algorithm-info" id="algorithm-info">
<h3 id="algo-title">广度优先搜索 (BFS)</h3>
<p id="algo-description">BFS是一种盲目搜索算法,它会系统地探索图的每一个节点。从起点开始,首先访问所有相邻节点,然后再访问这些节点的相邻节点,依此类推。</p>
<div class="complexity-info">
<span>时间复杂度:<span id="time-complexity">O(V + E)</span></span>
<span>空间复杂度:<span id="space-complexity">O(V)</span></span>
</div>
</div>
</main>
</div>
<!-- 图例说明 -->
<footer class="legend">
<div class="legend-item">
<div class="legend-color start"></div>
<span>起点</span>
</div>
<div class="legend-item">
<div class="legend-color end"></div>
<span>终点</span>
</div>
<div class="legend-item">
<div class="legend-color wall"></div>
<span>墙壁</span>
</div>
<div class="legend-item">
<div class="legend-color visited"></div>
<span>已访问</span>
</div>
<div class="legend-item">
<div class="legend-color frontier"></div>
<span>待探索</span>
</div>
<div class="legend-item">
<div class="legend-color path"></div>
<span>最终路径</span>
</div>
</footer>
</div>
<!-- JavaScript模块 -->
<script src="js/GridManager.js"></script>
<script src="js/PathfindingWorker.js"></script>
<script src="js/CanvasRenderer.js"></script>
<script src="js/EventHandler.js"></script>
<script src="js/main.js"></script>
</body>
</html>
/* styles.css */
:root {
--primary-color: #4a6fa5;
--secondary-color: #6c8fc4;
--success-color: #4caf50;
--warning-color: #ff9800;
--danger-color: #f44336;
--dark-color: #2c3e50;
--light-color: #ecf0f1;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
header {
background: var(--primary-color);
color: white;
padding: 20px 30px;
text-align: center;
}
.subtitle {
opacity: 0.8;
margin-top: 5px;
}
.main-content {
display: flex;
min-height: 70vh;
}
.control-panel {
width: 300px;
background: var(--light-color);
padding: 20px;
border-right: 1px solid #ddd;
overflow-y: auto;
}
.canvas-container {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
}
.canvas-wrapper {
position: relative;
margin-bottom: 20px;
}
canvas {
border: 2px solid var(--dark-color);
background: white;
cursor: crosshair;
}
.canvas-info {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 14px;
color: #666;
}
.panel-section {
background: white;
padding: 15px;
margin-bottom: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.panel-section h3 {
color: var(--dark-color);
margin-bottom: 12px;
font-size: 16px;
border-bottom: 2px solid var(--primary-color);
padding-bottom: 5px;
}
button {
background: var(--primary-color);
color: white;
border: none;
padding: 10px 15px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: var(--secondary-color);
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.mode-buttons {
display: flex;
gap: 5px;
margin-top: 10px;
}
.mode-btn {
flex: 1;
padding: 8px;
font-size: 12px;
}
.mode-btn.active {
background: var(--success-color);
}
select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
margin-top: 10px;
}
.speed-control {
margin: 15px 0;
}
.speed-control input[type="range"] {
width: 100%;
margin: 10px 0;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.button-group button {
flex: 1;
}
.stats {
background: white;
}
.stat-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.stat-label {
font-weight: bold;
color: var(--dark-color);
}
.algorithm-info {
background: var(--light-color);
padding: 20px;
border-radius: 8px;
margin-top: auto;
}
.algorithm-info h3 {
color: var(--primary-color);
margin-bottom: 10px;
}
.algorithm-info p {
color: #555;
line-height: 1.6;
}
.complexity-info {
display: flex;
gap: 20px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #ddd;
}
.legend {
display: flex;
justify-content: center;
gap: 30px;
padding: 20px;
background: var(--light-color);
border-top: 1px solid #ddd;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 4px;
}
.start { background: #4CAF50; }
.end { background: #f44336; }
.wall { background: #333; }
.visited { background: #2196F3; opacity: 0.6; }
.frontier { background: #FF9800; }
.path { background: #FFEB3B; }
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
}
.control-panel {
width: 100%;
border-right: none;
border-bottom: 1px solid #ddd;
}
}
第2步:实现网格地图管理器
创建GridManager.js,负责管理网格地图的状态。
// js/GridManager.js
class GridManager {
constructor(rows = 30, cols = 20) {
this.rows = rows;
this.cols = cols;
this.grid = [];
this.startNode = null;
this.endNode = null;
this.isMouseDown = false;
this.currentMode = 'wall';
this.initializeGrid();
}
// 初始化网格
initializeGrid() {
this.grid = [];
for (let row = 0; row < this.rows; row++) {
this.grid[row] = [];
for (let col = 0; col < this.cols; col++) {
this.grid[row][col] = {
row,
col,
isWall: false,
isStart: false,
isEnd: false,
isVisited: false,
isInPath: false,
distance: Infinity,
totalDistance: Infinity, // 对于Dijkstra和A*算法
heuristic: 0, // 对于A*算法
parent: null,
fScore: Infinity, // A*算法的f(n) = g(n) + h(n)
gScore: Infinity, // A*算法的g(n)
};
}
}
// 设置默认起点和终点
this.startNode = this.grid[Math.floor(this.rows / 2)][5];
this.startNode.isStart = true;
this.endNode = this.grid[Math.floor(this.rows / 2)][this.cols - 6];
this.endNode.isEnd = true;
}
// 设置墙壁
setWall(row, col, isWall) {
if (this.isValidPosition(row, col)) {
const node = this.grid[row][col];
if (!node.isStart && !node.isEnd) {
node.isWall = isWall;
return true;
}
}
return false;
}
// 设置起点
setStart(row, col) {
if (this.isValidPosition(row, col)) {
const node = this.grid[row][col];
if (!node.isWall && !node.isEnd) {
// 清除旧起点
if (this.startNode) {
this.startNode.isStart = false;
}
// 设置新起点
node.isStart = true;
this.startNode = node;
return true;
}
}
return false;
}
// 设置终点
setEnd(row, col) {
if (this.isValidPosition(row, col)) {
const node = this.grid[row][col];
if (!node.isWall && !node.isStart) {
// 清除旧终点
if (this.endNode) {
this.endNode.isEnd = false;
}
// 设置新终点
node.isEnd = true;
this.endNode = node;
return true;
}
}
return false;
}
// 验证位置是否有效
isValidPosition(row, col) {
return row >= 0 && row < this.rows && col >= 0 && col < this.cols;
}
// 获取邻居节点(上下左右四个方向)
getNeighbors(node) {
const neighbors = [];
const directions = [
{ row: -1, col: 0 }, // 上
{ row: 1, col: 0 }, // 下
{ row: 0, col: -1 }, // 左
{ row: 0, col: 1 }, // 右
];
for (const dir of directions) {
const newRow = node.row + dir.row;
const newCol = node.col + dir.col;
if (this.isValidPosition(newRow, newCol)) {
const neighbor = this.grid[newRow][newCol];
if (!neighbor.isWall) {
neighbors.push(neighbor);
}
}
}
return neighbors;
}
// 获取对角线邻居(用于支持对角线移动)
getDiagonalNeighbors(node) {
const neighbors = this.getNeighbors(node);
const diagonalDirections = [
{ row: -1, col: -1 }, // 左上
{ row: -1, col: 1 }, // 右上
{ row: 1, col: -1 }, // 左下
{ row: 1, col: 1 }, // 右下
];
for (const dir of diagonalDirections) {
const newRow = node.row + dir.row;
const newCol = node.col + dir.col;
if (this.isValidPosition(newRow, newCol)) {
const neighbor = this.grid[newRow][newCol];
if (!neighbor.isWall) {
neighbors.push(neighbor);
}
}
}
return neighbors;
}
// 清除所有路径标记
clearPath() {
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
const node = this.grid[row][col];
node.isVisited = false;
node.isInPath = false;
node.distance = Infinity;
node.totalDistance = Infinity;
node.heuristic = 0;
node.parent = null;
node.fScore = Infinity;
node.gScore = Infinity;
}
}
}
// 清除整个地图
clearMap() {
this.initializeGrid();
}
// 生成简单迷宫
generateMaze() {
this.clearMap();
// 使用递归回溯算法生成迷宫
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
// 随机生成墙壁(30%概率)
if (Math.random() < 0.3) {
this.setWall(row, col, true);
}
}
}
// 确保起点和终点不是墙壁
this.setWall(this.startNode.row, this.startNode.col, false);
this.setWall(this.endNode.row, this.endNode.col, false);
return this.grid;
}
// 获取网格的副本(用于算法计算)
getGridCopy() {
return this.grid.map(row => row.map(node => ({...node})));
}
// 从副本恢复状态
restoreFromCopy(gridCopy) {
this.grid = gridCopy;
this.startNode = this.grid.find(row => row.find(node => node.isStart)).find(node => node.isStart);
this.endNode = this.grid.find(row => row.find(node => node.isEnd)).find(node => node.isEnd);
}
// 根据行列获取画布坐标
getCanvasPosition(row, col, cellSize) {
return {
x: col * cellSize,
y: row * cellSize
};
}
// 根据画布坐标获取行列
getGridPosition(x, y, cellSize) {
return {
row: Math.floor(y / cellSize),
col: Math.floor(x / cellSize)
};
}
}
// 导出GridManager类供其他模块使用
window.GridManager = GridManager;
第3步:实现路径查找算法
创建PathfindingWorker.js,包含四种算法的实现。
// js/PathfindingWorker.js
class PathfindingWorker {
constructor() {
this.algorithms = {
bfs: this.bfs.bind(this),
dfs: this.dfs.bind(this),
dijkstra: this.dijkstra.bind(this),
astar: this.astar.bind(this)
};
}
// 广度优先搜索算法
bfs(grid, startNode, endNode) {
const gridCopy = this.getGridCopy(grid);
const start = gridCopy[startNode.row][startNode.col];
const end = gridCopy[endNode.row][endNode.col];
const queue = [start];
const visited = new Set();
visited.add(`${start.row},${start.col}`);
let nodesExplored = 0;
while (queue.length > 0) {
const currentNode = queue.shift();
nodesExplored++;
// 找到终点
if (currentNode.row === end.row && currentNode.col === end.col) {
return this.reconstructPath(currentNode, nodesExplored);
}
// 获取邻居节点
const neighbors = this.getNeighbors(gridCopy, currentNode);
for (const neighbor of neighbors) {
const key = `${neighbor.row},${neighbor.col}`;
if (!visited.has(key)) {
visited.add(key);
neighbor.parent = currentNode;
queue.push(neighbor);
}
}
}
return { path: [], nodesExplored, success: false };
}
// 深度优先搜索算法
dfs(grid, startNode, endNode) {
const gridCopy = this.getGridCopy(grid);
const start = gridCopy[startNode.row][startNode.col];
const end = gridCopy[endNode.row][endNode.col];
const stack = [start];
const visited = new Set();
visited.add(`${start.row},${start.col}`);
let nodesExplored = 0;
while (stack.length > 0) {
const currentNode = stack.pop();
nodesExplored++;
// 找到终点
if (currentNode.row === end.row && currentNode.col === end.col) {
return this.reconstructPath(currentNode, nodesExplored);
}
// 获取邻居节点
const neighbors = this.getNeighbors(gridCopy, currentNode);
for (const neighbor of neighbors) {
const key = `${neighbor.row},${neighbor.col}`;
if (!visited.has(key)) {
visited.add(key);
neighbor.parent = currentNode;
stack.push(neighbor);
}
}
}
return { path: [], nodesExplored, success: false };
}
// Dijkstra算法
dijkstra(grid, startNode, endNode) {
const gridCopy = this.getGridCopy(grid);
const start = gridCopy[startNode.row][startNode.col];
const end = gridCopy[endNode.row][endNode.col];
// 初始化距离
start.distance = 0;
// 使用优先队列(最小堆)来优化
const priorityQueue = new MinHeap();
priorityQueue.enqueue(start, 0);
let nodesExplored = 0;
while (!priorityQueue.isEmpty()) {
const currentNode = priorityQueue.dequeue().node;
nodesExplored++;
// 跳过已处理的节点
if (currentNode.isProcessed) continue;
currentNode.isProcessed = true;
// 找到终点
if (currentNode.row === end.row && currentNode.col === end.col) {
return this.reconstructPath(currentNode, nodesExplored);
}
// 获取邻居节点
const neighbors = this.getNeighbors(gridCopy, currentNode);
for (const neighbor of neighbors) {
const tentativeDistance = currentNode.distance + 1; // 假设每步距离为1
if (tentativeDistance < neighbor.distance) {
neighbor.distance = tentativeDistance;
neighbor.parent = currentNode;
priorityQueue.enqueue(neighbor, tentativeDistance);
}
}
}
return { path: [], nodesExplored, success: false };
}
// A*算法
astar(grid, startNode, endNode, heuristic = 'manhattan') {
const gridCopy = this.getGridCopy(grid);
const start = gridCopy[startNode.row][startNode.col];
const end = gridCopy[endNode.row][endNode.col];
// 初始化距离
start.gScore = 0;
start.fScore = this.calculateHeuristic(start, end, heuristic);
// 使用优先队列(最小堆)
const openSet = new MinHeap();
openSet.enqueue(start, start.fScore);
const closedSet = new Set();
let nodesExplored = 0;
while (!openSet.isEmpty()) {
const currentNode = openSet.dequeue().node;
nodesExplored++;
// 找到终点
if (currentNode.row === end.row && currentNode.col === end.col) {
return this.reconstructPath(currentNode, nodesExplored);
}
closedSet.add(`${currentNode.row},${currentNode.col}`);
// 获取邻居节点
const neighbors = this.getNeighbors(gridCopy, currentNode);
for (const neighbor of neighbors) {
const neighborKey = `${neighbor.row},${neighbor.col}`;
if (closedSet.has(neighborKey)) continue;
const tentativeGScore = currentNode.gScore + 1; // 假设每步距离为1
if (tentativeGScore < neighbor.gScore) {
neighbor.parent = currentNode;
neighbor.gScore = tentativeGScore;
neighbor.fScore = tentativeGScore + this.calculateHeuristic(neighbor, end, heuristic);
if (!openSet.contains(neighbor)) {
openSet.enqueue(neighbor, neighbor.fScore);
}
}
}
}
return { path: [], nodesExplored, success: false };
}
// 计算启发式函数
calculateHeuristic(node, endNode, type = 'manhattan') {
const dx = Math.abs(node.col - endNode.col);
const dy = Math.abs(node.row - endNode.row);
switch (type) {
case 'manhattan': // 曼哈顿距离
return dx + dy;
case 'euclidean': // 欧几里得距离
return Math.sqrt(dx * dx + dy * dy);
case 'diagonal': // 对角线距离
return Math.max(dx, dy);
default:
return dx + dy;
}
}
// 重建路径
reconstructPath(endNode, nodesExplored) {
const path = [];
let currentNode = endNode;
while (currentNode) {
path.unshift({
row: currentNode.row,
col: currentNode.col
});
currentNode = currentNode.parent;
}
return {
path,
nodesExplored,
success: true,
pathLength: path.length
};
}
// 获取网格副本
getGridCopy(grid) {
return grid.map(row => row.map(node => ({
...node,
isProcessed: false,
parent: null
})));
}
// 获取邻居节点
getNeighbors(grid, node) {
const neighbors = [];
const directions = [
{ row: -1, col: 0 }, // 上
{ row: 1, col: 0 }, // 下
{ row: 0, col: -1 }, // 左
{ row: 0, col: 1 }, // 右
];
for (const dir of directions) {
const newRow = node.row + dir.row;
const newCol = node.col + dir.col;
if (newRow >= 0 && newRow < grid.length &&
newCol >= 0 && newCol < grid[0].length) {
const neighbor = grid[newRow][newCol];
if (!neighbor.isWall) {
neighbors.push(neighbor);
}
}
}
return neighbors;
}
// 执行指定算法
execute(algorithmName, grid, startNode, endNode) {
const algorithm = this.algorithms[algorithmName];
if (!algorithm) {
throw new Error(`不支持的算法: ${algorithmName}`);
}
return algorithm(grid, startNode, endNode);
}
}
// 最小堆实现(优先队列)
class MinHeap {
constructor() {
this.heap = [];
}
enqueue(node, priority) {
this.heap.push({ node, priority });
this.bubbleUp(this.heap.length - 1);
}
dequeue() {
if (this.isEmpty()) return null;
const min = this.heap[0];
const end = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = end;
this.sinkDown(0);
}
return min;
}
contains(node) {
return this.heap.some(item =>
item.node.row === node.row && item.node.col === node.col
);
}
isEmpty() {
return this.heap.length === 0;
}
bubbleUp(index) {
const element = this.heap[index];
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this.heap[parentIndex];
if (element.priority >= parent.priority) break;
this.heap[parentIndex] = element;
this.heap[index] = parent;
index = parentIndex;
}
}
sinkDown(index) {
const length = this.heap.length;
const element = this.heap[index];
while (true) {
let leftChildIdx = 2 * index + 1;
let rightChildIdx = 2 * index + 2;
let leftChild, rightChild;
let swap = null;
if (leftChildIdx < length) {
leftChild = this.heap[leftChildIdx];
if (leftChild.priority < element.priority) {
swap = leftChildIdx;
}
}
if (rightChildIdx < length) {
rightChild = this.heap[rightChildIdx];
if ((swap === null && rightChild.priority < element.priority) ||
(swap !== null && rightChild.priority < leftChild.priority)) {
swap = rightChildIdx;
}
}