汉诺塔问题的递归动画演示

汉诺塔问题是一个经典的递归问题,要求将一组盘子从一个柱子移动到另一个柱子,同时遵守规则:任何时候都不能将较大的盘子放在较小的盘子上面。这个问题与一个众所周知的递归算法相关联。本文将展示如何使用JavaScriptsetInterval函数来实现算法的动画演示。

虽然动画递归汉诺塔算法的想法并不新颖,但作者大约在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); }

算法的输入是四个整数参数,如下:

  • n: 盘子的数量;作为递归问题的规模
  • from: “from”柱子是盘子的初始位置
  • to: “to”柱子是盘子的最终位置
  • via: “via”柱子是盘子在移动过程中的中间位置

通常(如果用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中的动画处理问题

通常动画代码可以利用JavaScript的定时器函数:setIntervalsetTimeout。然而,事情并不那么简单。假设在上面的代码中,函数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"; }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485