前言
相信學過《數(shù)據(jù)結(jié)構(gòu)與算法》這門課程的同學都有聽過漢諾塔問題,但是可能在大學的時候沒有鉆研過,或者在學的時候就沒有弄懂,導致沒有很好的理解漢諾塔的經(jīng)典解法,下面讓我來給大家來分析一下。
背景
漢諾塔(又稱河內(nèi)塔)問題是源于印度一個古老傳說的益智玩具。大梵天創(chuàng)造世界的時候做了三個金剛石塔,在一個塔上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在最后一個塔上。并且規(guī)定,在小圓盤上不能放大圓盤,在三個塔之間一次只能移動一個圓盤。
分析
對圓盤編號1~n,數(shù)字大的圓盤比數(shù)字小的圓盤體積大。并且引入起始塔,中轉(zhuǎn)塔,目標塔的概念,在算法運行的時候,這3個塔的身份可能會互相變換。
1.如果現(xiàn)在在塔A上面只有1個圓盤,那么直接把圓盤移動到塔C即可;
2.如果現(xiàn)在在塔A上面有2個圓盤,那么先把圓盤1從塔A移動到塔B,再把圓盤2從塔A移動到塔C,最后把圓盤1從塔B移動到塔C;
3..……
4.如果塔A有n個圓盤,那么需要先把圓盤n之前的(圓盤n-1~ 圓盤1)從塔A先移動到塔B,再把圓盤n從塔A移動到塔C,最后把放在塔B的(圓盤n-1~圓盤1)從塔B移動到塔C。
可以看出,上面的解法是很典型的分治法算法思想。
因為漢諾塔要求一次只能移動一個圓盤,并且小圓盤上面不能放大圓盤,所以需要把同一個塔最底下的大圓盤上面的圓盤搬走。要想移動圓盤n,那么先要移動其上的(圓盤n-1~ 圓盤1)到中轉(zhuǎn)塔,要想移動(圓盤n-1~ 圓盤1)中的圓盤n-1,那么先要移動其上的(圓盤n-2~ 圓盤1)到中轉(zhuǎn)塔,依次類推……直到只有一個圓盤1,那么直接移動即可。同時,因為我們的目的是把所有的圓盤移動到目標塔,所以在移動完圓盤n到目標塔之后,需要把之前放在中轉(zhuǎn)塔上的(圓盤n-1~圓盤1)移動到目標塔。
可能大家會說了,道理都懂,但是代碼該如何寫呢?其實這也是算法學習中比較占時間的部分,用計算機編程語言把算法表示(寫)出來。下面給大家附上經(jīng)典的實現(xiàn),分析為何要這樣寫,并介紹一種我自己平時理解遞歸所用的思考方法。
Java版算法實現(xiàn)
一般來說,分治法總是結(jié)合遞歸去實現(xiàn),以下示例代碼就用了遞歸結(jié)構(gòu)。
在寫遞歸結(jié)構(gòu)的時候,可以從語義層面去出發(fā)。思考過程如上面分析所示:我們需要寫一個函數(shù),它的作用是把圓盤n~圓盤1由一個起始塔移動到目標塔,由于要求要先移動圓盤n上面的圓盤后才能移動圓盤n,所以需要一個中轉(zhuǎn)塔。經(jīng)過這樣的思考過程之后,我們就可以確定函數(shù)的簽名了,即void hanoi(int n, String sourceTower, String tempTower, String targetTower)。
根據(jù)算法,函數(shù)體里面就可這樣寫(語義層面):
如果當前為盤子1,那么直接移動到目標塔 move(n, sourceTower, targetTower);
否則先將圓盤n-1~圓盤1移動到中轉(zhuǎn)塔hanoi(n - 1, sourceTower, targetTower, tempTower),移動完之后把圓盤n先移動到目標塔move(n, sourceTower, targetTower),再把中轉(zhuǎn)塔上的圓盤n-1~圓盤1移動到目標塔hanoi(n - 1, tempTower, sourceTower, targetTower)。
public class Hanoi {
public static void hanoi(int n, String sourceTower, String tempTower, String targetTower) {
if (n == 1) {
//如果只有一個盤子1,那么直接將其從sourceTower移動到targetTower
move(n, sourceTower, targetTower);
} else {
//將(盤子n-1~盤子1)由sourceTower經(jīng)過targetTower移動到tempTower
hanoi(n - 1, sourceTower, targetTower, tempTower);
//移動盤子n由sourceTower移動到targetTower
move(n, sourceTower, targetTower);
//把之前移動到tempTower的(盤子n-1~盤子1),由tempTower經(jīng)過sourceTower移動到targetTower
hanoi(n - 1, tempTower, sourceTower, targetTower);
}
}
//盤子n的從sourceTower->targetTower的移動
private static void move(int n, String sourceTower, String targetTower) {
System.out.println("第" + n + "號盤子 move:" + sourceTower + "--->" + targetTower);
}
public static void main(String[] args) {
System.out.println("移動漢諾塔的步驟:");
hanoi(3, "a", "b", "c");
}
}
運行結(jié)果:
移動漢諾塔的步驟:
第1號盤子 move:a--->c
第2號盤子 move:a--->b
第1號盤子 move:c--->b
第3號盤子 move:a--->c
第1號盤子 move:b--->a
第2號盤子 move:b--->c
第1號盤子 move:a--->c
可以發(fā)現(xiàn),對于一次調(diào)用hanoi(n, sourceTower, tempTower, targetTower),中轉(zhuǎn)塔是給圓盤n-1~圓盤1用的。從語義上理解,hanoi(n, sourceTower, tempTower, targetTower)最終的調(diào)用效果是圓盤n~圓盤1從起始塔全部移動到了目標塔,并按要求(在小圓盤上不能放大圓盤)所有圓盤都放在目標塔上了。
可能有些同學還是不能理解為什么滿足了在小圓盤上不能放大圓盤的的這個要求,很簡單,因為我們每次想移動圓盤n的時候,都已經(jīng)把圓盤n上面的圓盤拿走了,移動完圓盤n之后,再把其他圓盤移動到圓盤n所在的塔上,所以當然是滿足了在小圓盤上不能放大圓盤的的這個要求。如果還是不能明白的話,可以從2個圓盤的情況和3個圓盤的情況去模擬看看。
附上一個算法調(diào)用的分析圖:

總結(jié)
平時在用分治法解決問題的時候,先根據(jù)分治法的原則把問題逐步拆分。而分治法往往需要和遞歸結(jié)合起來去寫代碼,閱讀或者設計遞歸結(jié)構(gòu)的時候可以從語義的層面出發(fā)去進行。
與分治法一樣,回溯法也可以跟遞歸結(jié)合去實現(xiàn)。但是兩者還是有些許區(qū)別的。
分治法使用遞歸解決問題,問題一般是有解的。如漢諾塔,鏈表反轉(zhuǎn)。
回溯法使用遞歸試探問題的解法,可能會無解。如迷宮問題,深度遍歷。
結(jié)語
其實鉆研算法的確挺燒腦的,而且需要投入比較多的時間。很多人覺得有這時間還不如去看看別的框架更好。其實吧,如果想在計算機領(lǐng)域成為大牛,算法知識是必不可少的。本人很喜歡看動漫《一拳超人》,里面有一句臺詞“興趣使然”。確實,我覺得算法是最能體現(xiàn)計算機科學之魅力的東西,所以研究算法就不會覺得無聊,因為“興趣使然”。
——寫于2018中秋佳節(jié)。