PTA 汉诺塔的非递归实现

本文探讨了汉诺塔问题的非递归解决方法,提供了三种不同的非递归实现方案,包括使用堆栈模拟递归过程、模拟函数栈工作流程及模拟递归函数的进出栈过程。

PTA 汉诺塔的非递归实现

  • 7-11 汉诺塔的非递归实现 (25分)

借助堆栈以非递归(循环)方式求解汉诺塔的问题(n, a, b, c),即将N个盘子从起始柱(标记为“a”)通过借助柱(标记为“b”)移动到目标柱(标记为“c”),并保证每个移动符合汉诺塔问题的要求。

输入格式:
输入为一个正整数N,即起始柱上的盘数。

输出格式:
每个操作(移动)占一行,按柱1 -> 柱2的格式输出。

输入样例:
3
输出样例:
a -> c
a -> b
c -> b
a -> c
b -> a
b -> c
a -> c

如下面这段代码一样。递归代码往往都十分简洁,分析出递归出口和递归规则以后也比较容易实现,但是每一次的递归调用都需要压栈占用内存,效率不高.所以才需要递归转非递归来提高效率,减轻函数栈的负担。

#include<iostream>
#include<cstdio>

using namespace std;

void move(int n, char x, char y, char z);
//将n个盘子从x借助y移动到z
int main()
{
    int n;
    scanf("%d", &n);
    move(n, 'a', 'b', 'c');
    return 0;
}
void move(int n, char x, char y, char z)
{
    if(n == 1)
        printf("%c -> %c\n",x,z);
    else
    {
        move(n - 1, x, z, y);  //将n-1个盘子从x借助z移动到y上
        printf("%c -> %c\n",x,z);  //将最底下的第n个盘子从x移动到z上
        move(n - 1, y, x, z);  //将n-1个盘子从y借助x移动到z上
    }
}

递归求解汉诺塔问题,能解决问题,但不是我们真正想做的。
非递归求解汉诺塔问题给出以下三种方案。

1.方案一:
这段代码是实现了一位美国学者对于汉诺塔问题提出的解决方案(第一次写不太会写非递归,就去百度翻,翻到了这个自己觉得可以实现的算法),用3个stack栈来模拟三根柱子,用一个长度为3的char数组对应三根柱子的名字(‘a’,‘b’,‘c’),一方面便于输出由哪一根移动到哪一根,另一方面也是由于这种算法会因为盘子数的奇偶来简单调整后两根柱子的顺序。
算法介绍:一位美国学者发现一种出人意料的简单方法,只要轮流进行两步操作就可以了。
若n为偶数,柱子摆放为 A B C;若n为奇数,柱子摆放 A C B。
(1)把圆盘1从现在的柱子移动到下一根柱子,即当n为偶数时,若圆盘1在柱子A,则把它移动到B;若圆盘1在柱子B,则把它移动到C;若圆盘1在柱子C,则把它移动到A。
(2)接着,把另外两根柱子(圆盘1移动前所在的柱子以外的那两根柱子)上可以移动的圆盘移动到新的柱子上。即把非空柱子上的圆盘移动到空柱子上;当两根柱子都非空时,移动较小的圆盘。
(3)反复进行(1)(2)

操作3阶汉诺塔的移动:A→C,A→B,C→B,A→C,B→A,B→C,A→C

#include <iostream>
#include <stack>
#include <cstdio>

using namespace std;

char column_name[3] = {'a', 'b', 'c'};
stack<int> column[3];
int main()
{
    int n, move_count = 0, flag = 2, temp;
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
    {
        column[0].push(n - i);//将原盘从大到小放到第一根柱子上
    }

    if(n % 2 == 1)//如果n是奇数,交换后两根柱子的位置
    {
        char alpha;
        alpha = column_name[1];
        column_name[1] = column_name[2];
        column_name[2] = alpha;
        flag = 1;
    }

    while(true)
    {
        if((int)column[flag].size() != n)//对应算法中的步骤(1),移动圆盘1
        {
            printf("%c -> %c\n", column_name[(move_count) % 3], column_name[(move_count + 1) % 3]);
            temp = column[(move_count) % 3].top();
            column[(move_count) % 3].pop();
            column[(move_count + 1) % 3].push(temp);
        }
        //对应算法中的步骤(2),把另外两根柱子上可以移动的圆盘移动到新的柱子上
        if((!column[(move_count) % 3].empty()) && (!column[(move_count + 2) % 3].empty()))
        {//当两根柱子都非空时,移动较小的圆盘。
            if(column[(move_count) % 3].top() - column[(move_count + 2) % 3].top() > 0  && (int)column[flag].size() != n)
            {
                printf("%c -> %c\n", column_name[(move_count + 2) % 3], column_name[(move_count) % 3]);
                temp = column[(move_count + 2) % 3].top();
                column[(move_count + 2) % 3].pop();
                column[(move_count ) % 3].push(temp);
            }
            else if(column[(move_count) % 3].top() - column[(move_count + 2) % 3].top() < 0  && (int)column[flag].size() != n)
            {
                printf("%c -> %c\n", column_name[(move_count) % 3], column_name[(move_count + 2) % 3]);
                temp = column[(move_count ) % 3].top();
                column[(move_count ) % 3].pop();
                column[(move_count + 2) % 3].push(temp);
            }
        }
        else
        {
            if(column[(move_count) % 3].empty() && (int)column[flag].size() != n)
            {
                printf("%c -> %c\n", column_name[(move_count + 2) % 3], column_name[(move_count) % 3]);
                temp = column[(move_count + 2) % 3].top();
                column[(move_count + 2) % 3].pop();
                column[(move_count ) % 3].push(temp);
            }
            else if(column[(move_count + 2) % 3].empty()  && (int)column[flag].size() != n)
            {
                printf("%c -> %c\n", column_name[(move_count) % 3], column_name[(move_count + 2) % 3]);
                temp = column[(move_count ) % 3].top();
                column[(move_count ) % 3].pop();
                column[(move_count + 2) % 3].push(temp);
            }
        }

        move_count++;
        if((int)column[flag].size() == n)
            break;
    }
    return 0;
}

可能光是文字还是不太好理解,以三个圆盘为例具体的移动过程如下。
在这里插入图片描述

在这里插入图片描述
A->C
在这里插入图片描述
A→B
在这里插入图片描述
C→B

在这里插入图片描述
A→C

在这里插入图片描述
B→A
在这里插入图片描述
B→C
在这里插入图片描述
A→C
2.方案二
从我的一位同学那里学习到的,代码的作者是他,我简单做了些修改,添加了一些注释便于理解。比第一种的可迁移性要高很多,理解起来的难度可能和第一个差不多(因人而异把,我自己觉得第一个可能更好理解些嘻嘻),因为是去模拟函数栈的工作过程,所以代码也相当简洁。
与递归函数高度对应,具体怎么对应参见主函数注释,用一个自己写的stack来辅助模拟函数递归的过程,有种生拆的感觉O(∩_∩)O哈哈~。

#include <iostream>
#include <cstdio>

using namespace std;

const int defaultSize = 10000;

class hanoi
{
public:
    int n;
    char a1;
    char a2;
    char a3;
    hanoi(int _n = 5):n(_n), a1('a'), a2('b'), a3('c'){}

    hanoi(int _n, char _a1, char _a2, char _a3):n(_n),
        a1(_a1), a2(_a2), a3(_a3){}

};

class Stack
{
    hanoi* data;
    int top;
    int maxSize;
public:
    Stack(int s_size = defaultSize):data(NULL), top(-1), maxSize(defaultSize)
    {
        data = new hanoi[s_size];
    }
    bool IsFull(){return top == (maxSize - 1);}
    bool IsEmpty(){return top == -1;}
    bool Pop()
    {
        if(IsEmpty())
            return false;
        top--;
        return true;
    }
    bool Push(hanoi x)
    {
        if(IsFull())
            return false;
        data[++top] = x;
        return true;
    }
    hanoi getTop(){return data[top];}
};

int main()
{
    int n;
    scanf("%d", &n);
    if(n < 1)
        return 0;

    Stack a;
    hanoi x(n);
    a.Push(x);//对应主函数中的传参,因为只有1个参数的构造函数会将三根柱子
    //依次赋值为'a','b','c',所以依然是对应的
    while(!a.IsEmpty())
    {
        if(a.getTop().n == 1)
        {
            //cout << a.getTop().a1 << " -> " << a.getTop().a3 << endl;
            printf("%c -> %c\n", a.getTop().a1, a.getTop().a3);
            a.Pop();
            continue;
        }
        hanoi x1(a.getTop().n - 1, a.getTop().a1, a.getTop().a3, a.getTop().a2);
        //第一个参数为1是为了利用循环中a.getTop()为1时会移动并打印一次移动过程
        hanoi x2(1, a.getTop().a1, a.getTop().a2, a.getTop().a3);
        hanoi x3(a.getTop().n - 1, a.getTop().a2, a.getTop().a1, a.getTop().a3);
        a.Pop();
        //本着栈后进先出的工作原理,所以调整三者的入栈顺序
        a.Push(x3);
        a.Push(x2);
        a.Push(x1);
    }
    return 0;
}


同样的,以三个盘子的情况为例,具体的过程大概是这样的

在这里插入图片描述
3.方案三
这段代码的作者也不是我(T ^ T),是来自我的一位舍友,这种递归转非递归的可迁移性我觉得应该是三者之中最高的,但是可能也是最难理解的一种。此种方法模拟的是函数工作栈的过程,是去模拟类似codeblocks中call stack的进入和返回的过程。
对于递归代码,每当当前工作栈的n值为1时,需要进行打印。而因为原递归函数中,每当把总问题或者原子问题的n - 1个盘子从a借助c移动到b时,递归代码else中的这一步就逐步返回了,此时它第二次出现在栈顶。进行递归代码else中的第二行,也需要进行打印。这就是非递归代码中if部分的原理。
而为了标记它第几次出现在栈顶,我们加一个mark标记,而刚进入栈顶(mark由0变成1)时,我们应该做的是先处理原子问题,所以应该让n为n - 1的结点后入栈而出现在栈顶。这是非递归代码中else的if分支的原理。
若其又一次进入栈顶(mark由1变成2),意味着下一次一定会是第二次出现在栈顶,所以下一次是需要打印这个mark为2的结点的信息的(严格的说不能叫做结点,而是函数工作栈的一个元素块),所以要让当前栈顶结点也就是n为n(当前所在函数中给的形参对应的元素块)的结点入栈。这是非递归代码中else的else if分支的原理。

#include <iostream>
#include <stdio.h>
#include <stack>

using namespace std;

struct Node
{
	int n;
	char a;
	char b;
	char c;
	int mark = 0;
	Node(int N, char A, char B, char C)
	{
		n = N;
		a = A;
		b = B;
		c = C;
	}
};
stack<Node> Hanno;
int main()
{
	int n;
	//scanf("%d", &n);
	cin >> n;
	Node hano(n, 'a', 'b', 'c');
	Hanno.push(hano);
	while (!Hanno.empty())
	{
		hano = Hanno.top();
		if (Hanno.top().n == 1 || Hanno.top().mark == 2)
		{
			printf("%c -> %c\n", Hanno.top().a, Hanno.top().c);
			Hanno.pop();
		}
		else
		{
			if (Hanno.top().mark == 0)
			{
				hano.mark++;
				Hanno.pop();
				Hanno.push(hano);
				Hanno.push(Node(hano.n - 1, hano.a, hano.c, hano.b));
			}
			else if (Hanno.top().mark == 1)
			{
				hano.mark++;
				Hanno.pop();
				Hanno.push(Node(hano.n - 1, hano.b, hano.a, hano.c));
				Hanno.push(hano);
			}
		}
	}

	return 0;
}

这道题画出来的模拟图其实和第二种方案差不多,但会由于标记,比第二种过程少一些,具体的图就不画出来了,但是可以根据代码去自己画一下。可能是由于汉诺塔问题已经比较成熟了,出现了多种解题方案,第一种毕竟有些就题论题,还是应该多试着写一些后两种这样的通用的递归转非递归的代码。第一篇博客就到此结束啦Hi~ o( ̄▽ ̄)ブ。

汉诺塔是一个经典的递归问题,但在某些场景下我们也可以通过栈的方式来实现它的非递归解法。以下是基于 PTA 平台的 Java 非递归实现思路: ### 思路分析 1. **模拟递归过程** 我们可以利用显式栈来代替系统调用栈,从而手动管理函数的状态转移。 2. **状态记录** 使用栈存储当前操作的信息(如盘子编号、起始柱、目标柱和辅助柱),并通过标记变量区分是“进入”还是“返回”的动作。 3. **移动规则** 根据汉诺塔的基本规则,在两个步骤之间完成必要的打印或实际移动,并更新栈内容直到所有任务结束。 --- #### 示例代码 (Java 实现) ```java import java.util.Stack; public class HanoiNonRecursive { public static void hanoi(int n) { Stack<MoveInfo> stack = new Stack<>(); // 初始化第一个任务:将n个盘从A移到C,借助B stack.push(new MoveInfo(n, "A", "C", "B", true)); while (!stack.isEmpty()) { MoveInfo current = stack.pop(); if (current.n == 1 && current.flag) { // 基本情况:直接移动 System.out.println("Move disk 1 from " + current.from + " to " + current.to); continue; } if (current.flag) { // 进入阶段:分解成更小的问题 stack.push(new MoveInfo(current.n, current.from, current.to, current.via, false)); // 返回标志置为false int subN = current.n - 1; // 减少一个问题规模 // 将subN个盘先移至via柱 stack.push(new MoveInfo(subN, current.from, current.via, current.to, true)); } else { // 返回阶段:处理中间一步并调整后续任务 System.out.println("Move disk " + current.n + " from " + current.from + " to " + current.to); int subN = current.n - 1; stack.push(new MoveInfo(subN, current.via, current.to, current.from, true)); // 移动剩余部分到to } } } private static class MoveInfo { int n; // 当前层数 String from;// 起点柱 String to; // 目标柱 String via; // 辅助柱 boolean flag; // 是否正在"进入" MoveInfo(int n, String from, String to, String via, boolean flag) { this.n = n; this.from = from; this.to = to; this.via = via; this.flag = flag; } } public static void main(String[] args) { int numberOfDisks = 4; // 设定盘数 hanoi(numberOfDisks); } } ``` --- #### 输出结果示例 假设输入 `numberOfDisks=3`,程序会生成以下输出序列: ``` Move disk 1 from A to C Move disk 2 from A to B Move disk 1 from C to B Move disk 3 from A to C Move disk 1 from B to A Move disk 2 from B to C Move disk 1 from A to C ``` --- #### 解释关键点 1. **栈的作用** 显式栈用于保存每次的操作信息以及是否需要继续深入或回退。 2. **方向控制** 利用布尔值 `flag` 区分递归中的“进入”和“退出”。 3. **复杂度** 时间复杂度仍是 O(2^n),空间复杂度由隐式的函数调用栈变为显式的数据结构开销。 ---
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值