汉诺塔问题

文章详细介绍了汉诺塔问题的起源、递归解决方案以及迭代解决方案。通过递归函数和栈操作,分别展示了如何使用递归和迭代方法移动圆盘,同时分析了两种方法的时间复杂度和空间复杂度,指出递归法空间效率更高,而迭代法直观但占用更多空间。
摘要由CSDN通过智能技术生成

汉诺塔问题

起源

  汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

  汉诺塔问题是一个经典的递归问题,最初由法国数学家 Edouard Lucas 提出。问题描述如下:

  有三根柱子,分别为 A、B、C,A 柱子上有 n 个盘子,盘子大小不一,大盘子在下,小盘子在上。现在需要将 A 柱子上的所有盘子移动到 C 柱子上,每次只能移动一个盘子,且大盘子不能放在小盘子上面。在移动过程中可以使用 B 柱子作为中转站。

分析

  接下来将从以下部分对汉诺塔问题进行分析:

  1. 自顶向下,逐渐求精:
      汉诺塔问题的解决方式是自顶向下逐渐求精。首先,我们需要将最上面的圆盘移动到第三个柱子上,然后将剩余的圆盘移动到第二个柱子上,最后将最上面的圆盘移动到第二个柱子上。因此,这个问题可以分解为三个子问题,即将 n-1 个圆盘从第一个柱子移动到第二个柱子,将第 n 个圆盘从第一个柱子移动到第三个柱子,再将 n-1 个圆盘从第二个柱子移动到第三个柱子。这三个子问题可以分别递归解决,最终得到整个问题的解。

  2. 函数调用、递归和分治:
      为了解决汉诺塔问题,我们需要使用函数调用、递归和分治。具体来说,我们可以编写一个函数 hanoi(n, a, b, c),其中 n 表示要移动的圆盘数,a、b 和 c 分别表示三个柱子。该函数首先检查 n 的值,如果 n == 1,则直接将圆盘从柱子 a 移动到柱子 c;否则,该函数会将问题分解为三个子问题,并递归调用 hanoi() 函数来解决这些子问题。

  3. 形参与实参:
      在函数调用过程中,我们使用形参来表示函数的输入参数,而实参则是在函数调用时提供的具体数值。在汉诺塔问题中,形参 n、a、b 和 c 分别表示要移动的圆盘数和三个柱子。在函数调用时,我们需要提供实际的圆盘数和三个柱子的具体名称,如 hanoi(3, ‘A’, ‘B’, ‘C’)。

  4. 有意义、规范的标识符:
      在编写程序时,使用有意义、规范的标识符可以使代码更易于理解和维护。在汉诺塔问题中,我们可以使用有意义的变量名和函数名,如 n、a、b、c 和 hanoi()。这些标识符可以更好地表达程序的含义,并提高代码的可读性。

  5. 时间复杂度:
      汉诺塔问题的时间复杂度为 O(2^n),其中 n 是圆盘的数量。这是因为在每次递归调用中,我们需要移动 2 个圆盘。此外,在解决汉诺塔问题的过程中,我们需要进行 n 次递归调用,每次递归调用需要进行 3 次移动操作。因此,总的移动次数为 3^(n - 1),这是一个指数级别的复杂度。在实际应用中,当圆盘数量较大时,解决汉诺塔问题的时间复杂度会变得非常高。

  6. 递归栈:
      递归函数的调用过程需要使用递归栈来保存每个函数调用的状态。在汉诺塔问题中,每个递归调用会创建一个新的函数调用帧,并将其压入递归栈中。当递归调用返回时,对应的函数调用帧会被弹出,恢复到上一层调用的状态。因此,在解决汉诺塔问题的过程中,递归栈的深度等于递归调用的层数,即 n。

  7. 空间复杂度:
      汉诺塔问题的空间复杂度为 O(n),其中 n 是圆盘的数量。这是因为在解决问题的过程中,我们需要使用一个递归栈来保存每个递归调用的状态。由于递归栈的深度等于递归调用的层数,因此空间复杂度为 O(n)。除了递归栈之外,我们还需要使用一些额外的变量来保存问题的状态,但这些变量的数量与圆盘的数量无关,因此不会影响空间复杂度。

  总之,汉诺塔问题涉及到自顶向下的逐渐求精、函数调用、递归和分治。通过合理地使用形参和实参、有意义、规范的标识符和递归栈,我们可以解决这个问题,并计算出它的时间复杂度和空间复杂度。

递归法

 解决该问题的思路需要用到一种比较重要的算法:递归算法(即函数自己调用自己本身)

  1. 当 n = 1 时,直接将盘子从 A 柱子上移到 C 柱子上即可。

  2. 当 n > 1 时,将问题分解为三个子问题:

    • 将 A 柱子上的前 n-1 个盘子移到 B 柱子上;

    • 将 A 柱子上的最后一个盘子移到 C 柱子上;

    • 将 B 柱子上的 n-1 个盘子移到 C 柱子上。

  3. 对于子问题 1 和 3,可以采用同样的方法进行递归求解,直到 n=1 时停止递归。

过程如图:
在这里插入图片描述

代码实现

#include <stdio.h>
void hanoi(int n,char a,char b,char c); //解决汉诺塔问题的主体函数
int count;//定义一个全局变量,记录挪动碟片所需要的总步数
int main(){
    int n;//汉诺塔碟片的数量
    printf("请输入碟片数量:\n");
    scanf("%d",&n);
    hanoi(n,'a','b','c');
    printf("移动共需%d步\n",count);
    return 0;
}
void hanoi(int n,char a,char b,char c){
    count++; //每调用一次就相当于挪动一次
    if(n==1){ //将最后一块碟片从a柱移向c柱
        printf("将第%d块碟片从%c柱移向%c柱\n",n,a,c);
    }else {
        hanoi(n - 1, a, c, b);//将上面n-1块碟片从a借助c移动到b柱上(递归进行)
        printf("将第%d块碟片从%c柱移向%c柱\n", n, a, c); //将第n块碟片从a直接移动到c柱上
        hanoi(n - 1, b, a, c);//将上面n-1块碟片从b借助a移动到c柱上(递归进行)
    }
}

运行结果

在这里插入图片描述

迭代栈

  首先定义了一个栈结构,包括栈顶指针top和存储数据的数组data,并定义了一些栈的基本操作,如初始化栈、判断栈是否为空(isEmpty())、入栈(push())、出栈(pop())等。
  接下来,我们定义了一个hanoi()函数,用于迭代实现汉诺塔问题。在该函数中,我们首先初始化三个栈s1、s2、s3,并将所有盘子入栈s1中。然后根据盘子数量的奇偶性,交换to(目标柱)和aux(过渡柱)的位置。
  然后,我们使用一个循环来迭代移动盘子,该循环中的变量i从1到(1 << n) - 1遍历。每次循环中,根据i的值模3的结果来决定移动哪个盘子。具体地,当i%3==1时,从s1移动盘子到s2或从s2移动盘子到s1;当i%3==2时,从s1移动盘子到s3或从s3移动盘子到s1;当i%3==0时,从s2移动盘子到s3或从s3移动盘子到s2。
  在每次移动盘子时,都输出一条移动信息。
  最后,在main函数中,我们从用户处输入盘子的数量n,并调用hanoi函数解决汉诺塔问题。

代码实现

#include <stdio.h>
#include <stdlib.h>

#define MAXSIZE 100

// 定义栈结构
typedef struct {
    int top;
    int data[MAXSIZE];
} Stack;

// 初始化栈
void init(Stack *s) {
    s->top = -1;
}

// 判断栈是否为空
int isEmpty(Stack *s) {
    return s->top == -1;
}

// 判断栈是否已满
int isFull(Stack *s) {
    return s->top == MAXSIZE - 1;
}

// 入栈
void push(Stack *s, int x) {
    if (isFull(s)) {
        printf("栈已满!无法添加!\n");
        return ;
    }
    s->data[++s->top] = x;
}

// 出栈
int pop(Stack *s) {
    if (isEmpty(s)) {
        printf("栈为空!\n");
        return -1;
    }
    return s->data[s->top--];
}

// 迭代实现汉诺塔
void hanoi(int n, char from, char to, char aux) {//from为起始柱,to为目标柱,aux为过渡柱
    Stack s1, s2, s3;
    int i;
    // 初始化三个栈
    init(&s1);
    init(&s2);
    init(&s3);
    // 先将所有盘子入栈s1
    for (i = n; i > 0; i--) {
        push(&s1, i);
    }
    // 如果盘子数量为偶数,交换to和aux的位置
    if (n % 2 == 0) {
        char temp = to;
        to = aux;
        aux = temp;
    }
    // 迭代移动盘子
    for (i = 1; i <= (1 << n) - 1; i++) {
        if (i % 3 == 1) {
            if (!isEmpty(&s1) && (isEmpty(&s2) || s1.data[s1.top] < s2.data[s2.top])) {
                push(&s2, pop(&s1));
                printf("将第%d个碟子从%c柱移动到%c柱上\n", s2.data[s2.top], from, to);
            }else {
                push(&s1, pop(&s2));
                printf("将第%d个碟子从%c柱移动到%c柱上\n", s1.data[s1.top], to, from);
            }
        }else if (i % 3 == 2) {
            if (!isEmpty(&s1) && (isEmpty(&s3) || s1.data[s1.top] < s3.data[s3.top])) {
                push(&s3, pop(&s1));
                printf("将第%d个碟子从%c柱移动到%c柱上\n", s3.data[s3.top], from, aux);
            }else {
                push(&s1, pop(&s3));
                printf("将第%d个碟子从%c柱移动到%c柱上\n", s1.data[s1.top], aux, from);
            }
        }else {
            if (!isEmpty(&s2) && (isEmpty(&s3) || s2.data[s2.top] < s3.data[s3.top])) {
                push(&s3, pop(&s2));
                printf("将第%d个碟子从%c柱移动到%c柱上\n", s3.data[s3.top], to, aux);
            }else {
                push(&s2, pop(&s3));
                printf("将第%d个碟子从%c柱移动到%c柱上\n", s2.data[s2.top], aux, to);
            }
        }
    }
}
int main() {
    int n;
    printf("请输入需要移动碟子的数量:\n");
    scanf("%d", &n);

    hanoi(n, 'A', 'C', 'B');

    return 0;
}

运行结果

在这里插入图片描述

  需要注意的是,使用迭代方法解决汉诺塔问题需要使用三个栈,而递归方法只需要一个栈,因此空间复杂度较高,但时间复杂度和递归方法相同,均为O(2^n)。

总结

  汉诺塔问题具有重要的算法思想和应用价值。在计算机科学中,汉诺塔问题经常被用作算法设计和分析的基础问题,可以帮助我们理解递归、分治和栈等基本概念和算法技术。此外,汉诺塔问题也被广泛应用于编译器、操作系统、数据库等计算机领域中的算法设计和优化,具有重要的实际意义。

  • 34
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值