汉诺塔可以说是一个非常经典的递归问题了,在很多书上也会把它作为递归的入门题,用来介绍递归的基本概念。
故事的背景和问题的具体内容就不在这里介绍了,我觉得我并没有搞明白递归是怎么一回事,比起迭代,递归从头到脚都透露着一种神秘;虽然我想不到,但是递归的逻辑很清晰。这两者并不矛盾。
抒情完毕,说正事。如何解决汉诺塔问题(在这里相当于是把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, }); } } }