漢諾塔可以說是一個非常經典的遞歸問題了,在很多書上也會把它作爲遞歸的入門題,用來介紹遞歸的基本概念。
故事的背景和問題的具體內容就不在這裏介紹了,我覺得我並沒有搞明白遞歸是怎麼一回事,比起迭代,遞歸從頭到腳都透露着一種神祕;雖然我想不到,但是遞歸的邏輯很清晰。這兩者並不矛盾。
抒情完畢,說正事。如何解決漢諾塔問題(在這裏相當於是把A移到C,並且直接在C上修改)?很簡單:
function hanota(A: number[], B: number[], C: number[]): void {
C.push(...A);
}
開玩笑的。正經的遞歸解法大概是這樣:
function hanota(A: number[], B: number[], C: number[]): void {
function move(n: number, A: number[], B: number[], C: number[]) {
if (n == 1) C.push(A.pop()!);
else {
move(n - 1, A, C, B);
C.push(A.pop()!);
move(n - 1, B, A, C);
}
}
move(A.length, A, B, C);
}
怎麼理解這個問題?先把上面n - 1
個較小的塊當成一個整體,從A先搬到B暫存,然後把A最底下那個大塊搬到目標點C,然後再把暫存區B裏的剩下的塊全搬到目標點C。就是這樣。
邏輯是那個邏輯,但生不生動,又是另一碼事了。看來看去,覺得還是所謂“冰箱裏的大象”的解釋最好,在此分享一下(轉載自如何理解漢諾塔的遞歸? - IT邊界的回答 - 知乎) 。怎麼把大象放進冰箱?
-
把冰箱門打開;
-
把大象裝進來;
-
把冰箱門關上。
其實,解決漢諾塔問題,我們只是在不斷地開門關門。我所希望的,不過是按照順序,每次把最底下的那個放到C而已。
這是最常規的遞歸解法。如果用棧呢?雖然說遞歸棧也是“棧”,但和真正的stack還是不太一樣的。當然,理論上所有能用遞歸解決的問題都可以用棧來解決。我在這裏給出一種可能的實現:
function hanota(A: number[], B: number[], C: number[]): void {
const stack: any[] = [[A.length, A, B, C]];
while (stack.length) {
const top = stack.pop()!,
n = top[0];
if (n === 1) top[3].push(top[1].pop()!); // C.push(A.pop()!)
else {
stack.push([n - 1, top[2], top[1], top[3]]); // [n-1, B, A, C]
stack.push([1, top[1], top[2], top[3]]); // [ 1, A, B, C]
stack.push([n - 1, top[1], top[3], top[2]]); // [n-1, A, C, B]
}
}
}
立刻就跟剛纔的遞歸一樣了。當然,因爲這裏用的是真正的棧,所以在進行“遞歸”的時候要倒序入棧,才能保證有序進行。
之前還寫了一個可讀性稍好的版本,也拿出來以供參考:
interface StackItem {
n: number;
src: number[];
cache: number[];
target: number[];
}
function hanota(A: number[], B: number[], C: number[]): void {
const stack: StackItem[] = [{ n: A.length, src: A, cache: B, target: C }];
while (stack.length) {
const top = stack.pop()!;
if (top.n === 1) top.target.push(top.src.pop()!);
else {
stack.push({
n: top.n - 1,
src: top.cache,
cache: top.src,
target: top.target,
});
stack.push({ n: 1, src: top.src, cache: top.cache, target: top.target });
stack.push({
n: top.n - 1,
src: top.src,
cache: top.target,
target: top.cache,
});
}
}
}