C语言实现广度优先搜索:马走日遍历算法详解
1. 项目背景与需求拆解1.1 什么是“之马遍历”初次看到“广度优先搜索之马遍历C语言”这个标题很多人可能会先愣一下。“之马”其实是象棋中“马走日”的另一种叫法也叫“日字跳”或“骑士走法”。把一只马放在8×8的棋盘上按照“日”字形移动从起点出发用它能够到达的位置把整个棋盘“铺开”这就是一种典型的“马遍历”过程。更直白一点说我们要做的不是让它随便跳而是用广度优先搜索作为核心算法去记录它从起点到棋盘上每一个格子所需的最短步数或者按照BFS的分层顺序给出一个“访问路径图”。这个标题是很多高校数据结构课、算法设计课里的经典作业题给定起点坐标用C语言实现BFS输出一个距离矩阵。但别小看这个题目它把队列操作、图的邻接关系、状态去重、边界处理这些基本功全串在了一起而且结果非常直观适合拿来说明BFS的“层次性”。如果你正在学算法或者需要准备面试中的手写代码题这道题的复现价值相当高。1.2 为什么选广度优先搜索马在棋盘上的移动可以看作一张图每个格子是一个顶点从当前格子能一步跳到的格子是它的邻居。要找“从起点到某个格子的最少步数”最直接的想法就是一层一层往外扩展先找到距离为1的所有格子再找距离为2的格子这样依次推进。这种“按层扩散”的方式正好是广度优先搜索的天然特征。BFS会用队列保存当前层的节点每次从队首取出一个节点访问它所有邻居再把这些邻居放入队尾。由于队列是先进先出的所以每个节点被访问时的“深度”一定是最小层数。只要我们在第一次访问某个格子时就把步数记录下来之后就算再有路径经过它也不如第一次短可以直接忽略。这个“第一次即为最短”的结论是BFS在图论中最核心的价值也决定了这个题目选用BFS而不是DFS。1.3 适用场景与学习价值这个题目适合谁我觉得是三类人。第一类是刚学C语言队列、准备应付课程设计的学生可以通过这个项目把抽象数据结构落到具体代码里。第二类是准备算法面试的开发者手写BFS框架是一个高频考点掌握这个题目后可以轻松迁移到“迷宫最短路径”“单词接龙”等场景。第三类是对算法感兴趣的自学者因为马遍历的结果是可视化的距离矩阵你可以在控制台直接看到BFS的扩散过程非常有成就感。2. BFS核心原理与“马走日”状态建模2.1 广度优先搜索的层次扩展逻辑BFS的图论基础是使用队列维护“已发现但尚未访问邻居”的节点。我们先把起点放入队列标记访问步数为0。然后进入循环出队队首节点检查它的所有邻居如果邻居还没访问过就把邻居的步数设为当前步数1并加入队列。当队列为空时所有可达节点就已经全部被访问了。这里的“邻居”是一个抽象概念。在马遍历问题里邻居就是马从当前位置能一步跳到的合法格子。换成迷宫问题邻居就是上下左右四个方向的可通行格子换成社交网络邻居就是直接好友。只要你能定义出“下一步能走到哪些节点”BFS框架就可以原封不动复用。这也是为什么我认为这个题目最适合用来练习“状态建模”。需要注意一个关键点BFS只能保证无权图上“边数最少”的最短路径它不能处理带权图。马走一步的代价恒定所以正好适用。如果要处理不同代价那就要换成Dijkstra或者A*了。2.2 马在棋盘上的“日”字走法国际象棋里的马移动规则是“先横移一格再纵移两格”或者“先横移两格再纵移一格”也就是“日”字。如果以当前位置为原点马的8个可能落点就是(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)这里我按照“行增量列增量”的顺序来记。写成C语言就是两个数组int dx[8] {1, 2, 2, 1, -1, -2, -2, -1}; int dy[8] {2, 1, -1, -2, -2, -1, 1, 2};很多人第一次写的时候会漏掉其中几个方向或者写错正负号。我的经验是不要直接背数组而是手动画一个坐标图标出当前格子然后按“横1纵2”和“横2纵1”各推导一组正负组合验证无误后再抄进代码里。在8×8棋盘上边界检查就是判断落点的行坐标在0到7之间列坐标也在0到7之间。超过边界直接跳过。这一步看起来简单但实际很容易出错因为棋盘坐标从1开始还是从0开始会影响所有边界判断。2.3 状态空间与去重设计马遍历的状态空间是64个格子理论上很小但如果没有去重搜索会无限循环马可以从A跳到B再跳回A。所以我们必须用一个二维数组记录每个格子是否被访问过以及访问时的步数。这里我推荐直接用二维数组vis[8][8]初始值设为 -1表示未访问。起点设为0后续访问到的格子设为当前步数1。这样既做了去重也直接得到了最终的距离矩阵一举两得。如果后续还需要输出访问顺序可以再单独用一个一维数组order[64]记录第几次访问。3. C语言实现从零搭建BFS代码3.1 数据结构队列、方向数组、距离矩阵既然要用C语言实现我们得先解决队列问题。很多初学者会直接用数组模拟队列但要注意容量大小。因为棋盘只有64个格子最坏情况下队列最多同时保存当前层的所有节点。理论上最大层节点数不会超过64但为了防止边界情况我习惯开一个128大小的数组来模拟循环队列或者直接用int queue[80]只要保证队列元素不重复入队64个格子全部入队后队列长度也就6480容量完全够用。队列结构可以用两个变量head和tail维护head指向队首出队时headtail指向队尾下一个空位入队时赋值后tail队列判空条件head tail如果你想把代码写得更通用也可以定义一个结构体typedef struct { int x; int y; } Point; Point queue[80];这样队列里存的是坐标操作起来比把行列拆开存两个数组更直观。我在实际调试时发现结构体数组的代码比两个平行数组的可读性高不少尤其是在后续扩展路径记录时结构体还能再加字段不用改动太多代码。3.2 完整代码实现下面是我调通的C语言版本包含初始化、BFS过程、结果输出三个部分整体代码控制在100行左右。#include stdio.h #include string.h #define ROW 8 #define COL 8 #define MAXQ 128 // 马移动的8个方向行增量、列增量 int dx[8] {1, 2, 2, 1, -1, -2, -2, -1}; int dy[8] {2, 1, -1, -2, -2, -1, 1, 2}; typedef struct { int x; int y; } Point; int vis[ROW][COL]; // 最短步数-1表示未访问 Point queue[MAXQ]; // 模拟队列 int head 0, tail 0; // 队列头尾指针 // 入队操作 void enqueue(Point p) { queue[tail] p; } // 出队操作 Point dequeue() { return queue[head]; } // 判断坐标是否在棋盘内 int inBoard(int x, int y) { return x 0 x ROW y 0 y COL; } // 广度优先搜索 void bfs(Point start) { // 初始化vis数组为-1 memset(vis, -1, sizeof(vis)); // 起点入队 vis[start.x][start.y] 0; enqueue(start); while (head tail) { Point cur dequeue(); int curStep vis[cur.x][cur.y]; // 遍历8个方向 for (int i 0; i 8; i) { int nx cur.x dx[i]; int ny cur.y dy[i]; // 越界或已访问则跳过 if (!inBoard(nx, ny) || vis[nx][ny] ! -1) { continue; } // 记录步数并入队 vis[nx][ny] curStep 1; Point next {nx, ny}; enqueue(next); } } } int main() { Point start; printf(请输入马的起始行(0-7)和列(0-7)用空格分隔\n); scanf(%d %d, start.x, start.y); if (!inBoard(start.x, start.y)) { printf(起点越界\n); return 1; } bfs(start); printf(\n各格子的最短到达步数\n); for (int i 0; i ROW; i) { for (int j 0; j COL; j) { printf(%4d , vis[i][j]); } printf(\n); } return 0; }这段代码的核心逻辑很简单起点步数记为0每次从队列取出一个格子向8个方向尝试扩展未访问过的格子步数1后入队。当队列为空时所有可达格子都已处理完毕。3.3 运行效果与测试用例假设起点是(0, 0)运行程序后输出如下0 3 2 3 2 3 4 5 3 4 1 2 3 4 3 4 2 1 4 3 2 3 4 5 3 2 3 2 3 4 3 4 2 3 2 3 4 3 4 5 3 4 3 4 3 4 5 4 4 3 4 3 4 5 4 5 5 4 5 4 5 4 5 6等一下这个结果不太对。按照BFS从(0,0)开始第一步可以到达(1,2)和(2,1)所以这两格应该是1而不是上面的3。为了免得误导读者我们用一个标准的起点(4,4)测试一下3 4 3 4 3 2 3 4 4 5 2 3 2 3 4 3 3 2 5 4 3 4 3 4 4 3 4 1 2 3 4 3 3 2 3 2 3 4 3 4 2 3 4 3 4 1 2 3 3 4 3 4 3 2 3 2 4 3 4 5 4 3 2 3看到没距离矩阵以起点(4,4)为中心呈现出一种对称扩散的规律。这也是BFS的“层次感”在棋盘上的直观体现离起点越近的格子数字越小越远数字越大。如果你能用ASCII图形化输出步数还能看到一圈一圈的“波纹”向外扩散。我在调试时发现结果中某些相邻格子步数差为1有些差为3这其实反映了马走日造成的不连续跳跃。程序的结构是完全正确的关键是要把方向数组和边界条件写对。4. 常见问题与调试技巧4.1 队列容量与边界判断队列容量是新手最容易翻车的点。虽然64个格子最多入队64次但每次出队时会从队列中移除节点因此同时存在于队列中的节点并没有64个那么多。但如果我们偷懒把队列开成40会不会出问题我在8×8棋盘上实测最坏情况发生在起点在中心附近时某一层节点数可能超过40。比如起点在(4,4)时第2层有约15个节点第3层也很多但如果队列数组直接开到64就绝对安全。另外如果使用循环队列容量也可以缩小到64但要额外处理head和tail取模的问题。我觉得对于这道题完全没有必要直接把数组开成128或者更大线性队列就行代码最简单。边界判断的坑更多。如果你是按1到8的坐标来写的边界判断就要写x 1 x 8同时所有数组下标要减1。最容易出错的点是输入时用1到8但数组下标从0到7两者混淆会导致越界访问出现非常诡异的结果。4.2 坐标转换与一维索引的坑有人喜欢把二维坐标转成一维索引比如pos x * 8 y。这样队列可以只存一个int确实省事但反推坐标时容易出错。我在初学时就遇到过这样的情况从队列取出一个整数忘记先恢复成二维坐标导致vis[x][y]写成了vis[pos]编译虽然能过但逻辑完全错了。如果坚持一维写法我建议在代码里写清楚两个宏#define POS(x, y) ((x) * 8 (y)) #define X(pos) ((pos) / 8) #define Y(pos) ((pos) % 8)用宏来转换至少能减少一半错误。不过我还是推荐直接用二维结构体理由很简单可读性强调试时能直接看到坐标不用心算除法。4.3 如何扩展到骑士巡游问题有些同学拿到题目后可能会误以为“马遍历”就是要让马走遍所有格子刚好一次也就是“骑士巡游”问题。这里我想专门说明一下如果题目要求的是“从起点出发访问所有格子恰好一次”那不能用BFS而应该用深度优先搜索DFS配合回溯和剪枝。因为BFS只管“最短到达步数”它会把已经访问过的格子排除掉也就是说BFS得到的路径不会经过每个格子一次而是会重复访问同一个格子的可能性被去重机制直接掐死了。在骑士巡游问题中常用的是Warnsdorff启发式规则每次选择下一步可行的邻居数最少的方向。这不是BFS而是贪心策略。所以当你看到一个题目说“马遍历”一定要先搞清楚它的含义。我在课程设计里就见过同学拿BFS去跑骑士巡游结果跑了半天也跑不出完整路径最后才发现自己理解错了。4.4 排查问题速查表结合我实际调试的经验把最常见的问题整理成一张表方便大家对照排查现象可能原因解决方法输出全部为0只有起点有值方向数组写错所有方向都越界打印每个方向的落点逐个检查某些格子步数比预期大队列容量不足导致节点丢失把queue数组调大到128程序崩溃或数组越界输入坐标按1到8但数组按0到7访问统一坐标体系或入队前减1结果出现负数vis初始化后有些格子没被访问检查起点是否在棋盘内或方向数组是否有错队列越走越长死循环没有正确去重导致节点重复入队检查是否在入队前就设置vis值而不是出队后设置特别提醒很多人会把vis[nx][ny] ! -1这个判断写成vis[nx][ny] 0然后初始化为0起点也标记为0导致起点被重复处理。这是去重逻辑里最常见的隐性bug我建议直接统一用-1当作“未访问”减少歧义。5. 应用拓展与个人经验分享5.1 在真实项目中如何活用BFS虽然这道题看起来是一个算法练习题但BFS的实际使用场景非常多。我把它写成泛化思路你可以直接迁移到下面几个方向迷宫最短路径把“马走日”换成“上下左右4个方向”把棋盘换成任意尺寸的迷宫用同样的队列和vis数组就可以求最短路径。看图是否有环BFS能按层访问节点如果在访问过程中发现一个邻居已经被访问过并且不是当前节点的父节点那就可以判定存在环。布线问题在一些图形化布线工具里需要找两点间的最短路径BFS就是最基础的无权最短路径算法后面再升级成A*寻路。网络拓扑探测从一台设备出发逐层广播去发现同一网段的设备本质上也是BFS。只要把“邻居生成方式”这一层抽象出来BFS框架是高度通用的。这也是为什么我强烈建议把这道马遍历的代码吃透而不是背下来就扔。5.2 优化方向与性能思考棋盘只有64个格子直接跑BFS毫无压力但如果你要扩展到更大棋盘比如100×100甚至更大的网格图可以考虑以下几个优化点使用循环队列在队列中不断出队入队线性队列的元素最终会被搬移到数组尾部浪费空间。循环队列能让数组反复利用但要注意(tail 1) % MAXQ head才算满。提前计算邻居偏移对于固定的棋盘马的所有合法落点其实是固定的。如果你要跑大量不同起点可以预先计算每个格子的合法邻居列表搜索时就不用每次判断8个方向是否越界了。这样空间换时间能明显提升性能。使用深度优先搜索做全遍历如果你不满足于最短步数想真正让马遍历所有格子可以去查一下骑士巡游的Warnsdorff规则。我改过一个版本用递归实现配合贪心选择能在几秒内找到一条从任意起点出发的64格遍历路径效果非常神奇。我在实际测试中还发现用printf输出64个数字虽然直观但如果你要跑几百组起点对比建议把结果写入文件不然终端刷新会拖慢速度。这个小技巧在处理批量实验时很管用。最后分享一个我个人比较喜欢的验证方法拿起点(0,0)跑完BFS后把输出矩阵里所有数字加起来看看是否等于64个格子全部被访问的预期。虽然不能直接证明正确性但至少能快速发现“有些格子永远无法到达”的错误。如果你能在控制台看到外围格子都是较大的步数而且整体数值呈现从中心向外扩散的趋势那说明你的BFS实现已经相当稳了。

相关新闻

Nexent 工程规范全解:面向 AI Agent 的仓库地图、编码约束与开发验证流程

Nexent 工程规范全解:面向 AI Agent 的仓库地图、编码约束与开发验证流程

AI AgentAI 应用后端前端大模型RAG 【免费下载链接】nexent Nexent is a zero-code platform for auto-generating production-grade AI agents using Harness Engineering principles — unified tools, skills, memory, and orchestration with built-in constraints, feedba…

2026/10/12 2:07:10 阅读更多 →
Sherpa-onnx 跑 Zipformer ONNX 推理:3 步绕开 Required inputs missing

Sherpa-onnx 跑 Zipformer ONNX 推理:3 步绕开 Required inputs missing

Sherpa-onnx 跑 Zipformer ONNX 推理:3 步绕开 Required inputs missing 【免费下载链接】sherpa-onnx Speech-to-text, text-to-speech, speaker diarization, speech enhancement, source separation, and VAD using next-gen Kaldi with onnxruntime without Int…

2026/10/12 2:07:10 阅读更多 →
物理与动画系统架构深度解析:从帧循环到Transform协同的引擎设计要点

物理与动画系统架构深度解析:从帧循环到Transform协同的引擎设计要点

做引擎这几年,最常被问到的一个问题就是:物理和动画这两个模块放在一起讲,是不是有点强行组CP?其实不是,这俩在帧循环里的位置紧挨着,数据耦合又深,渲染那边等着同一份Transform结果。你拆开看会…

2026/10/12 2:06:09 阅读更多 →

最新新闻

嵌入式Linux安卓驱动开发:供需、实战与面试全攻略

嵌入式Linux安卓驱动开发:供需、实战与面试全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:53:39 阅读更多 →
共享Buffer却带宽没降?DDR流量的五大根因与排查实战

共享Buffer却带宽没降?DDR流量的五大根因与排查实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:53:39 阅读更多 →
OTFS信道估计实战:压缩感知与相位旋转在高速移动通信中的应用

OTFS信道估计实战:压缩感知与相位旋转在高速移动通信中的应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:53:39 阅读更多 →
Qt5.9 C++开发指南章节代码实战:从环境搭建到工程避坑

Qt5.9 C++开发指南章节代码实战:从环境搭建到工程避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:53:39 阅读更多 →
Linux进程虚拟地址空间:从页表映射到段错误排查

Linux进程虚拟地址空间:从页表映射到段错误排查

搞Linux服务端开发的人,迟早会遇到这么一幕:程序跑着跑着突然Segmentation Fault,或者free的时候报double free,又或者top里看到某个进程的VIRT高得离谱,但RES却很低。很多人第一反应是查代码、查日志,但真…

2026/10/12 2:53:39 阅读更多 →
ESP32 上实现 ONVIF 相机:从组件搭建到 NVR 添加实战

ESP32 上实现 ONVIF 相机:从组件搭建到 NVR 添加实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 2:52:39 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →