shell 递归求阶乘

21 篇文章 0 订阅
21 篇文章 1 订阅

本文将逐一探讨在 bash 中编写递归函数时需要注意的返回值、参数传递问题

返回值问题

1.递归求阶乘的初步构思

 1  #!/bin/bash
 2
 3  factorial()
 4  {
 5    i=$1
 6
 7    if [ $i -eq 0 ]
 8    then
 9      return 1;
10    else
11      factorial `expr $i - 1`
12      return `expr $i \* $? `
13    fi
14  }
15
16  if [ -z $1 ]
17  then
18    echo "Need one parameter."
19    exit 1
20  fi
21
22  factorial $1
23
24  echo $?

这个脚本看上去并没有什么问题:递归函数的参数传递和普通函数没什么不同,返回值是通过获取 $? 的值实现的,这是利用了执行命令的退出码。然而,最终的结果却显然是错误的。调试一下就会发现,当递归回溯到尽头时,变量 i 的值被修改为 0;而退出上次函数调用之后,变量 i 的新值也被带了回来

这段脚本问题的根源在于变量的作用域:在 shell 脚本中,不管是否在函数中定义,变量默认就是全局的,一旦定义之后,对于此后执行的命令全部可见。bash 也支持局部变量,不过需要使用 local 关键字进行显式地声明。local 是bash 中的一个内嵌命令,其作用是将变量的作用域设定为只有对本函数及其子进程可见。局部变量只能在变量声明的代码块中可见,这也就意味着在函数内声明的局部变量只能在函数代码块中才能被访问,它们并不会污染同名全局变量。因此为了解决上面这个程序的问题,我们应该使用 local 关键字将 i 声明为局部变量。

2. 递归函数中使用 local 关键字声明局部变量

 1  #!/bin/bash
 2
 3  factorial()
 4  {
 5    local i=$1
 6
 7    if [ $i -eq 0 ]
 8    then
 9      return 1;
10    else
11      factorial `expr $i - 1`
12      return `expr $i \* $? `
13    fi
14  }
15
16  if [ -z $1 ]
17  then
18    echo "Need one parameter."
19    exit 1
20  fi
21
22  factorial $1
23
24  echo $?

这下 5 的阶乘计算对了,但是稍微大一点的数字都会出错,比如 6 的阶乘计算出来是错误的 208。这个问题的原因在于脚本中传递函数返回值的方式存在缺陷,$? 所能传递的最大值是 255,超过该值就没有办法利用这种方式来传递返回值了。解决这个问题的方法有两种,一种是利用全局变量,另外一种则是利用其他方式进行周转(例如标准输入输出设备)。

3.使用全局变量传递返回值

 1  #!/bin/bash
 2
 3  factorial()
 4  {
 5    local i=$1
 6
 7    if [ $i -eq 0 ]
 8    then
 9      rtn=1
10    else
11      factorial `expr $i - 1`
12      rtn=`expr $i \* $rtn `
13    fi
14
15    return $rtn
16  }
17
18  if [ -z $1 ]
19  then
20    echo "Need one parameter."
21    exit 1
22  fi
23
24  factorial $1
25
26  echo $rtn

4. 利用标准输入输出设备传递返回值

 1  #!/bin/bash
 2
 3  factorial()
 4  {
 5    local i=$1
 6
 7    if [ $i -eq 0 ]
 8    then
 9      echo 1
10    else
11      local j=`expr $i - 1`
12      local k=`factorial $j` 
13      echo `expr $i \* $k `
14    fi
15  }
16
17  if [ -z $1 ]
18  then
19    echo "Need one parameter."
20    exit 1
21  fi
22
23  rtn=`factorial $1`
24  echo $rtn

尽管利用全局变量或标准输入输出设备都可以解决如何正确传递返回值的问题,但是它们却各有缺点:如果利用全局变量,由于全局变量对此后的程序全部可见,一旦被其他程序修改,就会出错,所以编写代码时需要格外小心,特别是在编写复杂的递归程序的时候;如果利用标准输入输出设备,那么递归函数中就存在诸多限制,例如任何地方都不能再向标准输出设备中打印内容,否则就可能被上一层调用当作正常输出结果读走了,另外速度方面也可能存在严重问题。

参数传递问题

在设计函数时,除了返回值之外,我们可能还希望所调用的函数还能够返回其他一些信息。例如,在上面的阶乘递归函数中,我们除了希望计算最后的结果之外,还希望了解这个函数一共被调用了多少次。熟悉 c 语言之类的读者都会清楚,这可以通过传递一个指针类型的参数实现。然而,在 bash 中并不支持指针,它提供了另外一种在解释性语言中常见的设计:间接变量引用(indirect variable reference)。让我们看一下下面这个例子:

var2=$var3
var1=$var2
其中变量 var2 的存在实际上就是为了让 var1 能够访问 var3,实际上也可以通过 var1 直接引用 var3 的值,方法是 var1=\$$var3(请注意转义字符是必须的,否则 $$ 符号会被解释为当前进程的进程 ID 号),这种方式就称为间接变量引用。从 bash2 开始,对间接变量引入了一种更为清晰的语法,方法是 var1=${!var3}。

5. 利用间接变量引用统计递归函数的调用次数

 1  #!/bin/bash
 2
 3  factorial()
 4  {
 5    local i=$1
 6    local l=$2
 7
 8    if [ $i -eq 0 ]
 9    then
10      eval ${l}=1
11      rtn=1
12    else
13      factorial `expr $i - 1` ${l}
14      rtn=`expr $i \* $rtn `
15      
16      local k=${!l}
17      eval ${l}=`expr ${k} + 1`
18    fi
19
20    return $rtn
21  }
22
23  if [ -z $1 ]
24  then
25    echo "Need one parameter."
26    exit 1
27  fi
28
29  level=0
30  factorial $1 level
31
32  echo "The factorial of $1 is : $rtn"
33  echo " the function of factorial is invoked $level times."

在上面我们曾经介绍过,为了解决变量作用域和函数返回值的问题,在递归函数中我们使用 local 声明局部变量,并采用全局变量来传递返回值。但是随着调用关系变得更加复杂,全局变量的值有可能在其他地方被错误地修改。实际上,使用局部变量也存在一个问题,下面让我们来看一下给出的例子。

6. 查找字符串在文件中是否存在,并计算所在行数和出现次数

 1  #!/bin/bash
 2
 3  GetLine()
 4  {
 5    string=$1
 6    file=$2
 7
 8    line=`grep -n $string $file`
 9    if [ $? -eq 0 ]
10    then
11      printf "$string is found as the %drd line in $file \n" `echo $line \
                     | cut -f1 -d:`
12      num=`grep $string $file | wc -l`
13      rtn=0
14    else
15      printf "$string is not found in $file \n"
16      num=0
17      rtn=1
18    fi
19
20    return $rtn;
21  }
22
23  if [ ! -f testfile.$$ ]
    24  then
    25    cat >> testfile.$$ <<EOF
26  first line .
27  second line ..
28  third line ...
29  EOF
30  fi
31
32  num=0
33  rtn=0
34  for i in "second" "six" "line"
35  do
36    echo
37    GetLine $i testfile.$$
38    echo "return value: $rtn"
39
40    if [ $num -gt 0 ]
41    then
42      echo "$num occurences found totally."
43    fi
44  done

这段程序的目的是查找某个字符串在指定文件中是否存在,如果存在,就计算第一次出现的行数和总共出现的次数。为了说明局部变量和后面提到的子函数的问题,我们故意将对出现次数的打印也放到了 GetLine 函数之外进行处理。6 中全部使用全局变量,并没有出现什么问题。下面让我们来看一下将 GetLine 中使用的局部变量改用 local 声明后会出现什么问题,修改后的代码和执行结果下。

7. 使用 local 声明局部变量需要注意的问题

 1  #!/bin/bash
 2
 3  GetLine()
 4  {
 5    local string=$1
 6    local file=$2
 7
 8    local line=`grep -n $string $file`
 9    if [ $? -eq 0 ]
10    then
11      printf "$string is found as the %drd line in $file \n" `echo $line \
                     | cut -f1 -d:`
12      num=`grep $string $file | wc -l`
13      rtn=0
14    else
15      printf "$string is not found in $file \n"
16      num=0
17      rtn=1
18    fi
19
20    return $rtn;
21  }
22
23  if [ ! -f testfile.$$ ]
    24  then
    25    cat >> testfile.$$ <<EOF
26  first line .
27  second line ..
28  third line ...
29  EOF
30  fi
31
32  num=0
33  rtn=0
34  for i in "second" "six" "line"
35  do
36    echo
37    GetLine $i testfile.$$
38    echo "return value: $rtn"
39
40    if [ $num -gt 0 ]
41    then
42      echo "$num occurences found totally."
43    fi
44  done

7的运行结果显示,在文件中搜索 six 关键字时的结果是错误的,调试会发现,问题的原因在于:第 8 行使用 local 将 line 声明为局部变量,并将 grep 命令的执行结果赋值给 line 变量。然而不论 grep 是否成功在文件中找到匹配项(grep 程序找到匹配项返回值为 0,否则返回值为 1),第 9 行中 ?08使grepgrepline9 ? 的值实际上是执行 local 命令的返回值,不管 grep 命令的结果如何,它总是 0。

要解决这个问题,可以将第 8 行的命令拆分开,首先使用单独一行将变量 line 声明为 local的,然后再执行这条 grep 命令,并将结果赋值给变量 line(此时前面不能加上 local)。

解决变量作用域的另外一种方法是使用子 shell。所谓子 shell 是在当前 shell 环境中启动一个子 shell 来执行所调用的命令或函数,这个函数中所声明的所有变量都是局部变量,它们不会污染原有 shell 的名字空间。

8. 利用子 shell 实现局部变量

 1  #!/bin/bash
 2
 3  GetLine()
 4  {
 5    string=$1
 6    file=$2
 7
 8    line=`grep -n $string $file`
 9    if [ $? -eq 0 ]
10    then
11      printf "$string is found as the %drd line in $file \n" `echo $line  \
                 | cut -f1 -d:`
12      num=`grep $string $file | wc -l`
13      rtn=0
14    else
15      printf "$string is not found in $file \n"
16      num=0
17      rtn=1
18    fi
19
20    return $rtn;
21  }
22
23  if [ ! -f testfile.$$ ]
    24  then
    25    cat >> testfile.$$ <<EOF
26  first line .
27  second line ..
28  third line ...
29  EOF
30  fi
31
32  num=0
33  rtn=0
34  for i in "second" "six" "line"
35  do
36    echo
37    (GetLine $i testfile.$$)
38    echo "return value: $? (rtn = $rtn)"
39
40    if [ $num -gt 0 ]
41    then
42      echo "$num occurences found totally."
43    fi
44  done

在8 中,GetLine 函数并不需要任何变化,变量定义和程序调用都沿用正常方式。唯一的区别在于调用该函数时,要将其作为一个子 shell 来调用(请注意第 37 行两边的圆括号)。另外一个问题是在子 shell 中修改的所有变量对于原有 shell 来说都是不可见的,这也就是为什么在第 38 行要通过 $? 来检查返回值,而 rtn 变量的值却是错误的。另外由于 num 在 GetLine 函数中也被当作是局部变量,同样无法将修改后的值传出来,因此也并没有打印所匹配到的 line 的数目是 3 行的信息。

解决上面这个问题就只能使用前面提到的利用标准输入输出设备的方法了,否则即使使用间接变量引用也无法正常工作。9 给出了一个使用间接变量引用的例子,尽管我们使用不同的名字来命名全局变量和局部变量,从而确保不会引起同名混淆,但是依然无法正常工作。原因同样在于 GetLine 函数是在另外一个子进程中运行的,它对变量所做的更新随着子 shell 的退出就消失了。

9. 利用间接变量索引也无法解决子 shell 通过变量回传值的问题

 1  #!/bin/bash
 2
 3  GetLine()
 4  {
 5    string=$1
 6    file=$2
 7    num=$3
 8    rtn=$4
 9
10    line=`grep -n $string $file`
11    if [ $? -eq 0 ]
12    then
13      printf "$string is found as the %drd line in $file \n"  \
                   `echo $line | cut -f1 -d:`
14      eval ${num}=`grep $string $file | wc -l`
15      eval ${rtn}=0
16    else
17      printf "$string is not found in $file \n"
18      eval ${num}=0
19      eval ${rtn}=1
20    fi
21
22    return ${!rtn};
23  }
24
25  if [ ! -f testfile.$$ ]
    26  then
    27    cat >> testfile.$$ <<EOF
28  first line .
29  second line ..
30  third line ...
31  EOF
32  fi
33
34  g_num=0
35  g_rtn=0
36  for i in "second" "six" "line"
37  do
38    echo
39    (GetLine $i testfile.$$ g_num g_rtn)
40    echo "return value: $? (g_rtn = $g_rtn)"
41
42    if [ $g_num -gt 0 ]
43    then
44      echo "$g_num occurence(s) found totally."
45    fi
46  done
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值