汉诺塔问题递归与非递归实现

汉诺塔问题描述

  • 问题:有三根柱子(A、B、C)和若干个不同大小的盘子,最初所有盘子都在柱子 A 上,按大小顺序从上到下排列。目标是将所有盘子移动到柱子 C 上,遵循以下规则:
    1. 每次只能移动一个盘子。
    2. 不能将较大的盘子放在较小的盘子上。
    3. 需要使用辅助柱子 B。

递归解决方案

汉诺塔问题的解决方案可以通过递归来实现,具体步骤如下:

  1. 基本情况:如果只有一个盘子,直接将其从源柱子 A 移动到目标柱子 C。
  2. 递归步骤
    • 将上面的 n−1个盘子从 A 移动到辅助柱子 B(使用 C 作为辅助)。
    • 将第 n个盘子直接从 A 移动到 C。
    • 将 n−1个盘子从 B 移动到 C(使用 A 作为辅助)。
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

// 汉诺塔递归函数
void hanoi(int n, char A, char B, char C)
{
    if (n == 1)
    {
        printf("将盘子 1 从 %c 移动到 %c\n", A, C);
        return;
    }

    // 将 n-1 个盘子从 A 移动到 B,使用 C 作为辅助
    hanoi(n - 1, A, C, B);

    // 将第 n 个盘子从 A 移动到 C
    printf("将盘子 %d 从 %c 移动到 %c\n", n, A, C);

    // 将 n-1 个盘子从 B 移动到 C,使用 A 作为辅助
    hanoi(n - 1, B, A, C);
}

int main()
{
    int n;
    printf("请输入盘子个数:");
    scanf("%d", &n);

    // 从 A 移动到 C,使用 B 作为辅助
    hanoi(n, 'A', 'B', 'C');

    return 0;
}

调用过程示例

假设我们有 3 个盘子,调用 hanoi(3, 'A', 'B', 'C')

hanoi(3, A, B, C)
├── hanoi(2, A, C, B)
│   ├── hanoi(1, A, B, C)
│   │   └── 打印: 将盘子 1 从 A 移动到 C
│   ├── 打印: 将盘子 2 从 A 移动到 B
│   └── hanoi(1, C, A, B)
│       └── 打印: 将盘子 1 从 C 移动到 B
├── 打印: 将盘子 3 从 A 移动到 C
└── hanoi(2, B, A, C)
    ├── hanoi(1, B, C, A)
    │   └── 打印: 将盘子 1 从 B 移动到 A
    ├── 打印: 将盘子 2 从 B 移动到 C
    └── hanoi(1, A, B, C)
        └── 打印: 将盘子 1 从 A 移动到 C

输出:

请输入盘子个数:3
将盘子 1 从 A 移动到 C
将盘子 2 从 A 移动到 B
将盘子 1 从 C 移动到 B
将盘子 3 从 A 移动到 C
将盘子 1 从 B 移动到 A
将盘子 2 从 B 移动到 C
将盘子 1 从 A 移动到 C

非递归解决方案

虽然递归是直观的,但在某些情况下,非递归解决方案可能更高效。

对于 n个盘子的汉诺塔问题,移动盘子最少次数为 2^n−1。

汉诺塔移动的基本规则

在汉诺塔中,盘子只能一个一个地移动,且较大的盘子不能放在较小的盘子上。

  1. 目标:将所有盘子从源柱子(A)移动到目标柱子(C),使用辅助柱子(B)。
  2. 移动的奇偶性:移动的顺序依赖于盘子的数量 n 的奇偶性。
     
  3. 偶数盘子:当盘子的数量为偶数时,交换辅助柱子 B 和目标柱子 C 是必要的。这是因为在偶数情况下,最后一个盘子的移动需要在辅助柱子和目标柱子之间进行调整。

  4. 奇数盘子:当盘子的数量为奇数时,移动的顺序则是直接按照汉诺塔的标准规则进行,不需要交换柱子。在这种情况下,移动的顺序会自然按照规则完成,确保每个盘子都能正确地从源柱子 A 移动到目标柱子 C,而无需进行柱子的交换。

如果盘子数量为偶数,交换辅助柱子 B 和目标柱子 C,以确保移动顺序正确。

在汉诺塔问题中,有三个柱子(A、B、C),需要将盘子从源柱子移动到目标柱子。非递归的实现中,可以通过循环来控制移动的顺序。这里的关键在于使用 % 运算符来决定每一步的移动。

代码解析

// 确定源柱子与目标柱子
if (i % 3 == 1) 
{
    from = A; to = C; // 第一步:A → C
}
else if (i % 3 == 2) 
{
    from = A; to = B; // 第二步:A → B
}
else 
{
    from = B; to = C; // 第三步:B → C
}

这里第三步看着像错误的,其实不是,因为只是赋值并没移动,后面这是在栈保证大小顺序的情况下才进行移动的。

逻辑解释

  1. i 的角色

    • i 是当前的移动步骤,从 1 到 2^n−1。
    • 每一个步骤代表着一个盘子的移动。
  2. i % 3 的使用

    • 使用 % 运算符是为了实现循环和定期的移动模式。结果可能是 0、1 或 2。
  3. 具体移动

    • i % 3 == 1
      • i 能被 3 取余为 1 时,这表示是第一个移动。
      • 将盘子从柱子 A 移动到柱子 C。
      • 这是因为在第 1 步,最小的盘子应该从源柱子直接移动到目标柱子。
    • i % 3 == 2
      • i 能被 3 取余为 2 时,表示是第二个移动。
      • 将盘子从柱子 A 移动到柱子 B。
      • 这通常是将一个较大的盘子暂时放到辅助柱子,以便后续的移动操作。
    • i % 3 == 0
      • i 能被 3 取余为 0 时,表示是第三个移动。
      • 将盘子从柱子 B 移动到柱子 C。
      • 这一步通常是将之前放在辅助柱子上的盘子移动到目标柱子。
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

// 模拟栈结构
typedef struct
{
    int data[64]; // 最多支持64个盘子
    int top;      // 栈顶指针
} Stack;

// 初始化栈
void initStack(Stack* s)
{
    s->top = -1; // 栈顶指针初始化为-1,表示栈为空
}

// 入栈操作
void push(Stack* s, int x)
{
    s->data[++s->top] = x; // 将元素x压入栈中,并更新栈顶指针
}

// 出栈操作
int pop(Stack* s)
{
    return s->data[s->top--]; // 返回栈顶元素,并更新栈顶指针
}

// 判断栈是否为空
int isEmpty(Stack* s)
{
    return s->top == -1; // 如果栈顶指针为-1,表示栈为空
}

// 获取栈顶元素
int getTop(Stack* s)
{
    return s->data[s->top]; // 返回栈顶元素
}

// 汉诺塔非递归实现
void hanoi_iterative(int n, char A, char B, char C)
{
    Stack a, b, c; // 定义三个栈,分别表示柱子A、B、C
    initStack(&a); // 初始化栈A
    initStack(&b); // 初始化栈B
    initStack(&c); // 初始化栈C

    // 初始化A柱子上的盘子,从大到小压入栈中
    for (int i = n; i >= 1; i--)
    {
        push(&a, i);
    }

    int total_moves = (1 << n) - 1; // 计算总移动次数,2^n - 1

    // 如果盘子数量为偶数,交换B和C柱子
    if (n % 2 == 0)
    {
        char temp = B; // 临时变量存储B
        B = C;         // B赋值为C
        C = temp;     // C赋值为B
    }

    // 遍历所有移动操作
    for (int i = 1; i <= total_moves; i++)
    {
        char from, to;

        // 确定源柱子与目标柱子
        if (i % 3 == 1)
        {
            from = A; to = C; // 第一步:A → C
        }
        else if (i % 3 == 2)
        {
            from = A; to = B; // 第二步:A → B
        }
        else
        {
            from = B; to = C; // 第三步:B → C
        }

        // 确定对应的栈
        Stack* fromStack, * toStack;
        if (from == A) fromStack = &a; // 根据源柱子选择栈
        else if (from == B) fromStack = &b;
        else fromStack = &c;

        if (to == A) toStack = &a; // 根据目标柱子选择栈
        else if (to == B) toStack = &b;
        else toStack = &c;

        // 确保从小盘子移动到大盘子
        if (!isEmpty(fromStack) && (isEmpty(toStack) || getTop(fromStack) < getTop(toStack)))
        {
            // 从源柱子移动到目标柱子
            int disk = pop(fromStack); // 从源栈中弹出盘子
            push(toStack, disk);       // 将盘子压入目标栈
            printf("将盘子%d从 %c 移动到 %c\n", disk, from, to);
        }
        else
        {
            // 如果目标柱子有盘子,反向移动
            int disk = pop(toStack); // 从目标栈中弹出盘子
            push(fromStack, disk);   // 将盘子压入源栈
            printf("将盘子%d从 %c 移动到 %c\n", disk, to, from);
        }
    }
}

int main()
{
    int n;
    printf("请输入盘子个数:");
    scanf("%d", &n); // 用户输入盘子数量

    hanoi_iterative(n, 'A', 'B', 'C'); // 调用非递归汉诺塔函数

    return 0; // 返回0,表示程序成功结束
}

输出:

请输入盘子个数:3
将盘子1从 A 移动到 C
将盘子2从 A 移动到 B
将盘子1从 C 移动到 B
将盘子3从 A 移动到 C
将盘子1从 B 移动到 A
将盘子2从 B 移动到 C
将盘子1从 A 移动到 C

跟上面递归的输出一样

总结:

递归实现:

特点
  • 简洁性:代码简洁易读,直接表达了问题的递归性质。
  • 直观:递归调用自然地描述了移动过程。
优点
  • 逻辑清晰,易于理解和实现。
  • 适合较小规模的盘子。
缺点
  • 对于较大的盘子,递归深度可能导致栈溢出。
  • 空间复杂度较高,主要是由于函数调用栈。

非递归实现

特点
  • 使用栈:通过栈模拟柱子的行为,实现非递归的移动。
  • 循环控制:使用循环而非递归,避免了栈溢出的问题。
优点
  • 能处理更大规模的盘子,避免了递归深度限制。
  • 空间复杂度更低。
缺点
  • 代码相对复杂,需要手动管理栈的状态。
  • 理解上可能不如递归直观。

汉诺塔问题的时间复杂度

        汉诺塔问题的时间复杂度是 指数级 的,具体为 O(2^n),这意味着随着盘子数量 n 的增加,所需的时间将指数级增加。这也是为什么在实际应用中,处理较大数量的盘子(如 64 个盘子)是不可行的。

### 汉诺塔问题的解决方法 #### 递归算法解析 汉诺塔问题是计算机科学中用于展示递归算法的经典案例之一[^1]。该问题的核心在于将一系列不同大小的圆盘从一根柱子移动到另一根柱子上,在此过程中遵循特定规则:每次只允许移动一个圆盘;任何时候都不能将较大的圆盘放置于较小的圆盘之上。 对于给定数量`n`个圆盘,解决问题的关键思路是将其视为更简单的子问题组合而成。具体来说: - 当只有一个圆盘时(`n=1`),只需直接将其移至目标位置即可完成操作; - 对于多于一个圆盘的情形,则需先将顶部`n-1`个小圆盘临时转移到第三根柱子作为过渡区,之后单独处理最底部的最大圆盘,并最终再次利用剩余空间来安置先前转移出去的小圆盘群组[^2]。 这种逐步拆解并重组的方式体现了分而治之的思想,即通过不断细分原始难题直至触及基础情形——这正是递归的本质特征所在。 以下是基于上述逻辑编写的Python版本递归函数实现: ```python def hanoi(n, source='A', auxiliary='B', target='C'): if n == 1: print(f'Move disk from {source} to {target}') return # Move top n-1 disks from source to auxiliary using target as temporary storage. hanoi(n - 1, source, target, auxiliary) # Move the nth largest disk directly from source to target. print(f'Move disk from {source} to {target}') # Finally move all previously moved n-1 disks back on top of this one at target location, # now treating original source peg as new temp space since it's empty again after step above. hanoi(n - 1, auxiliary, source, target) ``` 这段代码清晰地展示了如何按照前述原则执行每一步骤,从而成功解决了任意规模下的汉诺塔挑战。 #### 数学特性观察 值得注意的是,随着圆盘数目增加,所需总步数呈现出指数级增长的趋势。事实上,当有`n`个圆盘时,总共需要\(2^n - 1\)次基本动作才能完全迁移完毕[^4]。这一规律可以从实际模拟过程中的模式归纳得出,同时也反映了递归结构下计算量随输入尺寸扩大的特点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值