C++汉诺塔课程设计:递归实现、非递归栈模拟与完整代码解析

📅 发布时间:2026/10/13 3:29:09
C++汉诺塔课程设计:递归实现、非递归栈模拟与完整代码解析
简介C课程设计常用的汉诺塔源代码适用于正在学习C递归与分治思想的初学者以及需要完成相关课程设计的学生。代码完整实现了汉诺塔三柱模型任何时候大盘必须放在小盘之下一次仅能移动一个盘子程序将全部盘子从起始柱A借助辅助柱B移至目标柱C并在控制台逐行打印移动盘子的编号与路径。核心逻辑由一个递归函数完成函数接收当前柱、辅助柱、目标柱三个参数对于n个盘子先把n-1个盘子经目标柱移到辅助柱再把第n个盘子直接移到目标柱最后将辅助柱上的n-1个盘子经起始柱移回目标柱逐步拆解直到只剩一个盘子时完成最终移动直观展示递归调用与参数传递的完整轨迹。资源共5个文件核心是cpp源文件另附dsw、dsp、opt、ncb等Visual C工程与配置文件rar压缩包仅7KB代码结构精简可直接编译运行或修改测试不同盘子数。目前已有207人浏览学习适合作为课程设计配套参考、递归算法课堂演示或课后拓展练习材料。1. C汉诺塔课程设计从小递归到大作业的四块拼图先说一个反直觉的结论汉诺塔的递归核心只有五六行但课程设计最吃亏的恰恰是「只交了五六行」。网上随手能搜到大量 C 汉诺塔代码多数是黑匣子——能打印移动过程却答不上「步数为什么是 2^n-1」「被问到非递归怎么办」「换台机器为什么显示乱码」。这份 C 汉诺塔源代码的价值不在递归函数本身而在把「能跑的递归」扩成了完整课程设计递归版打底非递归栈模拟做对照交互输入、步数统计、多局运行全部配齐。适合第一次做 C 课程设计、需要一个能写进报告也能讲清楚的参照也适合想弄懂递归展开过程的读者。2. 递归核心怎么落地终止条件、递推关系与步数统计汉诺塔是递归教学里的标准案例一个盘子直接从 A 移到 C两个盘子先移小的到 B、移大的到 C、再把小的从 B 移到 C三个盘子以上完全复用同一套规则——「把上面 n-1 个先挪走」这个动作在每一层不断重复自己。落到 C 代码上第一关不是语法而是把「谁是从柱、谁是辅助柱、谁是目标柱」三个角色在参数里分清楚。这一章先把最核心的递归函数写对再把步数统计和类型边界一并解决。2.1 递归函数骨架先把移动规则翻译成代码#include iostream using namespace std; // from: 起始柱 aux: 辅助柱 to: 目标柱 void hanoi(int n, char from, char aux, char to) { if (n 0) return; // 没有盘子什么都不做 hanoi(n - 1, from, to, aux); // 上面 n-1 个从 from 借 to 挪到 aux cout from - to endl; // 第 n 个from 直接到 to hanoi(n - 1, aux, from, to); // 上面 n-1 个从 aux 借 from 挪到 to } int main() { hanoi(3, A, B, C); return 0; }这段代码里最容易写错的是第二次调用那行参数轮换。hanoi(n - 1, from, to, aux) 的含义是把上面 n-1 个盘子从起始柱挪到辅助柱但这期间要借用目标柱来中转所以原来代表「目标柱」的 to 跑到了 aux 的位置。很多初学者把三个柱子名字背下来但不懂为什么换位置写出来的结果就是递归能跑但输出路径错误。用 n3 手推一遍会更清楚。main 调用 hanoi(3, A, B, C)它先执行 hanoi(2, A, C, B)这个子调用又会先执行 hanoi(1, A, B, C)hanoi(1) 先调 hanoi(0, ...) 直接返回输出 A-C再调 hanoi(0, ...) 返回。等 hanoi(2, A, C, B) 全部展开完总共输出 7 行正好是 2^3 - 1。想把递归过程真正看穿可以在 hanoi 开头临时加一行cout enter n n from from aux aux to to endl;再跑 3 层会看到 n 值先降后升的完整调用树。调试完记得删掉不然会污染课程设计报告里的运行结果。2.2 步数统计与类型坑用 2^n - 1 校准答案课程设计只打印移动过程不算总步数报告里会少一块内容答辩也容易被追着问。步数统计最常见的做法是让计数器以引用方式穿透所有递归层保证每一层自增的都是同一个变量。#include iostream using namespace std; void hanoi(int n, char from, char aux, char to, unsigned long long steps) { if (n 0) return; hanoi(n - 1, from, to, aux, steps); cout from - to endl; steps; hanoi(n - 1, aux, from, to, steps); } int main() { unsigned long long steps 0; hanoi(10, A, B, C, steps); cout total steps steps endl; return 0; }这里有两个关键点。第一steps 用引用传递递归每一层操作的都是 main 里那一个变量如果声明成普通局部变量每一层递归都会创建自己的副本最后统计结果永远是 0 或 1。第二计数器类型直接选 unsigned long long因为汉诺塔步数是指数增长int 很容易翻车。用 2^n - 1 这个公式可以验证任何一层的输出n10 必须等于 1023n20 必须等于 1048575。下面这张表说明了为什么不能用 int 做计数器。盘子数理论步数 2^n - 1int 是否安全101023安全1532767安全201048575安全312147483647恰好到 int 上限324294967295int 已溢出unsigned long long 也不是无限大的。它的最大值是 2^64 - 1对应 63 层盘子的理论步数 2^63 - 164 层以上连统计步数都会溢出。课程设计演示时把输入范围锁在 1~20 层是合理选择这一层会在第 3 章的输入校验里落实。3. 从函数到课程设计交互、状态跟踪与循环运行递归函数能跑只是第一步。课程设计要交的不是一段代码而是一个能演示的完整小程序用户能输入盘子数、能看到每步移动后的三根柱子状态、能连续跑多组数据。这些要求听起来不难但把代码从「函数」扩成「小系统」时三个点最容易出问题状态怎么保存、非法输入怎么拦、多局数据怎么隔离。这一章按模块拆开讲。3.1 用类封装汉诺塔从「能跑」到「能演示」课程设计报告最怕内容单薄。单函数递归版虽然正确但撑不起「设计」两个字一种常见做法是把汉诺塔包成一个类用三个 vector 模拟柱子上的盘子。移动时不再只是打印一行箭头而是真正操作柱子的数据之后要加图形界面或导出每步状态都有数据基础。#include iostream #include vector using namespace std; class Hanoi { public: explicit Hanoi(int n) : n_(n), steps_(0) { for (int i n; i 1; --i) { pegs_[0].push_back(i); } } void run() { dfs(n_, 0, 1, 2); cout total steps steps_ endl; } private: int n_; vectorint pegs_[3]; // 三根柱子盘号小的在上层 unsigned long long steps_; void dfs(int idx, int from, int aux, int to) { if (idx 0) return; dfs(idx - 1, from, to, aux); pegs_[from].pop_back(); // 从起始柱取下 pegs_[to].push_back(idx); // 放到目标柱 steps_; cout move disk idx : peg from 1 - peg to 1 endl; dfs(idx - 1, aux, from, to); } }; int main() { Hanoi game(3); game.run(); return 0; }这个类版本的核心逻辑与第 2 章的递归函数完全一致只是多了两个动作移动前 pop_back移动后 push_back。盘号直接用数字表示大小柱子上的 vector 从栈底到栈顶按从大到小排列这样每次取出的都是当前柱子上最小的盘。这里可以留意一个有趣的事实最大的第 n_ 号盘子在整个过程中只移动一次从第 0 根柱子直接到第 2 根柱子其余所有递归展开都在为这一动做铺垫。把这个观察写进课程设计报告的「算法分析」部分比单纯贴代码有说服力得多。三步移动的逻辑用 idx 表示问题规模idx-1 递归处理上方更小的盘号和前面 hanoi 函数里的 n-1 是一个意思。3.2 交互输入与输入校验非法值不能进递归课程设计一般要求用户输入盘子数不能写死。键盘输入恰恰是最容易出问题的边界手滑输入一个字母cin 会进入 fail 状态后续读取全部失败程序要么死循环要么直接跳过逻辑。下面是带校验的读取函数。#include iostream #include limits using namespace std; int readDiskCount() { int n 0; while (true) { cout input disk count (1-20): ; cin n; if (cin.fail()) { cin.clear(); cin.ignore(numeric_limitsstreamsize::max(), \n); cout not a number, try again. endl; continue; } if (n 1 || n 20) { cout range should be 1-20, try again. endl; continue; } return n; } }cin.fail() 在输入不是数字时置位不清除的话之后所有 cin 读取都会直接失败。cin.clear() 恢复流状态cin.ignore(...) 把缓冲区里残留的垃圾字符清掉避免下次读取读到同一个非法输入导致死循环。范围限制在 1~20 是实用考量20 层是 1048575 步已经够演示再往上步数千万级控制台滚动输出几万行课程设计现场演示会卡到怀疑人生。有些参考代码只检查 n 0 不检查 n 20看起来灵活实际是在给自己埋雷。第 4 章会提到递归深度超过一定量还会触发栈溢出输入范围必须在入口就锁死不能等进了递归再后悔。3.3 多局运行闭环一次提交多个测试样例课程设计演示时通常要连续测几组数据3 层、5 层、8 层各跑一遍验证步数和移动路径。如果程序只跑一次就退出现场演示就得反复重新启动体验很差。用循环包一层就能解决。int main() { char again y; while (again y || again Y) { int n readDiskCount(); Hanoi game(n); game.run(); cout play again? (y/n): ; cin again; } return 0; }多局运行的关键在于每局都创建新的 Hanoi 实例steps_ 在构造函数里初始化为 0数据天然隔离。这里踩过的坑是网上有些汉诺塔代码把计数器写成全局 int第一局跑 3 层显示 7 步没问题第二局跑 5 层直接显示 39 步而不是 31 步因为全局变量没有清零。用类或引用传参的写法天然规避这个问题后面调试代码时能省出半小时。4. 避坑与常见问题从编译到答辩的五个高频翻车点汉诺塔代码不长但工程细节上的坑一点不少。下面五条是实际做这类课程设计时最容易踩的按「现象 - 原因 - 解决」记录每一条都值得在交报告前自查一遍。4.1 步数溢出int 计数器用 32 层就变负数现象输入盘子数 31 时步数正常输入 32 时 total steps 变成 -1再往上数字越来越离谱。原因int 上限是 21474836472^31 - 1 恰好等于这个值。32 层的理论步数是 4294967295已经超过 int 上限溢出后回绕成负数。用 int 做计数器等于默认用户不会超过 31 层但课程设计输入框可不会替你保证。解决计数器用 unsigned long long并在输入校验阶段把范围限制在 1~20。即使如此也要心里有数unsigned long long 最大对应 63 层64 层以上步数统计仍然溢出。课程设计任务书一般只要求 3~8 层20 层上限有充足余量。4.2 cin 输入字母后程序像死循环错误状态不清理现象运行程序输入 abc程序不再等待输入疯狂重复打印同一条提示或者直接跳过后续所有逻辑。原因cin n 读到非法字符时会把输入流置为 fail 状态这个状态下后续所有读取操作都会立刻失败。如果代码里没有处理 fail 的分支循环条件可能退化成无限空转。解决每次读取失败后先 cin.clear() 恢复流状态再 cin.ignore() 清空缓冲区残留字符。ignore 的参数要足够大用numeric_limitsstreamsize::max()最稳妥。这个坑在第 3.2 节的 readDiskCount 里已经封死直接把那个函数抄进自己的代码即可。4.3 递归深度过大会栈溢出实验范围要主动限制现象盘子数输入 1000程序还没开始输出就崩溃或者在输出几百行后异常退出。原因递归每层调用都要占用栈空间1000 层的调用链在默认栈大小下很容易撑爆。汉诺塔递归深度等于盘子数盘子越多栈压力越大这个问题是递归写法天然带出来的。解决课程设计演示把盘子数限制在 20 以内彻底避开栈溢出风险。如果需要在答辩时表演「大数据」用第 5 章的显式栈非递归版本栈空间分配在堆上不再受调用栈限制。4.4 中文输出乱码源码编码和控制台编码打架现象源码里写「请输入盘子数」编译运行后控制台显示一串乱码同学机器上正常自己机器上乱。原因源码文件保存的编码和控制台代码页不一致中文提示在部分环境下显示异常。这是编码环境差异导致的经典问题跟算法本身完全无关但课程设计现场演示遇到它非常尴尬。解决我一般会把所有控制台输出统一用英文中文说明只留在课程设计报告里。这样最省事也完全避开不同编译环境之间的编码玄学问题。如果任务书强制要求中文提示注意源文件编码要与控制台代码页匹配。4.5 网上的全局计数器版本每局步数互相污染现象第一次跑 3 层显示 7 步第二次跑 5 层显示 39 步而不是 31 步多跑几局数字越加越大。原因很多网上流传的汉诺塔代码把计数器声明成全局 int函数内部直接 steps没有在每局开始时重置。全局变量生命周期贯穿整个程序上一局的数据留到了下一局。解决把计数器放进类里构造函数初始化为 0用函数版本就把计数器作为引用参数传入并在外部清零。第 2 章和第 3 章的写法都规避了这个问题写课程设计报告时把「数据隔离」作为设计要点写进去反而是加分项。5. 进阶用显式栈做非递归汉诺塔答辩时扛得住追问5.1 结构体 Frame 与手动模拟递归栈递归版的每一层调用在底层就是一个栈帧那完全可以用 std::stack 手动模拟这个过程。答辩时被问到「递归会不会栈溢出」「能不能改成非递归」拿出这个版本就能从容应对。柱子改用数字 0、1、2 表示因为显式栈里计算「第三根柱子」可以用 3 - from - to 一步得到。#include iostream #include stack using namespace std; struct Frame { int n; int from; int aux; int to; }; void hanoiIter(int n, int from, int aux, int to) { stackFrame st; st.push({n, from, aux, to}); // 初始任务入栈 while (!st.empty()) { Frame f st.top(); st.pop(); if (f.n 1) { cout f.from - f.to endl; continue; } // 逆序压栈右子树先入栈左子树后入栈保证左子树先处理 st.push({f.n - 1, f.aux, f.from, f.to}); cout f.from - f.to endl; st.push({f.n - 1, f.from, f.to, f.aux}); } } int main() { hanoiIter(3, 0, 1, 2); return 0; }这段代码的执行顺序与递归版严格对应。以 n2 为例初始任务 {2,0,1,2} 出栈先是右子树 {1,1,0,2} 入栈再输出 0 - 1接着左子树 {1,0,2,1} 入栈。栈顶是左子树先打印 0 - 2再弹右子树打印 1 - 2结果与递归版完全一致。5.2 递归版与非递归版结果对照手改逻辑最容易改错最稳妥的验证方式是把两个版本编译后分别跑同一输入输出到文件再逐行比较。g hanoi_rec.cpp -o hanoi_rec g hanoi_iter.cpp -o hanoi_iter ./hanoi_rec rec.txt ./hanoi_iter iter.txt diff rec.txt iter.txt没有 diff 命令的环境用 fc 代替两边输出完全一致说明非递归逻辑没有改错。比对前要保证两个版本的输出格式相同字符标签和数字标签会直接导致 diff 全是差异。步数校验用 2^n - 1 这个硬指标非递归版不需要额外加计数器直接用输出文件的行数核对即可。我后来做任何递归相关的课程设计都会强制自己写一版递归、再写一版显式栈模拟跑完顺手用 diff 核对输出。这个习惯起源于某次答辩被问「递归会不会栈溢出」当时只答得上「会吧」回去就把非递归版本补上了。以后你拿到这份源码建议也先完整跑一轮对照再把运行结果贴进报告希望帮到你。本文还有配套的精品资源点击获取