简介: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 的位置。很多初学者把三个柱子名字背下来但不懂为什么换位置,写出来的结果就是递归能跑但输出路径错误。
用 n=3 手推一遍会更清楚。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 这个公式可以验证任何一层的输出:n=10 必须等于 1023,n=20 必须等于 1048575。下面这张表说明了为什么不能用 int 做计数器。
| 盘子数 | 理论步数 2^n - 1 | int 是否安全 |
|---|---|---|
| 10 | 1023 | 安全 |
| 15 | 32767 | 安全 |
| 20 | 1048575 | 安全 |
| 31 | 2147483647 | 恰好到 int 上限 |
| 32 | 4294967295 | int 已溢出 |
unsigned long long 也不是无限大的。它的最大值是 2^64 - 1,对应 63 层盘子的理论步数 2^63 - 1,64 层以上连统计步数都会溢出。课程设计演示时把输入范围锁在 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_; vector<int> 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_limits<streamsize>::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 上限是 2147483647,2^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_limits<streamsize>::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) { stack<Frame> 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; }这段代码的执行顺序与递归版严格对应。以 n=2 为例:初始任务 {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 核对输出。这个习惯起源于某次答辩被问「递归会不会栈溢出」,当时只答得上「会吧」,回去就把非递归版本补上了。以后你拿到这份源码,建议也先完整跑一轮对照,再把运行结果贴进报告,希望帮到你。
本文还有配套的精品资源,点击获取