使用函数:汉诺塔

作者:Dave Taylor

在本文中,我认为回顾一些 shell 脚本的基础知识并了解函数的工作原理将是有益的。大多数脚本编写者可能避免使用函数,因为它有点违反脚本演变的方式,即在文件中捕获的命令行上的一系列命令。

然而,与通用编程一样,如果您有一个甚至几行的代码块被多次调用,在脚本中的多个位置,那么将其转换为函数会更有意义。

因此,通用语法是


MyFunction() {
  commands to execute
}

在主脚本或任何其他函数或函数本身中,通过简单地引用其名称来调用函数


echo "before call to MyFunction"
MyFunction
echo "after call to MyFunction"

很简单。位置参数的给定方式与命令标志传递给整个脚本的方式完全类似,如 $1、$2、$3 等等。

这意味着在函数内部,我可以编写


if [ -n "$1" ] ; then
  echo "I was given $1 as my first parameter"
fi

好吧,我有点懒惰了,测试 $# 将是确定是否将位置参数传递给函数的更好方法,所以让我们将其重写为


if [ $# -gt 0 ] ; then
  echo "I was given $# parameters"
fi

shell 函数的最大限制是它们只能返回 0-127 的整数值,其中典型的脚本实际上使用 0 = false 或失败,1 = true 或成功。或者,许多脚本编写者完全忽略返回值,并使用全局变量来传回值,特别是当它们是字符串值时,否则在函数中是无法处理的。

与 Ruby、Java 或 C++ 中的最佳实践相比,函数操作全局变量有点草率,但你必须在你拥有的基础上工作,对吧?

汉诺塔

为了使本专栏更有趣,我将回顾一个经典的递归程序,看看如何使其作为 shell 脚本工作。该程序称为汉诺塔,您可能见过儿童玩具版的。它是一组不同尺寸的圆盘和三个柱子,目标是将所有圆盘从一个柱子移动到另一个柱子,同时永远不要违反大圆盘永远不能放在小圆盘之上的规则。如果柱子标记为 0、1 和 2,则最简单的一个圆盘的情况是简单地将其从柱子 0 移动到柱子 2。对于两个圆盘,第一个(较小的)圆盘从 0 移动到 1;第二个圆盘从 0 移动到 2,然后第一个圆盘从 1 移动到 2 并放在其顶部。

有一个简洁的迭代解决方案



#!/bin/sh
/bin/echo -n "Towers of Hanoi. How many disks? "
read disk
for (( x=1; x < (1 << $disk ); x++ )) ; do
  i=$((($x & $x - 1 ) % 3))
  j=$(((($x | $x - 1 ) + 1 ) % 3))
  echo "Move from tower $i to tower $j"
done

运行时,这个非常简短的脚本产生了我想得到的结果,汉诺塔问题的逐步解决方案


Towers of Hanoi. How many disks? 4
Move from tower 0 to tower 2
Move from tower 0 to tower 1
Move from tower 2 to tower 1
Move from tower 0 to tower 2
Move from tower 1 to tower 0
Move from tower 1 to tower 2
Move from tower 0 to tower 2
Move from tower 0 to tower 1
Move from tower 2 to tower 1
Move from tower 2 to tower 0
Move from tower 1 to tower 0
Move from tower 2 to tower 1
Move from tower 0 to tower 2
Move from tower 0 to tower 1
Move from tower 2 to tower 1

事实证明,n 个圆盘的解法在数学上是 2**n + 1 步。简而言之,20 个圆盘将需要惊人的 1,048,577 步。这比我对益智游戏更有耐心;我不知道你怎么看。

Shell 脚本函数中的递归

然而,引入汉诺塔谜题的目的是为了演示 shell 脚本中一个巧妙的递归技巧,所以让我们看看它是如何工作的。

基本算法有三个步骤

  1. 将最上面的圆盘从源柱移动到临时柱。

  2. 将下一个最大的圆盘从源柱移动到目标柱。

  3. 将最上面的圆盘从临时柱移动到目标柱。

有各种网站用图示说明了这是如何工作的,但展示我的脚本可能会更容易,这样你就可以看到我在说什么


moves=0     # start with no moves
hanoi()
{
    if [ $1 -gt 0 ] ; then
      hanoi "$(($1-1))" $2 $4 $3
      echo move $2 "-->" $3
      moves=$(( $moves + 1 ))
      hanoi "$(($1-1))" $4 $3 $2
    fi
}
/bin/echo -n "Towers of Hanoi. How many disks? "
read disks
hanoi $disks 1 2 3
echo "It took $moves moves to solve Towers for $disks disks."

请注意,您必须为递归汉诺塔函数提供四个参数,并且您必须使用以下初始调用来“启动泵”


hanoi $disks 1 3 2

参数按顺序排列,分别是圆盘的数量和您将使用的三个柱子的标识。对于四个圆盘和三个柱子,解决方案是


Towers of Hanoi. How many disks? 4
move 1 --> 3
move 1 --> 2
move 3 --> 2
move 1 --> 3
move 2 --> 1
move 2 --> 3
move 1 --> 3
move 1 --> 2
move 3 --> 2
move 3 --> 1
move 2 --> 1
move 3 --> 2
move 1 --> 3
move 1 --> 2
move 3 --> 2

解决四个圆盘的汉诺塔问题需要 15 步。仔细查看代码,您会意识到您实际上可以通过更改脚本底部的初始调用来使用柱子的助记符名称,这使得输出作为解决方案更易于理解,这次仅针对三个圆盘


Towers of Hanoi. How many disks? 3
move source --> temp
move source --> destination
move temp --> destination
move source --> temp
move destination --> source
move destination --> temp
move source --> temp
It took 7 moves to solve Towers of Hanoi for 3 disks.

虽然您可能不会遇到需要在 shell 脚本中创建递归函数的情况,但更通用的函数创建和使用肯定可以使您的脚本更高效且更易于理解。至于汉诺塔?好吧,你有一个更好的算法吗?它是计算机科学教育的支柱,所以我敢打赌你们很多人过去都解决过这个问题。

感谢 Kamaraj Subramanian 的迭代汉诺塔脚本和 phoxis 的递归汉诺塔脚本——它们被证明是我本月创作的良好起点。

Dave Taylor 在 UNIX 和 Linux 系统上破解 shell 脚本已经很长时间了。他是 Learning Unix for Mac OS XWicked Cool Shell Scripts 的作者。您可以在 Twitter 上找到他,账号是 @DaveTaylor,您可以通过他的技术问答网站联系他:Ask Dave Taylor

加载 Disqus 评论