65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript 中的汉诺塔

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (29投票s)

2013年11月7日

CPOL

5分钟阅读

viewsIcon

84904

downloadIcon

2261

本文介绍了一个基于 JavaScript 的汉诺塔问题解决方案

JS_Hanoi Image

引言

本文的目的是提出一个基于 JavaScript 的著名汉诺塔问题的解决方案。该问题要求将一组盘子从一个塔移动到另一个塔,限制条件是任何时候盘子都不能放在比它小的盘子上面。这个问题有一个著名的递归算法。

提出的解决方案(HTML 和 JavaScript 全部包含在一个 HTML 文件中)使用 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);

  // callStack.push([from,to]); // save parameters to callStack array
  
  Hanoi(n-1, via, to , from);
}

算法的输入是 4 个整数参数,如下所示:

  1. n:盘子数量;作为递归的问题规模
  2. from:“from”塔,即盘子放置的塔
  3. to:“to”塔,即盘子最终必须放置的塔
  4. via:“via”塔,在盘子在from塔和to塔之间移动时用作中间位置的塔。

通常(如果我们用 0, 1, 2 表示塔),对于 n 个盘子的初始调用将是 Hanoi(n,0,2,1)

递归算法通常会尝试找到子问题(原始问题的实例,但问题规模更小)。然后用子问题的解来表达原始问题的解。汉诺塔的递归算法基于这样一个观察:“from”塔上最上面的 n-1 个盘子(连同其他两个塔)代表了原始问题的一个更小规模的实例,因此可以通过调用 Hanoi(n-1, 0,1,2) 来解决。这会将盘子移动到中间的塔(#1),并使用另一个塔(#2)作为中间。在此之后,我们可以将第 n 个盘子从塔 #0 移动到塔 #2,然后使用塔 #0 作为中间,通过调用 Hanoi(n-1, 1,2,0)n-1 个盘子从中间的塔移动到最后一个塔。

上述算法编码了前面的描述,但塔的参数被保留为变量而不是特定值。

JavaScript 中的动画处理存在问题

通常动画代码可以利用 JavaScript 定时器函数:setInterval()setTimer()。但是,事情并非如此简单。假设在上面的代码中,对 moveDisk() 函数的调用使用 setInterval() 进行动画处理。会发生一个问题,因为调用者中的代码将继续执行并干扰(混淆共享数据)动画。有人可能会建议调用者稍作等待以允许动画完成。然而,JavaScript 缺乏“真正的”延迟或睡眠机制。如果我们通过自旋循环来实现延迟,它将导致程序停滞,因为所有 JavaScript 代码都在单个线程上执行。

似乎 JavaScript 动画使用了以下模式:如果你开始动画,就不要做任何其他事情

对于汉诺塔算法,我们希望为一次 moveDisk() 调用制作动画,然后返回调用者,直到遇到下一个 moveDisk() 调用,为其制作动画,依此类推。但是,使用 JavaScript 动画,我们必须以某种方式遵循此模式:为 moveDisk() 制作动画,为下一个 moveDisk() 制作动画,依此类推。换句话说,每次调用 moveDisk() 时,都应该(在完成后)发出下一个 moveDisk() 的调用。但是如何找到所有这些 moveDisk() 调用呢?经过一番思考,我问自己:为什么不使用栈(callStack)来保存 moveDisk() 的“from,to”调用参数呢?使用栈还将保留调用的顺序。这正是我所做的,也就是说,在上面的代码中注释掉 moveDisk(); 行,并取消注释 callStack.push([from,to]); 行。现在,修改后的 Hanoi() 函数是从下面给出的 executeHanoi() 函数调用的。

以下列表显示了我的脚本的完整源代码。

var callcallStack;   

function executeHanoi()
{ //  Some initialization code goes here
 
  callStack=[];  // callStack array is global

  Hanoi(diskCount, 0,2,1);
    
  moveDisk();  // moveDisk takes its parameters from callStack
}

function moveDisk()
{  if (callStack.length==0) return; 

   var param = callStack.shift(); // Get call parameters from callStack
   // Note: throughout the code, I use fromBar, toBar to refer to towers
   fromBar = param[0];
   toBar = param[1];
   // find top element in fromBar
   var elem = document.getElementById(barsInfo[fromBar].disks.pop());  
    
   moveInfo = { elem: elem,
                fromBar: fromBar,
                toBar: toBar,
                whichPos: "top", // element position property for movement
                dir: -1,  // 1 or -1
                state: "up", // move upward
                endPos:60    // end position (in pixels) for move upward
             } 

   myTimer = setInterval(animateMove,speed); // Start animation
}

调用 Hanoi() 后,callStack 将包含每个 moveDisk() 调用的一个条目。这些条目的处理由 moveDisk() 函数处理。moveDisk() 函数从 callStack 中检索一个条目,并为 animateMove() 函数设置一个 moveInfo 对象以传递数据。动画由 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)))
   {  // alert(moveInfo.state); 
     if (moveInfo.state == "up")
     {  moveInfo.state = "hor";
        moveInfo.whichPos ="left";
        moveInfo.dir = 1;
        if ( moveInfo.fromBar >  moveInfo.toBar) moveInfo.dir = -1;
        //alert("toBar:" + moveInfo.toBar);
        var toBar = document.getElementById("bar"+ moveInfo.toBar);
        // alert(toBar.offsetLeft); 
 
        moveInfo.endPos = toBar.offsetLeft - 
               Math.floor(elem.offsetWidth/2)+15; // 15 is half of tower width
        return;
     }

     else if (moveInfo.state  == "hor" ) // move down
     {  // alert("here");
        moveInfo.state = "down";
        moveInfo.whichPos ="top";
        moveInfo.dir = 1; 
        //alert(elem.offsetHeight);
        moveInfo.endPos = document.getElementById("bottombar").offsetTop - 
               (barsInfo[moveInfo.toBar].disks.length+1)*elem.offsetHeight;
        return; 
     }

     else // end of current call to moveDisk, issue next call
     {  clearInterval(myTimer);  // cancel timer
        barsInfo[moveInfo.toBar].disks.push(elem.id);
        moveDisk();
        return; 
     }
   }

   pos = pos + dir*moveInc;
   elem.style[moveInfo.whichPos] = pos+ "px"; 
}

animateMove() 函数基本上会将一个盘子向上移动,然后向右或向左移动(如果 toBar < fromBar),然后向下移动,最后发出对 moveDisk() 的下一个调用。请注意,在进行对 moveDisk() 的下一次调用之前,会取消计时器(使用 clearInterval(myTimer) 调用)。这是为了防止 animateMove() 错误地使用 moveInfo 对象。

局限性,建议

所提供的代码已在最新版本的流行浏览器中进行了测试。它在 IE(版本 11)、Chrome(版本 30)和 Firefox(版本 24)中运行完美。我们解决方案中的动画受 JavaScript setInterval() 函数的计时器精度的限制,该精度不能小于一毫秒。使用 window.requestAnimationFrame() 函数可能可以实现更快的动画。最后,可以通过使用 3D 图像作为盘子,在盘子向上和向下移动时部分被塔覆盖,来增强图形效果。

历史

  • 2013 年 11 月 7 日:初始版本。 
  • 2013 年 11 月 15 日:更新了更好的图形。
  • 2015 年 11 月 7 日:对程序代码进行了少量更新,以便在单击“开始”按钮时,所有盘子都移动到最左边的列。
© . All rights reserved.