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

news2024/10/12 19:55:19

汉诺塔问题描述

  • 问题:有三根柱子(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 个盘子)是不可行的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2208655.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leetcode 在排序数组中查找元素的第一个和最后一个位置

这段代码的目的是在一个有序的数组中查找目标元素的第一个和最后一个位置。如果目标元素不存在&#xff0c;返回 [-1, -1]。算法要求时间复杂度为 O(log n)&#xff0c;所以使用了二分查找的思想。 主要思路&#xff1a; 使用两次二分查找&#xff1a; 第一次二分查找用于找到…

《OpenCV计算机视觉》—— 人脸识别

识别图片如下&#xff1a; 完整代码&#xff1a; import cv2image cv2.imread(face.png) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) """ 加载分类器 """ faceCascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml) &…

UE5 使用Animation Budget Allocator优化角色动画性能

Animation Budget Allocator是UE内置插件&#xff0c;通过锁定动画系统所占CPU的预算&#xff0c;在到达预算计算量时对动画进行限制与优化。 开启Animation Budget Allocator需要让蒙皮Mesh使用特定的组件&#xff0c;并进行一些编辑器设置即可开启。 1.开启Animation Budget…

浏览器指纹原理及技术实现探索

文章目录 [TOC](文章目录) 前言一、什么是浏览器指纹&#xff1f;二、浏览器指纹的作用三、 浏览器指纹如何保证唯一性四、浏览器指纹的隐私问题五、如何应对浏览器指纹&#xff1f;六、目前常用的技术方案七、技术实现探索1、简易方式2、fingerprintjs2方式 八、总结 前言 在…

国家药包材标准数据库在线查询方法<实用篇>

从业于医药相关的包材行业这么多年&#xff0c;对于许多医药行业的生产企业、药品检验机构、药品注册申请人以及医疗机构的朋友而言&#xff0c;查询国家药包材标准是他们日常工作的一部分&#xff0c;相对容易。然而&#xff0c;对于那些刚进入这个行业的新手或者普通大众来说…

【测试】自动化——常用函数

元素的定位 查找元素&#xff1a;find_element(方式&#xff0c;“元素”)&#xff0c;表示查找一个元素&#xff1b; find_element(方式&#xff0c;“元素”)&#xff0c;表示查找多个元素。 ###使用查找多个元素 ChromeInsChromeDriverManager().install() driverwebdriv…

uniapp学习(005-2 详解Part.2)

零基础入门uniapp Vue3组合式API版本到咸虾米壁纸项目实战&#xff0c;开发打包微信小程序、抖音小程序、H5、安卓APP客户端等 总时长 23:40:00 共116P 此文章包含第41p-第p51的内容 文章目录 mainifest.json文件配置获取微信小程序appid注册微信小程序微信小程序控制台图形界…

Linux 命令:每日一学,文件查找之find命令实践

[ 知识是人生的灯塔&#xff0c;只有不断学习&#xff0c;才能照亮前行的道路 ] 0x00 前言简述 描述&#xff1a;前面我们一些学习了Linux文件内容查看、分隔列、排序、统计等命令&#xff0c;相信认真学习实践过的看友都已经初步掌握了吧&#xff0c;今天我们继续学习下Linux中…

本地生活服务项目入局方案解析!本地生活服务商系统能实现怎样的作业效果?

当前&#xff0c;各大平台的本地生活服务业务日渐兴盛&#xff0c;提高创业者入局意向的同时&#xff0c;也让本地生活服务项目有哪些等问题也成为了多个创业者社群中的热议对象。而从目前的讨论情况来看&#xff0c;在创业者们所询问的众多本地生活服务项目中&#xff0c;通过…

从二维到三维,电商行业有哪些变化?

从二维到三维&#xff0c;电商行业经历了一系列显著的变化&#xff0c;这些变化不仅体现在商品展示的方式上&#xff0c;还深刻影响了消费者的购物体验、电商平台的运营策略以及整个电商行业的竞争格局。 一、商品展示方式的变革 二维展示阶段&#xff1a; 在电商行业的早期&…

一键安装与配置Stable Diffusion,轻松实现AI绘画

随着技术的迭代&#xff0c;目前 Stable Diffusion 已经能够生成非常艺术化的图片了&#xff0c;完全有赶超人类的架势&#xff0c;已经有不少工作被这类服务替代&#xff0c;比如制作一个 logo 图片&#xff0c;画一张虚拟老婆照片&#xff0c;画质堪比相机。 最新 Stable Di…

kubernetes(K8s)学习(一)

本文主要是搭建一个k8s平台&#xff0c;并部署一个springboot的jar包&#xff0c;后续以此作为学习k8s的环境。 1. 搭建k8s集群 网上有很多指导&#xff0c;大家可以在网上搜索一下&#xff0c;比如这个&#xff1a;K8s搭建集群-CSDN博客&#xff0c;本人通过VMware安装3台虚拟…

国外电商系统开发-运维系统操作脚本

查看脚本内容&#xff0c;只需要点击即可&#xff1a; 执行脚本&#xff0c;请点击 点击了下一步后&#xff0c;可以输出脚本参数&#xff0c;当然你可以可以不输入&#xff0c;直接下一步就行&#xff1a; 现在&#xff0c;点击【下一步】执行开始出初始化脚本&#xff1a; …

【力扣刷题实战】(归并排序)合并两个有序数组

大家好&#xff0c;我是小卡皮巴拉 文章目录 目录 力扣题目&#xff1a; 合并两个有序数组 题目描述 示例 1&#xff1a; 示例 2&#xff1a; 示例 3&#xff1a; 解题思路 具体思路 题目要点 作图助解 完整代码&#xff08;C语言&#xff09; 兄弟们共勉 &#…

Linux多任务编程(网络编程-数据库篇)

前言 本文记录嵌入式领域用的小型数据库 sqlite数据库&#xff0c;以及c语言中使用sqlite3。 数据库 数据存储方式&#xff08;3种&#xff09; &#xff08;1&#xff09;直接地址存储&#xff1a;单片机的烧写&#xff1b; &#xff08;2&#xff09;文件存储&…

接口多继承与子类继承多接口时的冲突问题,方法冲突与变量冲突.....

&#x1f680; 个人简介&#xff1a;某大型国企资深软件开发工程师&#xff0c;信息系统项目管理师、CSDN优质创作者、阿里云专家博主&#xff0c;华为云云享专家&#xff0c;分享前端后端相关技术与工作常见问题~ &#x1f49f; 作 者&#xff1a;码喽的自我修养&#x1f9…

IDEA2024最新版本运行Web应用时 Tomcat 日志中的中文乱码问题修复解决

一、IDEA2024运行Tomcat日志中的中文乱码问题修复 在使用tomcat的时候经常遇到乱码问题&#xff0c;要么是控制台输出乱码或者输出日志乱码&#xff0c;要么页面接收乱码&#xff0c;产生乱码的根本原因就是编码和解码不一致。网上有的文章写得也有问题&#xff0c;今天自己多看…

计算机毕业设计 | springboot商城售后管理系统 购物平台(附源码)

1&#xff0c;绪论 1.1 开发背景 在数字化时代的推动下&#xff0c;产品售后服务管理机构面临着信息化和网络化的挑战。传统的手工管理和纸质档案已经无法满足管理人员和读者的需求。为了提高产品售后服务管理机构的管理效率和服务质量&#xff0c;开发和实现一个基于Java的售…

轻量服务器和云服务器ecs哪个好用一些?

轻量服务器和云服务器ecs哪个好用一些&#xff1f;轻量服务器与云服务器ECS在多方面存在显著差异&#xff0c;对于需要高性能计算和大规模数据处理的用户来说&#xff0c;ECS可能是更好的选择&#xff1b;而对于预算有限且需求较为简单的用户来说&#xff0c;轻量服务器可能更为…

计算机网络:数据链路层 —— 可靠传输服务

文章目录 可靠传输停止-等待 (SW) 协议超时重传机制分组编号机制ACK 丢失问题ACK 延迟问题 注意事项信道利用率 回退 N 帧 (GBN) 协议滑动窗口信道利用率无传输差错超时重传、回退N帧 累计确认 选择重传 (SR) 协议滑动窗口 可靠传输 若数据链路层向其上层提供的服务类型为可靠…