汉诺塔问题是一个经典的递归问题,要求将一组盘子从一个柱子移动到另一个柱子,同时遵守规则:任何时候都不能将较大的盘子放在较小的盘子上面。这个问题与一个众所周知的递归算法相关联。本文将展示如何使用JavaScript的setInterval
函数来实现算法的动画演示。
虽然动画递归汉诺塔算法的想法并不新颖,但作者大约在15年前使用Visual Basic实现了它。相关视频可以在找到。
汉诺塔问题的著名递归算法可以用以下JavaScript函数表达:
function Hanoi(n, from, to, via) {
if (n == 0) return;
Hanoi(n - 1, from, via, to);
moveDisk(from, to);
Hanoi(n - 1, via, to, from);
}
算法的输入是四个整数参数,如下:
通常(如果用0,1,2表示柱子),n个盘子的初始调用将是Hanoi(n, 0, 2, 1)
。
递归算法通常试图找到子问题(原始问题的实例,但问题规模更小)。然后以子问题的解决方案来表达原始问题的解决方案。汉诺塔的递归算法基于观察到的,“from”柱子顶部的n-1个盘子(连同其他两个柱子)代表了原始问题的较小规模实例,因此可以通过调用Hanoi(n-1, 0, 1, 2)
来解决。这将把盘子移动到中间柱子(#1),使用另一个柱子(#2)作为中间。之后,可以将第n个盘子从柱子#0移动到柱子#2,然后通过调用Hanoi(n-1, 1, 2, 0)
将所有n-1个盘子从中间柱子移动到最后一个柱子,使用柱子#0作为中间。
上述算法编码了前面的描述,但将柱子的参数保持为变量,而不是特定值。
通常动画代码可以利用JavaScript的定时器函数:setInterval
或setTimeout
。然而,事情并不那么简单。假设在上面的代码中,函数moveDisk()
的调用使用setInterval()
进行动画化。会出现一个问题,因为调用者的代码会继续执行并干扰(搞乱共享数据)动画。有人可能会建议调用者休眠一段时间以允许动画完成。然而,JavaScript缺乏“真实”的延迟或休眠机制。如果通过自旋循环来实现延迟,它会使事情停滞不前,因为整个JavaScript代码在单个线程上执行。
看来JavaScript动画使用的模式是:如果开始动画,不要做其他任何事情。对于汉诺塔算法,想要动画化一次调用moveDisk()
,返回调用者,直到遇到下一次调用moveDisk()
,动画化它,等等。但是,使用JavaScript动画,不得不遵循模式:动画化moveDisk()
,动画化下一个moveDisk()
,等等。换句话说,每次调用moveDisk()
都应该在完成时发出下一次调用moveDisk()
。但是,如何找到所有这些moveDisk()
调用。经过一番思考,自问:为什么不使用栈(callStack)来保存“from,to”调用参数到moveDisk()
。使用栈也会保留调用的顺序。这正是所做的,即在上面的代码注释中moveDisk();
并取消注释callStack.push([from,to]);
。现在修改后的Hanoi()函数是从函数executeHanoi()
调用的,如下所示。
var callStack;
function executeHanoi() {
// Some initialization code goes here
callStack = [];
Hanoi(diskCount, 0, 2, 1);
moveDisk(); // moveDisk takes its parameters from callStack
}
function moveDisk() {
if (callStack.length == 0) return;
var param = callStack.shift();
var fromBar = param[0];
var toBar = param[1];
var elem = document.getElementById(barsInfo[fromBar].disks.pop());
var moveInfo = {
elem: elem,
fromBar: fromBar,
toBar: toBar,
whichPos: "top",
dir: -1,
state: "up",
endPos: 60
};
var myTimer = setInterval(animateMove, speed); // Start animation
}
在调用Hanoi()之后,callStack将有一个条目用于每个moveDisk()
调用。这些条目的处理由函数moveDisk()
处理。函数moveDisk()
从callStack中检索一个条目,并设置一个对象moveInfo
,用于传递animateMove()
函数所需的数据。动画是通过行myTimer = setInterval(animateMove,speed)
开始的。
以下代码列表显示了animateMove()
函数的代码。
function animateMove() {
var elem = moveInfo.elem;
var dir = moveInfo.dir;
var styleInfo = window.getComputedStyle(elem);
var pos = parseInt(styleInfo[moveInfo.whichPos]);
if (((dir == 1) && (pos >= moveInfo.endPos)) || ((dir == -1) && (pos <= moveInfo.endPos))) {
if (moveInfo.state == "up") {
moveInfo.state = "hor";
moveInfo.whichPos = "left";
moveInfo.dir = 1;
if (moveInfo.fromBar > moveInfo.toBar) moveInfo.dir = -1;
var toBar = document.getElementById("bar" + moveInfo.toBar);
moveInfo.endPos = toBar.offsetLeft - Math.floor(elem.offsetWidth / 2) + 15;
return;
} else if (moveInfo.state == "hor") {
moveInfo.state = "down";
moveInfo.whichPos = "top";
moveInfo.dir = 1;
moveInfo.endPos = document.getElementById("bottombar").offsetTop - (barsInfo[moveInfo.toBar].disks.length + 1) * elem.offsetHeight;
return;
} else {
clearInterval(myTimer);
barsInfo[moveInfo.toBar].disks.push(elem.id);
moveDisk();
return;
}
}
pos = pos + dir * moveInc;
elem.style[moveInfo.whichPos] = pos + "px";
}