问题描述:
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片,一次只移动一片,不管在哪根针上,小片必在大片上面。当所有的金片都从梵天穿好的那根针上移到另外一概针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。
不管这个传说的可信度有多大,如果考虑一下把64片金片,由一根针上移到另一根针上,并且始终保持上小下大的顺序。这需要多少次移动呢?这里需要递归的方法。假设有n片,移动最少次数是f(n).显然f(1)=1,f(2)=3,f(3)=7,且f(k+1)=2*f(k)+1。此后不难证明f(n)=2^n-1。(如果不理解,下面有讲述)
n=64时,
f(64)= 2^64-1=18446744073709551615
假如每秒钟一次,共需多长时间呢?一年大约有 31536926 秒,计算表明移完这些金片需要5800多亿年,比地球寿命还要长,事实上,世界、梵塔、庙宇和众生都已经灰飞烟灭。
递归算法:
//A表示开始塔,C表示目标塔,B表示中间塔 void Hanoi(int n, int A, int C, int B) { if (n > 0) { Hanoi(n - 1, A, B, C); Move(n, A, C); Hanoi(n - 1, B, C, A); } }
设f(n)为将n片圆盘所在塔全部移动到另一塔最少总次数;
由递归算法可知:f(1) = 1;当n>1时,f(n) = f(n-1) + 1 + f(n-1)。f(n) = 把上面n-1片圆盘移动到中间塔最少总次数f(n-1) + 把第n片圆盘移动到目标塔+ 把中间盘的n-1片圆盘移动到目标塔最少总次数为f(n-1)。由数学计算可得:f(n)=2^n-1。(n>0)
非递归算法:
算法思想与分析:
1
△
0 2
- 将三根柱子(塔)摆成三角形,对应索引0、1、2为三角形的三个顶点,A是开始堆满盘子的柱子(塔),C是目标柱子(塔); 三角形顶点0对应堆满盘子的柱子(塔)A,由于当n为奇数时开始移动盘子是A->C,n为偶数时开始移动的盘子是A->B,如果固定摆放顺序,这样导致开始移动的方向不同。由于柱子(塔)摆放的顺序对结果没有影响(这里说的摆放顺序是当盘子的个数n确定开始时,初始化摆放的顺序,随后摆放顺序也就不能改变了)
- 为了统一按顺时针方向移动盘子,做下面设置:
若n为偶数,按顺时针方向依次摆放柱子 A B C(即三角形顶点0--A;三角形顶点1--B;三角形顶点2--C)
若n为奇数,按顺时针方向依次摆放柱子 A C B(即三角形顶点0--A;三角形顶点1--C;三角形顶点2--B)
- 当盘子的个数为n时,max为移动的最少次数应等于2^n - 1。从一根柱子上移动圆盘,当前柱子最多移动两次(当前柱子只能跟两根柱子有移动关系,如果不理解可以接着看下面)
按顺时针方向考虑当前柱子与下一根柱子的移动关系:从当前柱子圆盘中取出最小值,把最小值按顺时针方向移动到下一根柱子,index记录存放最小值的柱子的索引,同时记录移动步骤且次数加1;不考虑下一根柱子移动到当前柱子原因是当前柱子是存放最小值的柱子,不存在把下一根柱子的顶值移动到当前柱子
按顺时针方向考虑当前柱子的下下根柱子的移动关系:由于按顺时针方向,所以下下根柱子其实就是当前柱子的上一根柱子。如果当前柱子圆盘不为空,并且当前柱子的上一根柱子为空或者上一根柱子的顶值(最小值)大于当前柱子的顶值,则我们可以把当前柱子的顶值移动到上一根柱子,同时记录移动步骤且次数加1;如果当前柱子为空且上一根柱子不为空,或者当前柱子不为空且上一根柱子不为空且上一根柱子的顶值小于当前柱子,则把上一个柱子的顶值移动到当前柱子,同时记录移动步骤且次数加1。
定义塔数据结构
public class Pillar { //用于存储柱子上的圆盘 private Stack<int> elements = new Stack<int>(); public Stack<int> Elements { get { return elements; } } //柱子的名称 public string Name { get; set; } public Pillar(string name) { Name = name; } }
初始化塔数据
//初始化数据 public static void Init(int n, out Pillar[] pillars) { // 1 // △ // 0 2 //将三根柱子摆成三角形,对应索引0、1、2为三角形的三个顶点 pillars = new Pillar[3]; //初始化三根柱子,A是开始堆满盘子的柱子,C是目标柱子 Pillar a = new Pillar("A"); Pillar b = new Pillar("B"); Pillar c = new Pillar("C"); pillars[0] = a; //索引0对应堆满盘子的柱子 //因为当n为奇数时开始移动盘子是A->C,n为偶数时开始移动的盘子是A->B,如果固定摆放顺序,这样导致开始移动的方向不同, //因为柱子摆放的顺序对结果没有影响(这里说的摆放顺序是当盘子的个数n确定开始时,初始化摆放的顺序,随后摆放顺序也就不能改变了) //这里为了统一按顺时针方向移动盘子,做下面设置 //若n为偶数,按顺时针方向依次摆放柱子 A B C(即0--A;1--B;2--C) if (n % 2 == 0) { pillars[1] = b; pillars[2] = c; } //若n为奇数,按顺时针方向依次摆放柱子 A C B(即0--A;1--C;2--B) else { pillars[1] = c; pillars[2] = b; } //把所有圆盘按从大到小顺序放到柱子A上 因为栈是后进先出,所以按从大到小 for (int i = 0; i < n; i++) { pillars[0].Elements.Push(n - i); } }
非递归算法
//n表示有N个圆盘 public static void Hanoi(int n) { Pillar[] pillars; Init(n, out pillars); //当盘子的个数为n时,max为移动的最少次数应等于2^n - 1 //设f(n):表示总共需要移动的最小次数:根据递归算法可知f(n) = f(n-1) + 1 +f(n-1) n>=2 ;f(1)=1; long max = (long)Math.Pow(2, n) - 1; int index = 0; //记录最小值也就是1所在的柱子的索引 最小值开始在a上,对应的索引为0 int count = 0; //累计移动次数 //从一根柱子上移动圆盘,一次while循环最多移动两次(当前柱子只能跟两根柱子有移动关系,如果不理解可以接着看下面) while (count < max) { #region 按顺时针方向考虑当前柱子与下一根柱子的移动关系 //从当前柱子开始按顺时针方向移动圆盘到下一根柱子 int min = pillars[index % 3].Elements.Pop(); //从当前柱子圆盘中取出最小值 pillars[(++index) % 3].Elements.Push(min); //把最小值按顺时针方向移动到下一根柱子,index记录存放最小值的柱子的索引 count++; //移动次数加1 Console.WriteLine("第" + count + "次移动:把 " + min + " 从 " + pillars[(index - 1) % 3].Name + " -> 到 " + pillars[index % 3].Name); //这里不用考虑下一根柱子移动到当前柱子原因是当前柱子是存放最小值的柱子,不存在把下一根柱子的顶值移动到当前柱子 #endregion int temp; #region 按顺时针方向考虑当前柱子的下下根柱子的移动关系 //继续判断当前柱子跟下下根柱子移动关系(为什么不是当前柱子的下一根柱子,这是因为刚才下一根柱子已经放了当前柱子的最小值) if (count < max) //如果移动次数小于最小次数 { count++; //因为按顺时针方向,所以下下根柱子其实就是当前柱子的上一根柱子 //如果当前柱子圆盘不为空,并且当前柱子的上一根柱子为空或者上一根柱子的顶值(最小值)大于当前柱子的顶值 //则我们可以把当前柱子的顶值移动到上一根柱子 if ((pillars[(index - 1) % 3].Elements.Count != 0) && (pillars[(index + 1) % 3].Elements.Count == 0 || pillars[(index + 1) % 3].Elements.Peek() > pillars[(index - 1) % 3].Elements.Peek())) { temp = pillars[(index - 1) % 3].Elements.Pop(); pillars[(index + 1) % 3].Elements.Push(temp); Console.WriteLine("第" + count + "次移动:把 " + temp + " 从 " + pillars[(index - 1) % 3].Name + " -> 到 " + pillars[(index + 1) % 3].Name); } //如果当前柱子为空且上一根柱子不为空,或者当前柱子不为空且上一根柱子不为空且上一根柱子的顶值小于当前柱子 //则把上一个柱子的顶值移动到当前柱子 else { temp = pillars[(index + 1) % 3].Elements.Pop(); pillars[(index - 1) % 3].Elements.Push(temp); Console.WriteLine("第" + count + "次移动:把 " + temp + " 从 " + pillars[(index + 1) % 3].Name + " -> 到 " + pillars[(index - 1) % 3].Name); } } #endregion } }
上面的按顺时针方向考虑当前柱子的下下根柱子的移动关系可以用下面的代替:
#region 按顺时针方向考虑当前柱子的下下根柱子的移动关系 //因为按顺时针方向,所以下下根柱子其实就是当前柱子的上一根柱子 //如果当前柱子圆盘不为空,并且当前柱子的上一根柱子为空或者上一根柱子的顶值(最小值)大于当前柱子的顶值,则我们可以把当前柱子的顶值移动到上一根柱子,同时记录移动步骤且次数加1 if ((pillars[(index - 1) % 3].Elements.Count != 0) && (pillars[(index + 1) % 3].Elements.Count == 0 || pillars[(index + 1) % 3].Elements.Peek() > pillars[(index - 1) % 3].Elements.Peek())) { temp = pillars[(index - 1) % 3].Elements.Pop(); pillars[(index + 1) % 3].Elements.Push(temp); count++; Console.WriteLine("第" + count + "次移动:把 " + temp + " 从 " + pillars[(index - 1) % 3].Name + " -> 到 " + pillars[(index + 1) % 3].Name); } // 如果当前柱子为空且上一根柱子不为空,或者当前柱子不为空且上一根柱子不为空且上一根柱子的顶值小于当前柱子,则把上一个柱子的顶值移动到当前柱子,同时记录移动步骤且次数加1 else { if (pillars[(index + 1) % 3].Elements.Count != 0) { temp = pillars[(index + 1) % 3].Elements.Pop(); pillars[(index - 1) % 3].Elements.Push(temp); count++; Console.WriteLine("第" + count + "次移动:把 " + temp + " 从 " + pillars[(index + 1) % 3].Name + " -> 到 " + pillars[(index - 1) % 3].Name); } } #endregion
拓展
其实由f(n)=2^n-1。(n>0) 我们可以想到满二叉树的中序遍历与递归的关系。
Hanoi(汉诺)问题的非递归算法(满二叉树中序遍历)
Hanoi(汉诺)问题的具体结果是:
1个盘子的结果是:1:A—C
2个盘子的结果是:1:A—B2:A—C 3:B—C
3个盘子的结果是:1:A—C2:A—B3:C—B4:A—C5:B—A6:B—C7:A—C
4个盘子的结果是:1:A—B2:A—C3:B—C4:A—B5:C—A6:C—B7:A—B8:A—C9:B—C10:B—A11:C~A12:B—C13:A—B14:A—C15:B—C
5个盘子的结果是:……不难发现,步骤总数f(n)是盘子总数n的确定函数:f(n)=2^n - 1,与满二叉树节点总数和树高的关系一致。于是,不的结果妨把上面的结果当作中序遍历满二叉树的结果,画出对应的满二叉树,如图1、图2、图3、图4所示。
观察这些满二叉树,发现以下结果
①盘子数n确定后,树高H是确定的:H一n;
②倒数第1层的序号分别是1(即2^0)、3、5、7…(2^n - 2^0),顺序排列的2^(n-1)个能被2^0整除且不能被2^1整除的数;
倒数第2层的序号为2、6、10、14、…(2^n - 2^1),2^(n – 2)个顺序排列的能被2^1整除且不能被2^2整除的数;
倒数第3层的序号为顺序排列的2^(n – 3)个能被2^2整除且不能被2^3整除的数…;
最上层的序号为2^(n – 1),只有2^(n一n)=1个;
③每一层的节点数是确定的,等于2^layer(layer,为从上往下数的层数,从1开始数);
④若不看节点前的序号,每幅图中相同层的结果完全相同(第1层全是A—C;第2层全是A—B,B—C;等);
⑤若只看字母(实为盘子名),每一层都从A开始,到C结束,且除首尾外字母都重复一次;
⑥移盘方向,奇数层都是A—C、C—B、B—A、A—C、C—B、B—A、…模3循环,且以A—C开始以A—C结束,偶数层都是A—B、B—C、C—A、A—B、B—C、C—A、…模3循环,且以A—B开始以B—C结束。
我觉得利用二叉树的中序遍历,其实也就是利用递归,有兴趣的人可以自己去实现,这里只是提供一种思想。
总结:以上纯属个人的理解,对于有些地方觉得还是理解不是很深,有不足之处和错误的地方希望大家帮我指出。谢谢