問題描述:
在印度,有這么一個古老的傳說:在世界中心貝拿勒斯(在印度北部)的聖廟里,一塊黃銅板上插着三根寶石針。印度教的主神梵天在創造世界的時候,在其中一根針上從下到上地穿好了由大到小的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結束。
我覺得利用二叉樹的中序遍歷,其實也就是利用遞歸,有興趣的人可以自己去實現,這里只是提供一種思想。
總結:以上純屬個人的理解,對於有些地方覺得還是理解不是很深,有不足之處和錯誤的地方希望大家幫我指出。謝謝