汉诺塔 - 递归的优雅与传说
趣味点:
这是一个古老的印度传说,世界中心有一根宝石柱,上面套着64个大小不同的金环,僧侣们按照“一次只能移动一个金环,且大盘不能放在小盘上”的规则,日夜不停地移动金环,传说当所有金环都移到另一根柱子上时,世界就会毁灭,这个算法完美地展示了递归思想的简洁和威力,其步骤数量是 2^64 - 1,是一个天文数字。

问题: 有三根柱子(A, B, C),开始时A柱上有n个大小不一的盘子,大盘在下,小盘在上,目标是将所有盘子移动到C柱上,移动时必须遵守以下规则:
- 一次只能移动一个盘子。
- 每次移动都是将柱子最上面的盘子移到另一根柱子的最上面。
- 大盘子不能放在小盘子上面。
C语言实现:
#include <stdio.h>
// 函数声明
void hanoi(int n, char from_rod, char to_rod, char aux_rod);
int main() {
int n = 4; // 假设有4个盘子
printf("汉诺塔移动步骤 (n=%d):\n", n);
hanoi(n, 'A', 'C', 'B'); // 将n个盘子从A柱移到C柱,B柱作为辅助
return 0;
}
/**
* @brief 汉诺塔递归函数
* @param n 要移动的盘子数量
* @param from_rod 源柱子
* @param to_rod 目标柱子
* @param aux_rod 辅助柱子
*/
void hanoi(int n, char from_rod, char to_rod, char aux_rod) {
// 基本情况:如果只有一个盘子,直接移动
if (n == 1) {
printf("将盘子 1 从 %c 移动到 %c\n", from_rod, to_rod);
return;
}
// 递归步骤:
// 1. 将上面的 n-1 个盘子从 from_rod 移动到 aux_rod (使用 to_rod 作为辅助)
hanoi(n - 1, from_rod, aux_rod, to_rod);
// 2. 将最底下的第 n 个盘子从 from_rod 移动到 to_rod
printf("将盘子 %d 从 %c 移动到 %c\n", n, from_rod, to_rod);
// 3. 将刚才移到 aux_rod 上的 n-1 个盘子,从 aux_rod 移动到 to_rod (使用 from_rod 作为辅助)
hanoi(n - 1, aux_rod, to_rod, from_rod);
}
妙趣解析:
这个算法的精髓在于“大事化小,小事化了”,移动 n 个盘子的问题被巧妙地分解成了三个更小的子问题:
- 移动
n-1个盘子。 - 移动
1个盘子。 - 再次移动
n-1个盘子。
这个过程不断递归,直到问题简单到可以直接解决(n=1),代码的逻辑和问题的描述几乎一模一样,这就是递归的魅力所在。
生命游戏 - 简单规则下的复杂涌现
趣味点: 由英国数学家约翰·康威发明,是一个“零玩家游戏”,你只需要设定一个初始状态,然后游戏就会按照简单的规则自行演化,展现出无法预测的、复杂而美丽的模式,从滑翔机到振荡器,再到能自我复制的“滑翔机枪”,它完美地展示了“复杂源于简单”的哲学思想。
问题: 在一个无限的二维网格上,每个格子有两种状态:“存活”或“死亡”,每个格子都与其周围的8个邻居互动,演化规则如下:
- 存活规则: 任何存活的细胞,如果活邻居少于2个(孤独)或多于3个(过度拥挤),它都会在下一代死亡。
- 死亡规则: 任何死亡的细胞,如果恰好有3个活邻居,它会在下一代复活(模拟繁殖)。
C语言实现: 为了在计算机中模拟,我们需要一个有限的网格。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h> // for sleep()
#define WIDTH 40
#define HEIGHT 20
#define GENERATIONS 100
// 打印网格
void print_grid(bool grid[HEIGHT][WIDTH]) {
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
printf(grid[i][j] ? "■ " : "· ");
}
printf("\n");
}
printf("\n");
}
// 计算邻居存活数量
int count_neighbors(bool grid[HEIGHT][WIDTH], int x, int y) {
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0) continue; // 跳过自身
int nx = (x + i + HEIGHT) % HEIGHT; // 使用模运算实现边界环绕
int ny = (y + j + WIDTH) % WIDTH;
if (grid[nx][ny]) count++;
}
}
return count;
}
void life_game() {
bool current[HEIGHT][WIDTH] = {0};
bool next[HEIGHT][WIDTH] = {0};
// --- 初始化一个有趣的模式 ("滑翔机") ---
// 滑翔机
int glider_x = 1, glider_y = 1;
current[glider_x][glider_y + 1] = 1;
current[glider_x + 1][glider_y + 2] = 1;
current[glider_x + 2][glider_y] = 1;
current[glider_x + 2][glider_y + 1] = 1;
current[glider_x + 2][glider_y + 2] = 1;
// 振荡器
int blinker_x = 10, blinker_y = 10;
current[blinker_x][blinker_y] = 1;
current[blinker_x][blinker_y + 1] = 1;
current[blinker_x][blinker_y + 2] = 1;
printf("初始状态:\n");
print_grid(current);
for (int gen = 0; gen < GENERATIONS; gen++) {
// 1. 计算下一代状态
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
int neighbors = count_neighbors(current, i, j);
if (current[i][j]) {
// 存活规则
next[i][j] = (neighbors == 2 || neighbors == 3);
} else {
// 死亡/复活规则
next[i][j] = (neighbors == 3);
}
}
}
// 2. 更新网格
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
current[i][j] = next[i][j];
}
}
// 3. 打印并暂停
printf("第 %d 代:\n", gen + 1);
print_grid(current);
sleep(1); // 暂停1秒,方便观察
}
}
int main() {
life_game();
return 0;
}
妙趣解析: 生命游戏的魅力在于其涌现性,简单的局部规则(只看邻居)导致了复杂的全局行为,你输入一堆随机或有序的“活细胞”,观察它们如何演化、碰撞、形成稳定的结构或周期性的振荡,这就像我们宇宙的规律,简单的物理定律演化出了生命和智慧,在C语言中实现它,需要小心处理边界条件(这里用了环绕边界),并确保在计算下一代时,不能直接修改当前正在计算的网格。
约瑟夫环 - 数学与生存游戏
趣味点:
一个著名的古罗马传说故事,一群士兵被包围,决定通过一种自杀式的方式来决定谁去投降,他们排成一个圈,从某个指定的人开始报数,数到 k 的那个人就自杀,然后从下一个人重新开始报数,直到所有人都自杀,问题是:给定了 n 和 k,你站哪个位置才能活到最后?
问题:
n 个人围成一圈,编号从 0 到 n-1,从第 0 个人开始报数,数到 k-1 的那个人出局,然后从下一个人重新开始报数,直到所有人都出局,求最后剩下的人的初始编号。
C语言实现: 这个问题可以用递归和循环两种方式解决,循环解法更高效。
#include <stdio.h>
#include <stdlib.h>
/**
* @brief 使用循环解决约瑟夫环问题
* @param n 总人数
* @param k 报数的步长
* @return 最后剩下的人的初始编号
*/
int josephus(int n, int k) {
int winner = 0; // 当 n=1 时,胜利者是 0
// 从 2 个人开始,递推到 n 个人
for (int i = 2; i <= n; i++) {
winner = (winner + k) % i;
}
return winner;
}
int main() {
int n, k;
printf("请输入总人数 n: ");
scanf("%d", &n);
printf("请输入报数步长 k: ");
scanf("%d", &k);
int survivor = josephus(n, k);
printf("在 %d 个人中,每次数到 %d 的人,最后活下来的人是初始编号为 %d 的那个人,\n", n, k, survivor);
// 一个有趣的例子
printf("\n一个经典的例子:41个人,数到3的人出局,最后是第 %d 号活下来,\n", josephus(41, 3));
return 0;
}
妙趣解析:
这个算法的解法非常巧妙,它不是去模拟每一次淘汰,而是通过数学递推直接找到答案。f(n, k) 表示 n 个人时胜利者的位置,它的递推关系是 f(n, k) = (f(n-1, k) + k) % n。
- 含义: 当
n-1个人时,胜利者的位置是f(n-1, k),现在我们增加了一个人(变成了n个人),相当于整个序列向后移动了k个位置,所以新的胜利者位置是f(n-1, k) + k,因为是环,所以要取模n。 - 基础: 当只有1个人时 (
n=1),胜利者显然是位置0。 代码中的for循环就是从基础开始,一步步计算出n个人的结果,这种不模拟过程而直接计算结果的思路,是算法优化的一个典范。
这三个算法各有千秋:
- 汉诺塔 展示了递归的优雅与力量,将复杂问题分解为同类子问题。
- 生命游戏 展示了简单规则如何催生复杂系统,是涌现性的绝佳例子。
- 约瑟夫环 展示了数学建模和递推思想的威力,让我们跳脱出模拟的束缚。
希望这些例子能让你感受到算法的魅力!算法不仅仅是解决编程问题的工具,更是逻辑思维、数学之美和创造力的结晶。
