【问题标题】:Recursive function in bashbash中的递归函数
【发布时间】:2012-03-29 18:21:20
【问题描述】:

我想做一个函数,在 bash 中返回一个数字的阶乘

这是当前不起作用的代码,谁能告诉我出了什么问题以及如何纠正它?我刚开始学习 bash,我不太了解。

#!/bash/bin
factorial()
{
  let n=$1
  if (( "$n" <= "1" ))
  then return 1
  else
  factorial n-1
  return $n*$?
  fi
  return 0
}
factorial 5
echo "factorial 5 = $?"

【问题讨论】:

    标签: bash function recursion


    【解决方案1】:

    有几种语法和一个非常明显的逻辑之一(返回0)

    工作版本如下:

    #!/bin/bash
    
    factorial()
    {
        if (( $1 <= 1 )); then
            echo 1
        else
            last=$(factorial $(( $1 - 1 )))
            echo $(( $1 * last ))
        fi
    }
    factorial 5
    

    你不见了:

    1. return 不好(应该使用echo)

    2. shbang 行(是 /bin/bash 不是 /bash/bin)

    3. 不能在(( )) 或$(( ))(或let,但首选(( )))之外进行算术运算

    【讨论】:

    • 返回码是单字节,因此 n > 5 时会中断。要解决此问题,您需要更改函数以回显结果,而不是设置返回码。
    • +1 和 +9000 到@tripleee。 bash 中的函数应该回显结果,而不是返回结果。
    • $[...] 语法已被弃用数十年。停止使用它。
    • @osirisgothra:在数学中,非负整数 n 的阶乘,用 n! 表示,是所有 正数 整数的乘积小于或等于 n。不过,你的 cmets 是有效的——我很难摆脱过去 20 年来习惯的成语......
    • @tripleee 一个字节是 8 位或 2^8 个数字。因此 bash return 范围是 0-255 (2^8 - 1)。但我同意仍应避免 return 并用 echo 代替。
    【解决方案2】:
    #!/bin/bash
    
    function factorial() 
    { 
       if (( $1 < 2 ))
       then
         echo 1
       else
         echo $(( $1 * $(factorial $(( $1 - 1 ))) ))
       fi
    }
    

    这样会更好。

    (无论如何,它最多可以工作 25 个,这应该足以证明关于递归的观点。)

    对于更大的数字,bc 将是使用的工具,使上面的第九行:

    echo "$1 * $(factorial $(( $1 - 1 )))" | bc
    

    但你必须小心 bc --

    $ factorial 260
    38301958608361692351174979856044918752795567523090969601913008174806\
    51475135399533485285838275429773913773383359294010103333339344249624\
    06009974551133984962615380298039823284896547262282019684886083204957\
    95233137023276627601257325925519566220247124751398891221069403193240\
    41688318583612166708334763727216738353107304842707002261430265483385\
    20637683911007815690066342722080690052836580858013635214371395680329\
    58941156051513954932674117091883540235576934400000000000000000000000\
    00000000000000000000000000000000000000000
    

    对我糟糕的系统造成了相当大的压力!

    【讨论】:

    • 我发现您的方法将调用与 $1 一样多的子 shell,这不是必需的,而且效率非常低。如何避免创建子shell?
    • @TorosFanny 你是正确的,这可以被重写以避免创建子shell——这里有一个很好的答案:stackoverflow.com/questions/33568055/…感谢你的评论,它帮助我学到了新的东西!
    【解决方案3】:

    echo-ing 结果可能是获得 n > 5 的结果的唯一方法,但捕获回显结果需要子外壳,这意味着递归将很快变得昂贵。更便宜的解决方案是使用变量:

    factorial() {
        local -i val=${val:-($1)}
        if (( $1 <= 1 )); then
            echo $val
            return
        fi
        (( val *= $1 - 1 ))
        factorial $(( $1 - 1 ))
    }
    

    如果您想在启动时更加确定 val 未设置,请使用包装函数:

    factorial() {
        local -i val=$1
        _fact() {
            if (( $1 <= 1 )); then
                echo $val
                return
            fi
            (( val *= $1 - 1 ))
            _fact $(( $1 - 1 ))
        }
        _fact $1
    }
    

    比较:

    # My Method
    $ time for i in {0..100}; do factorial $(( RANDOM % 21 )); done > /dev/null 
    
    real    0m0.028s
    user    0m0.026s
    sys     0m0.001s
    
    # A capturing-expression solution
    $ time for i in {0..100}; do factorial $(( RANDOM % 21 )); done > /dev/null 
    
    real    0m0.652s
    user    0m0.221s
    sys     0m0.400s
    

    【讨论】:

    • 中肯的建议。尽管如此 - 如果您没有使用随机输入,您的测量结果会更有说服力。据我们所知,你的方法做了 100 次阶乘计数(1),而另一个方法做了 100 次阶乘计数(20):)
    • 我喜欢变量的使用,很好的例子说明了如何以及为什么。
    • 如果您真的怀疑 random 是否能提供令人信服的结果,您可以自己尝试实验。
    • +1 用于正确处理进程并避免分叉,但 变量 不是必需的!见my answer
    【解决方案4】:
    clear cat
    
    fact()
    
    {
    
            i=$1
            if [ $i -eq 0 -o $i -eq 1 ]
            then
                    echo 1
            else
                    f=`expr $i \- 1`
                    f=$(fact $f)
                    f=`expr $i \* $f`
                    echo $f
            fi
    }
    
    read -p "Enter the number : " n
    
    if [ $n -lt 0 ]
    
    then
    
            echo "ERROR"
    
    else
    
            echo "THE FACTORIAL OF $n : $(fact $n) "
    fi
    

    【讨论】:

      【解决方案5】:

      使用echo 代替return 的另一种实现

      #!/bin/bash
      
      factorial()
      {
              if [ $1 -le 1 ]
              then
                      echo 1
              else
                      echo $[ $1 * `factorial $[$1-1]` ]
              fi
      }
      echo "factorial $1 = " `factorial $1`
      

      【讨论】:

      • -1:这只是使用不推荐使用的语法重写 @user1070300 的答案。
      • 为什么 echo 1 under if 不只打印一个并且表现为返回类型?
      【解决方案6】:

      计算因子递归在下

      除了所有的答案,我想建议:

      存储计算的阶乘以避免重新计算

      如果您计划多次运行此函数,使用数组存储计算的阶乘,可以大大改善您的函数!!

      为此,我们需要反向递归的方式,以便存储每个计算的阶乘:

      declare -ia _factorials=(1 1)
      factorial() {
          local -i _req=$1 _crt=${2:-${#_factorials[@]}} _next=_crt+1 \
              _factor=${3:-${_factorials[@]: -1}}
          if [ "${_factorials[_req]}" ]; then
              printf %u\\n ${_factorials[_req]}
          else
              printf -v _factorials[_crt] %u $((_factor*_crt))
              factorial $1 $_next ${_factorials[_next]}
          fi
      }
      

      然后

      factorial 5
      120
      

      好的,然后:

      declare -p _factorials
      declare -ai _factorials=([0]="1" [1]="1" [2]="2" [3]="6" [4]="24" [5]="120")
      

      那如果我用set -x来追踪操作并要求更高的价值:

      set -x
      factorial 10
      + factorial 10
      + local -i _req=10 _crt=6 _next=_crt+1 _factor=120
      + '[' '' ']'
      + printf -v '_factorials[_crt]' %u 720
      + factorial 10 7
      + local -i _req=10 _crt=7 _next=_crt+1 _factor=720
      + '[' '' ']'
      + printf -v '_factorials[_crt]' %u 5040
      + factorial 10 8
      + local -i _req=10 _crt=8 _next=_crt+1 _factor=5040
      + '[' '' ']'
      + printf -v '_factorials[_crt]' %u 40320
      + factorial 10 9
      + local -i _req=10 _crt=9 _next=_crt+1 _factor=40320
      + '[' '' ']'
      + printf -v '_factorials[_crt]' %u 362880
      + factorial 10 10
      + local -i _req=10 _crt=10 _next=_crt+1 _factor=362880
      + '[' '' ']'
      + printf -v '_factorials[_crt]' %u 3628800
      + factorial 10 11
      + local -i _req=10 _crt=11 _next=_crt+1 _factor=3628800
      + '[' 3628800 ']'
      + printf '%u\n' 3628800
      3628800
      

      从5到10进行递归,直接使用阶乘5。

      declare -p _factorials
      declare -ai _factorials=([0]="1" [1]="1" [2]="2" [3]="6" [4]="24" [5]="120"
                        [6]="720" [7]="5040" [8]="40320" [9]="362880" [10]="3628800")
      

      避免分叉:通过设置变量替换echo:

      declare -ia _factorials=(1 1)
      factorialtovar() {
          local -i _req=$2 _crt=${3:-${#_factorials[@]}} _next=_crt+1 \
              _factor=${4:-${_factorials[@]: -1}}
          if [ "${_factorials[_req]}" ]; then
              printf -v $1 %u ${_factorials[_req]}
          else
              printf -v _factorials[_crt] %u $((_factor*_crt))
              factorialtovar $1 $2 $_next ${_factorials[_next]}
          fi
      }
      

      然后

      factorialtovar myvar 7 ; echo $myvar
      5040
      
      declare -p myvar _factorials 
      declare -- myvar="5040"
      declare -ai _factorials=([0]="1" [1]="1" [2]="2" [3]="6" [4]="24" [5]="120" [6]="720" [7]="5040")
      

      和

      set -x
      factorialtovar anothervar 10
      + factorialtovar anothervar 10
      + local -i _req=10 _crt=8 _next=_crt+1 _factor=5040
      + '[' '' ']'
      + printf -v '_factorials[_crt]' %u 40320
      + factorialtovar anothervar 10 9
      + local -i _req=10 _crt=9 _next=_crt+1 _factor=40320
      + '[' '' ']'
      + printf -v '_factorials[_crt]' %u 362880
      + factorialtovar anothervar 10 10
      + local -i _req=10 _crt=10 _next=_crt+1 _factor=362880
      + '[' '' ']'
      + printf -v '_factorials[_crt]' %u 3628800
      + factorialtovar anothervar 10 11
      + local -i _req=10 _crt=11 _next=_crt+1 _factor=3628800
      + '[' 3628800 ']'
      + printf -v anothervar %u 3628800
      set +x
      + set +x
      
      echo $anothervar
      3628800
      

      【讨论】:

      • 没有递归!
      • 很好的编辑,仍然是递归的并使用 array 来防止无用的处理! (但也许你能想象一种方法来存储从 1 到 requested factorial 的所有值。;-)
      【解决方案7】:

      避免分叉!

      这里唯一正确解决进程线程的答案是kojiro's answer,但我不喜欢为此使用专用变量。

      为此,我们可以使用参数来传递步骤或中间结果:

      factorial() { 
              if [ $1 -gt 1 ]; then
                  factorial $(( $1 - 1 ))   $(( ${2:-1} * $1 ))
              else
                  echo $2
              fi
      }
      

      没有分叉,结果作为第二个参数传递。

      避免分叉(二)

      那么为了避免分叉,我更喜欢将结果存储到变量中,而不是使用echo(或其他printf):

      setvarfactorial() { 
          if [ $2 -gt 1 ]; then
              setvarfactorial $1 $(( $2 - 1 )) $(( ${3:-1} * $2 ))
          else
              printf -v "$1" %u $3
          fi
      }
      

      用法:

      factorial 5
      120
      
      setvarfactorial result 5
      echo $result
      120
      

      当目标是将结果存储到变量中时,第二个方法比使用更系统友好和更快

      result=$(factorial 5)
      # or same
      result=`factorial 5`
      

      有或没有分叉的比较。

      没有设置变量,只是运行10x factorial 20:

      在我的旧树莓派上测试:

      time for i in {1..10};do factorial 20;done|uniq -c
           10 2432902008176640000
      real    0m0.376s
      user    0m0.159s
      sys     0m0.035s
      
      
      time for i in {1..10};do kojirofactorial 20;done|uniq -c
           10 2432902008176640000
      real    0m0.348s
      user    0m0.150s
      sys     0m0.023s
      
      
      time for i in {1..10};do danielfactorial 20;done|uniq -c
           10 2432902008176640000
      real    0m5.859s
      user    0m1.015s
      sys     0m1.732s
      

      如果小次郎的版本看起来比我的快一点,差异并不重要,但其他答案将花费 10 倍以上的时间!!

      查看分叉是否完成:

      set -x
      danielfactorial 5
      + danielfactorial 5
      + ((  5 <= 1  ))
      ++ factorial 4
      ++ ((  4 <= 1  ))
      +++ factorial 3
      +++ ((  3 <= 1  ))
      ++++ factorial 2
      ++++ ((  2 <= 1  ))
      +++++ factorial 1
      +++++ ((  1 <= 1  ))
      +++++ echo 1
      ++++ last=1
      ++++ echo 2
      +++ last=2
      +++ echo 6
      ++ last=6
      ++ echo 24
      + last=24
      + echo 120
      120
      

      对比:

      factorial 5
      + factorial 5
      + '[' 5 -gt 1 ']'
      + factorial 4 5
      + '[' 4 -gt 1 ']'
      + factorial 3 20
      + '[' 3 -gt 1 ']'
      + factorial 2 60
      + '[' 2 -gt 1 ']'
      + factorial 1 120
      + '[' 1 -gt 1 ']'
      + echo 120
      120
      

      【讨论】:

      【解决方案8】:
      #-----------------factorial ------------------------
      # using eval to avoid process creation
      fac=25
      factorial()
      {
          if [[ $1 -le 1 ]]
          then
              eval $2=1
          else
      
              factorial $[$1-1] $2
              typeset f2
              eval f2=\$$2
              ((f2=f2*$1))
              eval $2=$f2
      
          fi
      }
      time factorial $fac res 
      echo "factorial =$res"
      

      【讨论】:

      • 我想找到一种将 eval 集成到计算公式中的方法 (( eval .... ))
      【解决方案9】:

      要完成@Marc Dechico 解决方案,而不是eval,现在更可取的是传递引用(bash >= 4.3):

      factorial_bruno() {
          local -i val="$1"
          local -n var="$2"                             # $2 reference
          _fact() {
              if (( $1 <= 1 )); then
                  var="$val"
                  return
              fi
              ((val*=$1-1))
              _fact $(($1-1))
          }
          _fact "$1"
      }
      declare -i res
      factorial_bruno 20 res
      printf "res=%d\n" "$res"
      

      如果我们比较 @kojiro、@techno 的 1000 次运行的时间(为它们捕获结果,毕竟我们想要结果),@Marc Dechici' s,以及我的解决方案,我们得到:

      declare -i res
      TIMEFORMAT=$'\t%R elapsed, %U user, %S sys'
      
      echo "Kojiro (not catching result) :"
      time for i in {1..1000}; do factorial_kojiro $((i%21)); done >/dev/null
      echo "Kojiro (catching result) :"
      time for i in {1..1000}; do res=$(factorial_kojiro $((i%21))); done
      echo "Techno (not catching result) :"
      time for i in {1..1000}; do factorial_techno $((i%21)); done >/dev/null
      echo "Techno (catching result, 100% data already cached) :"
      time for i in {1..1000}; do res=$(factorial_techno $((i%21))); done
      _factorials=(1 1)
      echo "Techno (catching result, after cache reset) :"
      time for i in {1..1000}; do res=$(factorial_techno $((i%21))); done
      echo "Marc Dechico :"
      time for i in {1..1000}; do factorial_marc $((i%21)) res; done
      echo "This solution :"
      time for i in {1..1000}; do factorial_bruno $((i%21)) res; done
      
      Kojiro (not catching result) :
          0.182 elapsed, 0.182 user, 0.000 sys
      Kojiro (catching result) :
          1.510 elapsed, 0.973 user, 0.635 sys
      Techno (not catching result) :
          0.054 elapsed, 0.049 user, 0.004 sys
      Techno (catching result, 100% data already cached) :
          0.838 elapsed, 0.573 user, 0.330 sys
      Techno (catching result, after cache reset) :
          2.421 elapsed, 1.658 user, 0.870 sys
      Marc Dechico :
          0.349 elapsed, 0.348 user, 0.000 sys
      
      This solution :
          0.161 elapsed, 0.161 user, 0.000 sys
      

      有趣的是,在函数中输出 (echo/printf) 并使用 res=$(func...) (subshel​​l) 捕获结果总是非常昂贵,80% 的时间用于 Kojiro 的解决方案,>95% 用于 Techno一个……

      编辑:通过添加具有类似解决方案的缓存(@techno 新解决方案 - 不同之处在于使用 printf -v 而不是使用变量引用),如果我们需要计算多次阶乘,我们可以进一步提高响应时间。使用 bash 中的整数限制,这意味着我们需要计算多次相同的阶乘(可能对像这样的基准测试有用:-)

      【讨论】:

      • 您可以将my answer 添加到您的比较中! ;-)
      • U 可以使用printf '%u' 而不是%d 来表示无符号整数
      • @techno,我刚刚做到了 :-) 但你本可以完成这项工作并编辑我的帖子,哈哈...
      • @techno,我没有看到您的(更好的)第二个解决方案。抱歉,现在没时间添加它,请随时再次运行所有测试并编辑我的帖子。
      【解决方案10】:
      #!/bin/bash
      fact() {
         (( $1 == 0 )) && echo 1 || echo "$(( $1 * $(fact $(($1-1)) ) ))"
      }
      fact $1
      

      【讨论】:

      • 这个问题有 9 个现有答案,其中包括一个获得最高票数、超过 50 票 的被接受的答案。你确定你的解决方案还没有给出吗?如果不是,您为什么认为您的方法改进了已被社区验证的现有提案?在 Stack Overflow 上,提供解释总是很有用,但在问题已得到解决且让 OP 和社区都满意的情况下,它尤其很重要。通过解释您的答案有何不同以及何时可能更受欢迎,帮助读者了解答案。
      猜你喜欢
      • 2016-08-15
      • 1970-01-01
      • 1970-01-01
      • 2021-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多