shell 递归求阶乘
2017-07-08 12:51
295 查看
本文将逐一探讨在 bash 中编写递归函数时需要注意的返回值、参数传递问题
这个脚本看上去并没有什么问题:递归函数的参数传递和普通函数没什么不同,返回值是通过获取 $? 的值实现的,这是利用了执行命令的退出码。然而,最终的结果却显然是错误的。调试一下就会发现,当递归回溯到尽头时,变量 i 的值被修改为 0;而退出上次函数调用之后,变量 i 的新值也被带了回来
这段脚本问题的根源在于变量的作用域:在 shell 脚本中,不管是否在函数中定义,变量默认就是全局的,一旦定义之后,对于此后执行的命令全部可见。bash 也支持局部变量,不过需要使用 local 关键字进行显式地声明。local 是bash 中的一个内嵌命令,其作用是将变量的作用域设定为只有对本函数及其子进程可见。局部变量只能在变量声明的代码块中可见,这也就意味着在函数内声明的局部变量只能在函数代码块中才能被访问,它们并不会污染同名全局变量。因此为了解决上面这个程序的问题,我们应该使用 local 关键字将 i 声明为局部变量。
2. 递归函数中使用 local 关键字声明局部变量
这下 5 的阶乘计算对了,但是稍微大一点的数字都会出错,比如 6 的阶乘计算出来是错误的 208。这个问题的原因在于脚本中传递函数返回值的方式存在缺陷,$? 所能传递的最大值是 255,超过该值就没有办法利用这种方式来传递返回值了。解决这个问题的方法有两种,一种是利用全局变量,另外一种则是利用其他方式进行周转(例如标准输入输出设备)。
3.使用全局变量传递返回值
4. 利用标准输入输出设备传递返回值
尽管利用全局变量或标准输入输出设备都可以解决如何正确传递返回值的问题,但是它们却各有缺点:如果利用全局变量,由于全局变量对此后的程序全部可见,一旦被其他程序修改,就会出错,所以编写代码时需要格外小心,特别是在编写复杂的递归程序的时候;如果利用标准输入输出设备,那么递归函数中就存在诸多限制,例如任何地方都不能再向标准输出设备中打印内容,否则就可能被上一层调用当作正常输出结果读走了,另外速度方面也可能存在严重问题。
5. 利用间接变量引用统计递归函数的调用次数
在上面我们曾经介绍过,为了解决变量作用域和函数返回值的问题,在递归函数中我们使用 local 声明局部变量,并采用全局变量来传递返回值。但是随着调用关系变得更加复杂,全局变量的值有可能在其他地方被错误地修改。实际上,使用局部变量也存在一个问题,下面让我们来看一下给出的例子。
6. 查找字符串在文件中是否存在,并计算所在行数和出现次数
这段程序的目的是查找某个字符串在指定文件中是否存在,如果存在,就计算第一次出现的行数和总共出现的次数。为了说明局部变量和后面提到的子函数的问题,我们故意将对出现次数的打印也放到了 GetLine 函数之外进行处理。6 中全部使用全局变量,并没有出现什么问题。下面让我们来看一下将 GetLine 中使用的局部变量改用 local 声明后会出现什么问题,修改后的代码和执行结果下。
7. 使用 local 声明局部变量需要注意的问题
7的运行结果显示,在文件中搜索 six 关键字时的结果是错误的,调试会发现,问题的原因在于:第 8 行使用 local 将 line 声明为局部变量,并将 grep 命令的执行结果赋值给 line 变量。然而不论 grep 是否成功在文件中找到匹配项(grep 程序找到匹配项返回值为 0,否则返回值为 1),第 9 行中 ?的值总是0。实际上,第8行相当于执行了两条语句:第一条语句使用grep在文件中查找匹配项,第二条语句将grep命令的结果赋值给变量line,并设定其作用域只对于本函数及其子进程可见。因此第9行命令中? 的值实际上是执行 local 命令的返回值,不管 grep 命令的结果如何,它总是 0。
要解决这个问题,可以将第 8 行的命令拆分开,首先使用单独一行将变量 line 声明为 local的,然后再执行这条 grep 命令,并将结果赋值给变量 line(此时前面不能加上 local)。
解决变量作用域的另外一种方法是使用子 shell。所谓子 shell 是在当前 shell 环境中启动一个子 shell 来执行所调用的命令或函数,这个函数中所声明的所有变量都是局部变量,它们不会污染原有 shell 的名字空间。
8. 利用子 shell 实现局部变量
在8 中,GetLine 函数并不需要任何变化,变量定义和程序调用都沿用正常方式。唯一的区别在于调用该函数时,要将其作为一个子 shell 来调用(请注意第 37 行两边的圆括号)。另外一个问题是在子 shell 中修改的所有变量对于原有 shell 来说都是不可见的,这也就是为什么在第 38 行要通过 $? 来检查返回值,而 rtn 变量的值却是错误的。另外由于 num 在 GetLine 函数中也被当作是局部变量,同样无法将修改后的值传出来,因此也并没有打印所匹配到的 line 的数目是 3 行的信息。
解决上面这个问题就只能使用前面提到的利用标准输入输出设备的方法了,否则即使使用间接变量引用也无法正常工作。9 给出了一个使用间接变量引用的例子,尽管我们使用不同的名字来命名全局变量和局部变量,从而确保不会引起同名混淆,但是依然无法正常工作。原因同样在于 GetLine 函数是在另外一个子进程中运行的,它对变量所做的更新随着子 shell 的退出就消失了。
9. 利用间接变量索引也无法解决子 shell 通过变量回传值的问题
返回值问题
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 行中 ?的值总是0。实际上,第8行相当于执行了两条语句:第一条语句使用grep在文件中查找匹配项,第二条语句将grep命令的结果赋值给变量line,并设定其作用域只对于本函数及其子进程可见。因此第9行命令中? 的值实际上是执行 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
相关文章推荐
- [shell]递归求阶乘
- 获得任意一个整数的阶乘,递归
- shell 定时删除某个目录指定格式文件(递归)
- 用递归方法求n的阶乘(C语言)
- 递归求阶乘
- 递归计算阶乘
- 编程算法 - 阶乘和Fibonacci数列(递归)
- 阶乘的计算-递归思想
- 递归 ( 快速排序算法 ,台阶问题 ,阶乘问题 )
- 第四周项目五-用递归方法求解(求n的阶乘)
- 第四周项目:递归调用求n的阶乘
- 28、利用递归计算自然数 n 的阶乘
- Python递归求阶乘
- 利用递归进行阶乘是个非常方便的方法!
- 递归 10的阶乘为例子
- java 递归与非递归求n的阶乘
- Linux通过shell实现递归列目录及对文件的处理
- 递归--阶乘/斐波那契数列/判断回文字符串/字符串翻转
- Java练习——乘法口诀表、递归求阶乘
- 阶乘的实现:递归思想