递归解决汉诺塔问题

8 篇文章 0 订阅
4 篇文章 0 订阅

前言

先来看两个有趣的故事

  • 从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙。。。
    • 问:GNU全称是什么?
    • 答:GNU is Not UNIX
    • 问:第一个单词GNU的全称是什么?
    • 答:GNU is Not UNIX
    • 问:第一个单词GNU的全称是什么?
    • 答:GNU is Not UNIX
    • 。。。

类似于上述这种使用自己来定义自己的结构,称之为递归

递归思想很有意思,在很多方面都有实际的用途。在计算机编程中,对于一些特定的问题,常规方法往往会显得很繁琐,抓不住问题的本质,这种时候采用递归的思想编程,往往能够写出很优雅的代码。

汉诺塔问题

“汉诺塔”是由数学家爱德华 · 卢卡斯于1883年发明的游戏,爱德华当初用了一个神话来描述这个问题

传说越南河内某间寺院有三根银棒,上串 64 个金盘。寺院里的僧侣依照一个古老的预言,以上述规则移动这些盘子;预言说当这些盘子移动完毕,世界就会灭亡。这个传说叫做梵天寺之塔问题(Tower of Brahma puzzle)。

—— 维基百科

这个问题被数学抽象描述就是

  • 问题:有3根细柱子(A、B、C),其中A柱子上有64个圆盘,64个圆盘按照从上到下由小到大的次序排列。求一共需要移动多少次才能将A柱子上的64个圆盘全部移动到B柱子上?
  • 移动规则(移动前、移动中、移动后,都必须遵循)
    • 一次只能移动柱子最上端的一个圆盘
    • 小圆盘上面不能放大圆盘

对于64个圆盘问题,太复杂太庞大了。可以先从最小的情况开始考虑

一层汉诺塔

算法图解如下

一层汉诺塔图解

规律:

  • 将1个圆盘,从A柱子移动到B柱子

记作: AB A → B

两层汉诺塔

算法图解如下

两层汉诺塔图解

规律:

  • 将2-1个圆盘,从A柱子移动到C柱子
  • 将第2个圆盘,从A柱子移动到B柱子
  • 将2-1个圆盘,从C柱子移动到B柱子

记作:

  • AC A → C
  • AB A → B
  • CB C → B
三层汉诺塔

算法图解如下

三层汉诺塔图解01

三层汉诺塔图解02

规律

  • 将3-1个圆盘,从A柱子移动到C柱子(第1次~第3次)
  • 将第3个圆盘,从A柱子移动到B柱子(第4次)
  • 将3-1个圆盘,从C柱子移动到B柱子(第5次~第7次)

记作:

  • ABACBC A → B , A → C , B → C
  • AB A → B
  • CACBAB C → A , C → B , A → B
规律一

由于第四层汉诺塔整个算法图解过程已经很长了,所以这里就不列举了。不过,从前面三层汉诺塔图解算法中,我们足以发现了n层汉诺塔移动的一般性规律

  • 将n-1个圆盘,从A柱子移动到C柱子
  • 将第n个圆盘,从A柱子移动到B柱子
  • 将n-1个圆盘,从C柱子移动到B柱子

我们将上述三个步骤,调换一下顺序

  • 将n-1个圆盘,从A柱子移动到C柱子
  • 将n-1个圆盘,从C柱子移动到B柱子
  • 将第n个圆盘,从A柱子移动到B柱子

调换顺序之后的发现,其实第一步和第二步合起来,便是将n-1个圆盘从A柱子移动到B柱子上

因此,算法可以进一步简化为

  • 将n-1个圆盘,从A柱子移动到B柱子
  • 将第n个圆盘,从A柱子移动到B柱子

将n-1个圆盘从A移动到B的过程,就是将n-2个圆盘从A移动到B,与将第n-1个圆盘从A移动到B;将n-2个圆盘从A移动到B的过程,就是将n-3个圆盘从A移动到B,与将第n-2个圆盘从A移动到B;……;将1个圆盘从A移动到B的过程,就是将第1-1个圆盘从A移动到B,与将第1个圆盘从A移动到B。

很明显,这样的一个过程,就是递归。在整个递归过程中,无论从第几层开始,最后会终止于一个确定的步骤——将1个圆盘从A移动到B,而每一层最关键的一个确定步骤就是——将第n个圆盘从A移动到B。因此整个递归过程其实都是在做一件事情——将1个圆盘从A移动到B

规律二

由规律一,我们可以得知,整个递归过程中都是在重复做一件事情——将1个圆盘从A移动到B。但事实上,一旦圆盘数目超过1个时,就需要一个借助3根柱子中的一根作为中转站,通过折中的方法实现将圆盘从A的移动到B。

  • 将n-1个圆盘,从A柱子移动到C柱子
  • 将第n个圆盘,从A柱子移动到B柱子
  • 将n-1个圆盘,从C柱子移动到B柱子

由上述总结规律可知,这样的一个过程,起到中转站的作用的柱子似乎就是C柱子。

但是事实上绝不是这样,很多情况下,起到中转站作用的柱子不是绝对的!

分析一个最具代表的模型,三层汉诺塔的表达式

  • ABACBC A → B , A → C , B → C
  • AB A → B
  • CACBAB C → A , C → B , A → B

从整个算法过程来看,以B为中转站能够将2个盘子从A移动到C,以A为中转站能够将2个盘子从C移动到B。在更多层的汉诺塔算法过程中,往往是A、B、C都会对某一段过程起到中转站的作用,因此使用中转站的概念来抽象出基本表达式是难以把握的。

因此,这里建议采用了一种目标柱子的概念(没听过对吧,我自创的)来理解上述的过程。

  • 首先,将算法中最特殊的一个步骤——从A移动到B,看做本次移动的目标柱子是B,因此在这样一个步骤中, 右边出现最多的就是B
  • 接着,第一步算法中的所有移动的目标柱子就是C,因此第一步算法中, 右边出现最多的就是C
  • 同理,第三步算法的目标柱子是B, 右边出现最多的就是B。

OK!到此,我们可以得知,在整个算法中,只有两个目标柱子——B和C存在。

算法实现

通过上面的一系列分析,我们得出了两个最为关键性的结论。根据这两个相关性结论,编写代码实现算法

C语言实现算法

#include <stdio.h>

//在参数x、y、z中,永远以中间那个参数为“目标柱子”
void hanoi(int n, char x, char y, char z)
{
    if (n == 1) {
        //递归的最后一种情况,1层汉诺塔:从A移动到B
        printf("第%d层圆盘移动方向 : %c -> %c\n", n, x, y);
    } else {
        //第一步,以C柱子为目标(规律二)
        hanoi(n - 1, x, z, y);

        //第二步,以B柱子为目标,直接输出(规律一)
        printf("第%d层圆盘移动方向 : %c -> %c\n", n, x, y);

        //第三步,以B柱子为目标(规律二)
        hanoi(n - 1, z, y, x);
    }
}

int main(void)
{
    hanoi(1, 'A', 'B', 'C');
    putchar('\n');
    hanoi(2, 'A', 'B', 'C');
    putchar('\n');
    hanoi(3, 'A', 'B', 'C');
    putchar('\n');
    hanoi(4, 'A', 'B', 'C');

    return 0;
}

/* 运行结果:
第1层圆盘移动 : A -> B

第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B


第1层圆盘移动 : A -> B
第2层圆盘移动 : A -> C
第1层圆盘移动 : B -> C
第3层圆盘移动 : A -> B
第1层圆盘移动 : C -> A
第2层圆盘移动 : C -> B
第1层圆盘移动 : A -> B


第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
第3层圆盘移动 : A -> C
第1层圆盘移动 : B -> A
第2层圆盘移动 : B -> C
第1层圆盘移动 : A -> C
第4层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
第2层圆盘移动 : C -> A
第1层圆盘移动 : B -> A
第3层圆盘移动 : C -> B
第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
*/

如分析中那样,递归算法采用的终止条件是——1层汉诺塔情况,但实际上这个算法是缺乏健壮性不合理的,而且存在冗余。因为它没能考虑汉诺塔最特殊的情况——0层汉诺塔 。当程序运行hanoi(0)时,函数无法返回,造成异常错误。事实上,0层汉诺塔是存在且合理的,而且把0层汉诺塔考虑进去才符合我们对汉诺塔问题的本质分析——每一次都是从A移动到B ,而且我们可以发现上述代码中1层汉诺塔也并没有什么特殊之处,函数中两个printf()语句执行的操作是一模一样的。下面给出改进版本的代码

Python实现算法

def hanoi(n, x, y, z):
    if not n:
        pass   #0层汉诺塔不需要做任何移动
    else:
        hanoi(n-1, x, z, y)
        print("第%d层圆盘移动方向:%c -> %c" % (n, x, y))
        hanoi(n-1, z, y, x)

hanoi(0, 'A', 'B', 'C')
# 什么也不输出

hanoi(1, 'A', 'B', 'C')
第1层圆盘移动方向:A -> B

hanoi(2, 'A', 'B', 'C')
第1层圆盘移动方向:A -> C
第2层圆盘移动方向:A -> B
第1层圆盘移动方向:C -> B

hanoi(3, 'A', 'B', 'C')
第1层圆盘移动方向:A -> B
第2层圆盘移动方向:A -> C
第1层圆盘移动方向:B -> C
第3层圆盘移动方向:A -> B
第1层圆盘移动方向:C -> A
第2层圆盘移动方向:C -> B
第1层圆盘移动方向:A -> B

改进版本的Python代码,不但能正确计算出汉诺塔问题,而且算法还能够兼容最特殊的情况——0层汉诺塔问题。这才是该问题最优雅的算法。

问题答案

回到汉诺塔的问题上,64个圆盘的汉诺塔需要移动多少次才能完成从A柱子到B柱子。

通过上面发现的规律,以及算法的实现,我们可以得出该算法的递归公式

H(n)=H(n1)+1+H(n1) H ( n ) = H ( n − 1 ) + 1 + H ( n − 1 )

使用递推公式计算汉诺塔移动次数

H(0)=0 H ( 0 ) = 0

H(1)=H(0)+1+H(0)=0+1+0=1 H ( 1 ) = H ( 0 ) + 1 + H ( 0 ) = 0 + 1 + 0 = 1

H(2)=H(1)+1+H(1)=1+1+1=3 H ( 2 ) = H ( 1 ) + 1 + H ( 1 ) = 1 + 1 + 1 = 3

H(3)=H(2)+1+H(2)=3+1+3=7 H ( 3 ) = H ( 2 ) + 1 + H ( 2 ) = 3 + 1 + 3 = 7

H(4)=H(3)+1+H(3)=7+1+7=15 H ( 4 ) = H ( 3 ) + 1 + H ( 3 ) = 7 + 1 + 7 = 15

H(5)=H(4)+1+H(4)=15+1+15=31 H ( 5 ) = H ( 4 ) + 1 + H ( 4 ) = 15 + 1 + 15 = 31

H(6)=H(5)+1+H(5)=31+1+31=63 H ( 6 ) = H ( 5 ) + 1 + H ( 5 ) = 31 + 1 + 31 = 63

... . . .

观察函数与函数值的规律可得知

H(0)=0=201 H ( 0 ) = 0 = 2 0 − 1

H(1)=1=211 H ( 1 ) = 1 = 2 1 − 1

H(2)=3=221 H ( 2 ) = 3 = 2 2 − 1

H(3)=7=231 H ( 3 ) = 7 = 2 3 − 1

H(4)=15=241 H ( 4 ) = 15 = 2 4 − 1

H(5)=31=251 H ( 5 ) = 31 = 2 5 − 1

H(6)=63=261 H ( 6 ) = 63 = 2 6 − 1

... . . .

因此,通项表达式为

H(n)=2n1 H ( n ) = 2 n − 1

n = 64 时,H(64) = 18446744073709551615。就算是每秒可以移动一次,也需要5845亿年多,而目前所知的宇宙的年龄也不过才137亿年,所以说就算传说当真,那么距离世界末日还早着呢。。。

总结

递归算法的关键就在于抓住递归中重复的步骤,然后每次递归都是在重复那一关键步骤。递归算法虽然内存开销比较大,但是代码可读性很好,而且体现出了一种数学的优雅美(反正我觉得很美!)。

数学中分形,具有递归美,下面放出福利图片,送给大家欣赏(图片来源:http://www.matrix67.com,侵权必删!)

科赫雪花

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是C语言非递归解决汉诺塔问题的代码示例: ```c #include <stdio.h> #include <stdlib.h> // 定义栈的结构 typedef struct { int *arr; int top; int capacity; } Stack; // 初始化栈 Stack* initStack(int capacity) { Stack *stack = (Stack*)malloc(sizeof(Stack)); stack->arr = (int*)malloc(capacity * sizeof(int)); stack->top = -1; stack->capacity = capacity; return stack; } // 判断栈是否为空 int isEmpty(Stack *stack) { return stack->top == -1; } // 判断栈是否已满 int isFull(Stack *stack) { return stack->top == stack->capacity - 1; } // 入栈 void push(Stack *stack, int data) { if (isFull(stack)) { printf("Stack is full.\n"); return; } stack->arr[++stack->top] = data; } // 出栈 int pop(Stack *stack) { if (isEmpty(stack)) { printf("Stack is empty.\n"); return -1; } return stack->arr[stack->top--]; } // 非递归解决汉诺塔问题 void hanoiNonRecursive(int n, Stack *src, Stack *aux, Stack *dest) { int totalMoves = (1 << n) - 1; // 总共需要移动的次数 // 根据奇偶性确定第一个辅助塔 Stack *temp; if (n % 2 == 0) { temp = aux; aux = dest; dest = temp; } for (int i = n; i >= 1; i--) { push(src, i); // 将盘子依次入栈 } for (int i = 1; i <= totalMoves; i++) { if (i % 3 == 1) { if (isEmpty(src)) { push(src, pop(aux)); } else if (isEmpty(aux)) { push(aux, pop(src)); } else if (src->arr[src->top] > aux->arr[aux->top]) { push(src, pop(aux)); } else { push(aux, pop(src)); } } else if (i % 3 == 2) { if (isEmpty(src)) { push(src, pop(dest)); } else if (isEmpty(dest)) { push(dest, pop(src)); } else if (src->arr[src->top] > dest->arr[dest->top]) { push(src, pop(dest)); } else { push(dest, pop(src)); } } else if (i % 3 == 0) { if (isEmpty(aux)) { push(aux, pop(dest)); } else if (isEmpty(dest)) { push(dest, pop(aux)); } else if (aux->arr[aux->top] > dest->arr[dest->top]) { push(aux, pop(dest)); } else { push(dest, pop(aux)); } } } } int main() { int n = 3; // 汉诺塔的盘子数量 Stack *src = initStack(n); Stack *aux = initStack(n); Stack *dest = initStack(n); hanoiNonRecursive(n, src, aux, dest); printf("Move the disks from source to destination:\n"); for (int i = n - 1; i >= 0; i--) { printf("%d -> %d\n", src->arr[i], dest->arr[i]); } free(src->arr); free(aux->arr); free(dest->arr); free(src); free(aux); free(dest); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值