1. 程式人生 > >漢諾塔問題的遞迴和非遞迴演算法

漢諾塔問題的遞迴和非遞迴演算法

       漢諾塔問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著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。

在本文中,將討論用遞迴和非遞迴的方法來解決漢諾塔問題。

1、 通過遞迴實現漢諾塔問題的求解

設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)。此演算法的遞迴程式碼實現如下所示:

#include <fstream>
#include <iostream>
#include<time.h>
using namespace std;
void Move(int n,char x,char y)
{
   cout<<"把"<<n<<"號從"<<x<<"挪動到"<<y<<endl;
}
void Hannoi(int n,char a,char b,char c)
{
      if(n==1)
              Move(1,a,c);
    else
    {
          Hannoi(n-1,a,c,b);
            Move(n,a,c);
            Hannoi(n-1,b,a,c);
    }
}
int main()
{
  int n;
  cout<<"請輸入要求解的漢諾塔的階數: ";
  cin>>n;
  clock_t start,finish;
  start = clock();
    cout<<"以下是7層漢諾塔的解法:"<<endl;
    Hannoi(n,'a','b','c');
    cout<<"輸出完畢!"<<endl;
  finish = clock();
  printf("解決此 %d 階漢諾塔所需的時間為:%.2f ms\n",n,(double)(finish-start));
  system("pause");
    return 0;
}

2、 通過非遞迴的思想來實現漢諾塔問題的求解

漢諾塔的非遞迴演算法描述如下:

首先容易證明,當盤子的個數為n時,移動的次數應等於2^n - 1。

一位美國學者發現一種出人意料的方法,只要輪流進行兩步操作就可以了。

首先把三根柱子按順序排成品字型,把所有的圓盤按從大到小的順序放在柱子A上。

根據圓盤的數量確定柱子的排放順序:若n為偶數,按順時針方向依次擺放 A B C;

若n為奇數,按順時針方向依次擺放 A C B。

(1)按順時針方向把圓盤1從現在的柱子移動到下一根柱子,即當n為偶數時,若圓盤1在柱子A,則把它移動到B;

若圓盤1在柱子B,則把它移動到C;若圓盤1在柱子C,則把它移動到A。

(2)接著,把另外兩根柱子上可以移動的圓盤移動到新的柱子上。

即把非空柱子上的圓盤移動到空柱子上,當兩根柱子都非空時,移動較小的圓盤

這一步沒有明確規定移動哪個圓盤,你可能以為會有多種可能性,其實不然,可實施的行動是唯一的。

(3)反覆進行(1)(2)操作,最後就能按規定完成漢諾塔的移動。

該演算法的實現程式碼如下:

#include <iostream>
#include<time.h>
using namespace std;
//圓盤的個數最多為64
const int MAX = 64;
//用來表示每根柱子的資訊
struct st{
     int s[MAX]; //柱子上的圓盤儲存情況
     int top; //棧頂,用來最上面的圓盤
     char name; //柱子的名字,可以是A,B,C中的一個
     int Top()//取棧頂元素
     {
           return s[top];
     }
     int Pop()//出棧
     {
           return s[top--];
     }
     void Push(int x)//入棧
     {
           s[++top] = x;
     }
} ;
 
long Pow(int x, int y); //計算x^y
void Creat(st ta[], int n); //給結構陣列設定初值
void Hannuota(st ta[], long max); //移動漢諾塔的主要函式
int main(void)
{
     clock_t start,finish;
           int n;
           cout<<"請輸入漢諾塔的階數:";
     cin >> n; //輸入圓盤的個數
           start = clock();
     st ta[3]; //三根柱子的資訊用結構陣列儲存
     Creat(ta, n); //給結構陣列設定初值
     long max = Pow(2, n) - 1;//動的次數應等於2^n - 1
     Hannuota(ta, max);//移動漢諾塔的主要函式
           finish = clock();
           printf("解決此 %d 階漢諾塔所需的時間為:%.2f ms\n",n,(double)(finish-start));
     system("pause");
     return 0;
}
void Creat(st ta[], int n)
{
     ta[0].name = 'A';
     ta[0].top = n-1;
    //把所有的圓盤按從大到小的順序放在柱子A上
     for (int i=0; i<n; i++)
           ta[0].s[i] = n - i;
     //柱子B,C上開始沒有沒有圓盤
     ta[1].top = ta[2].top = 0;
     for (int i=0; i<n; i++)
           ta[1].s[i] = ta[2].s[i] = 0;
    //若n為偶數,按順時針方向依次擺放 A B C
     if (n%2 == 0)
     {
           ta[1].name = 'B';
           ta[2].name = 'C';
     }
     else  //若n為奇數,按順時針方向依次擺放 A C B
     {
           ta[1].name = 'C';
           ta[2].name = 'B';
     }
}
 
long Pow(int x, int y)
{
     long sum = 1;
     for (int i=0; i<y; i++)
           sum *= x;
     return sum;
}
 
void Hannuota(st ta[], long max)
{
  intk = 0; //累計移動的次數
  inti = 0;
  intch;
 while (k < max)
  {
   //按順時針方向把圓盤1從現在的柱子移動到下一根柱子
   ch = ta[i%3].Pop();
  ta[(i+1)%3].Push(ch);
  cout << ++k << ": " <<
        "Move disk " << ch << " from " <<ta[i%3].name <<
        " to " << ta[(i+1)%3].name << endl;
  i++;
   //把另外兩根柱子上可以移動的圓盤移動到新的柱子上
   if(k < max)
  {    
   //把非空柱子上的圓盤移動到空柱子上,當兩根柱子都為空時,移動較小的圓盤
   if (ta[(i+1)%3].Top() == 0 ||
       ta[(i-1)%3].Top() > 0 &&
       ta[(i+1)%3].Top() > ta[(i-1)%3].Top())
   {
       ch =  ta[(i-1)%3].Pop();
       ta[(i+1)%3].Push(ch);
       cout << ++k << ": " << "Move disk"
            << ch << " from " << ta[(i-1)%3].name
            << " to " << ta[(i+1)%3].name << endl;
    }
   else
    {
      ch =  ta[(i+1)%3].Pop();
      ta[(i-1)%3].Push(ch);
      cout << ++k << ": " << "Move disk"
           << ch << " from " << ta[(i+1)%3].name
           << " to " << ta[(i-1)%3].name << endl;
    }
 }
}
}


3、 實驗結果及分析(測試時以7階漢諾塔為例)

使用遞迴演算法時執行的情況:


使用非遞迴演算法時執行的情況:


      從實驗結果可以看出,與n皇后問題不同,對於漢諾塔問題的求解,當使用遞迴的方法來解決時它的時間複雜度比非遞迴的方法要好。而且,使用遞迴演算法寫程式碼時更容易理解。通過對於漢諾塔問題非遞迴與遞迴方法的對比可以得出結論:有的時候使用的遞迴的方法對於問題的求解不僅更能使人容易理解,而且效率更高。我們在以後編程式碼時也應該注意遞迴方法的使用。




相關推薦

的改編題(用棧求解,分別

限制不能從最左側的塔直接移動到最右側,也不能從最右側直接移動到最左側,而是必須經過中間,求當塔有N層的時候,列印最優移動過程和最優移動總步數 例如:當塔為兩層時,最上層的塔記為1,最下層的塔記為2,則

問題的演算法

       漢諾塔問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能

(Hanoi)問題&的C++實現及總結

漢諾塔(Hanoi)問題遞迴&非遞迴的C++實現及總結 由於剛入門不算很久,所以就那漢諾塔這種簡單問題來練下手啦~~ 【漢諾塔問題內容】(雖然路人皆知但還是寫一下好了。。。)   相傳在古印度聖廟中,有一種被稱為漢諾塔(Hanoi)的遊戲。

(必須經過中間柱子)詳解與程式碼實現

首先介紹一下漢諾塔最初始的規則: 有三根相鄰的柱子,標號為A,B,C,A柱子從上到下按照金字塔狀疊放著n個不同大小的圓盤,現在把所有的盤子一個一個移動到柱子B上,並且每次移動同一根柱子上都不能出現大盤子在小盤子上方。 這是最初始的規則,實現的思路可以分為

的原理剖析以及的解決辦法

漢諾塔:源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。

(C的經典習題)

漢諾塔問題:設有三個塔座,依次命名為  x,y,z  。有  z  個直徑不同的圓盤,由小到大依次  編號為  1  、  2  、  ……  ,  n  。開始時,它們全部按遞減的次序插在塔座上。現

一列數字的規則如下;1,1,2,3,5,8,13,21,34........ 求第30位數字是多少,用兩種方法演算法實現

斐波納契數列(Fibonacci Sequence),又稱黃金分割數列。在數學上,斐波納契數列以如下被以遞迴的方法定義:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)在現代物理、準晶體結構、化學等領域,斐波納契數列都有直接的應用,現在我從演算法的角度,利用遞迴和非

二叉樹的前序,中序,後序的遍歷的程式碼-C語言

#include <stdio.h> #include<stdlib.h> /* run this program using the console pauser or add your own getch, system("pause") or input l

二叉樹的前中後序遍歷(版本)

各位讀者週末愉快呀,今天我想來說說一道很常見的面試題目 —— 關於二叉樹前中後序遍歷的實現。本文將以遞迴和非遞迴方式實現這 3 種遍歷方式,程式碼都比較短,可以放心食用。 先簡單說明一下這 3 種遍歷方式有什麼不同 —— 對於每種遍歷,樹中每個結點都需要經過 3 次(對於葉結點,其左右子樹視為空子樹),但前

演算法】二叉樹的遍歷(轉)

原文地址 【寫在前面】   二叉樹是一種非常重要的資料結構,很多其它資料結構都是基於二叉樹的基礎演變而來的。對於二叉樹,有前序、中序以及後序三種遍歷方法。因為樹的定義本身就 是遞迴定義,因此採用遞迴的方法去實現樹的三種遍歷不僅容易理解而且程式碼很簡潔。而對於樹的遍歷若採用非遞迴的方法,就要採

leetcode 783. 二叉搜尋樹結點最小距離(實現java)

題目描述: 給定一個二叉搜尋樹的根結點 root, 返回樹中任意兩節點的差的最小值。 示例: 輸入: root = [4,2,6,1,3,null,null] 輸出: 1 解釋: 注意,root是樹結點物件(TreeNode object),而不是陣列。 給定的樹 [4,

No.19程式碼練習:斐波那契數列,某數k次冪,模擬實現strlen(),階乘 ,逆置字串(

學習不易,需要堅持。 遞迴 程式呼叫自身的程式設計技巧稱為遞迴( recursion)。遞迴做為一種演算法在程式設計語言中廣泛應用。 一個過程或函式在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞迴策略只需

楊氏矩陣查詢數字(

楊氏矩陣  有一個二維陣列. 陣列的每行從左到右是遞增的,每列從上到下是遞增的. 在這樣的陣列中查詢一個數字是否存在。 要求:時間複雜度小於O(N);  例:  1 2 3          4 5 6  

求第n個斐波那契數(分別用兩種方法求解)

斐波那契數列指的是這樣一個數列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55……這個數列從第3項開始,每一項都等於前兩項之和。 這裡分別用遞迴和非遞迴的方法實現: 遞迴 #define _CRT_SECURE_NO_WARNINGS 1 #include&l

c++二叉樹的的前序中序後序遍歷以及層序遍歷

二叉樹的遞迴版的前序,中序和後序遍歷很簡單也很容易理解,這裡就放一個前序遍歷的例子 //前序遍歷遞迴演算法,遞迴演算法都大同小異,這裡就不一一列舉了 void binaryTree::pro_order(NodeStack::Node *t) { NodeStack::Node *h = t;

猴子吃桃問題,用方法

猴子吃桃問題:猴子第一天摘下若干個桃子,當即吃了一半,還不過癮,又多吃了一個 第二天早上又將剩下的桃子吃掉一半,又多吃了一個。以後每天早上都吃了前一天剩下的一半零一個。到第10天早上想再吃時,見只剩下一個桃子了。求第一天共摘了多少。   public class Test{ &nb

全面分析再動手的習慣:連結串列的反轉問題(方式)

https://www.cnblogs.com/kubixuesheng/p/4394509.html dashuai的部落格 要麼牛B!要麼滾!   首頁   聯絡 訂閱 管理 隨筆-88  文章-0 

二叉樹的後序遍歷-演算法

同樣的,建立的演算法在先序中有,略去。 後序遞迴遍歷演算法 void PostOrder(BiTree bt){ if(bt){ PostOrder(bt->lchild); PostOrder(bt->rchild); cout<<bt-

二叉樹的中序遍歷-演算法

建立二叉樹就不說了,這裡直接: 中序遞迴遍歷演算法 void InOrder(BiTree T){ if(T){ InOrder(T->lchild); cout<<T->data<<" "; InOrder(T->rch

二叉樹的先序遍歷-演算法

需要實踐先序遍歷,我們先建立二叉樹。這裡採用先序序列建立二叉樹,不為別的,因為簡單。 typedef int ElemType; typedef struct BiTNode{ ElemType data; struct BiTNode *lchild, *rchild; }*BiTre