【问题标题】:Is nested recursion possible or should we avoid recursion ?嵌套递归是可能的还是我们应该避免递归?
【发布时间】:2017-11-04 18:20:45
【问题描述】:

我遇到了这样的问题

  • F(1) = 1
  • F(2n) = F(n)
  • F(2n+1) = F(n) + F(n+1)

开发一个递归程序来计算F

一些用户提到使用两个递归函数调用:

def calc(n):
  if n=1 :
    return 1
  else if(n%2)==0:
    return calc(n/2)
  else :
    return calc(n/2)+calc(n/2+1)  **NESTED RECURSION**

这是一个正确的逻辑吗?算法不会呈指数级增长吗? 我想到了一个简单的代码:

def calc(count):
  result[1]=1
  n=2
  for range(1,count):
      if n%2=0:
          result.append(result[n])
      else :
          result.append(result[n/2]+result[n/2+1])
  return result

【问题讨论】:

  • def calc(n): if … else: return calc(n/2)+calc(n/2+1) 没有嵌套,只是重复。试试Ackermann

标签: python algorithm recursion big-o


【解决方案1】:

这两种方法都是正确的。从一个函数中进行多次递归调用确实是合法的,其含义就是您所想的 - 只需进行一次调用,然后进行下一次调用,然后再进行下一次调用,等等。

有趣的是,我认为递归版本的调用次数不会成倍增加。它最多进行两次递归调用,但每个都在一个问题上,其大小(大约)是原始调用的一半。本质上,重现看起来像这样:

T(1) = 1
T(2) = 1
T(n) <= T(n / 2) + T(n / 2 + 1) + 1

我用“小于或等于这里”来表示,在最好的情况下你可能只打一个电话,但在最坏的情况下你最多打两个电话。

我想证明这个函数 T(n)

T(1) = 1
T(2) = 1

所以让我们设置 a = 1。对于归纳步​​骤,我们考虑三种情况。首先,让我们考虑何时 floor(n / 2)

T(n) <= T(n / 2) + T(n / 2 + 1) + 1
     <= 1 + 1 + 1
     <= 3

如果我们假设 cn + d >= 3,当 n = 3 或 n = 4 时,那么这个结果是正确的。特别是,这意味着 3c + d >= 3 和 4c + d >= 3。

在下一种情况下,让我们看看如果 floor(n / 2) = 2 会发生什么。然后我们就有了

T(n) <= T(n / 2) + T(n / 2 + 1) + 1
     <= 1 + max{c(n / 2 + 1) + d, 1} + 1
     <= 2 + max{c(n / 2 + 1) + d, 1}
     <= 3 + c(n / 2 + 1) + d

所以如果我们有 3 + c(n / 2 + 1) + d

3 + c(n / 2 + 1) + d <= cn + d
3 + c(n / 2 + 1)     <= cn
3 + c(5 / 2 + 1)     <= 5c
3 + 5c/2 + c         <= 5c
3 + 7c/2             <= 5c
4                    <= 3c / 2
8 / 3                <= c

所以我们必须有 c >= 8 / 3。

最后,n / 2 和 n / 2 + 1 都不小于三的情况:

T(n) <= T(n / 2) + T(n / 2 + 1) + 1
     <= c(n / 2) + d + c(n / 2 + 1) + d + 1
     <= cn / 2 + cn / 2 + c + 2d + 1
      = cn + c + 2d + 1

如果小于 cn + d

 cn + c + 2d + 1 <= cn + d
      c + 2d + 1 <=      d
      c +  d + 1 <= 0

这在 d = -c - 1 时有效。

从前面开始,我们知道 3c + d >= 3,如果 2c - 1 >= 3 或 2c >= 4,则有效,因此 c >= 2。我们也有 4c + d >= 3,它如果 c >= 2 也有效。让 c = 8 / 3,我们得到 d = -11 / 3,所以

T(n) <= max{8n/3 - 11/3, 1}

所以 T(n) = O(n) 并且递归只会进行线性多次调用。


简而言之,递归和迭代版本都需要线性时间。不要害怕递归的指数时间爆炸,而不能确定它是指数的。 :-) 诚然,在这种情况下,我真的更喜欢迭代版本。它更清晰、更直观、更直接 O(n)。

【讨论】:

  • 什么解释,我一定会读的 :) 我是菜鸟。
  • @Nishant- 希望我没有搞砸数学。 :-)
  • @Paul Hankin- F(n) 的值可能不大于 n,但计算它的时间可能是如果我们对每个递归调用收费一个恒定的时间单位。这个递归公式 (T(1) = 1, T(2n) = T(n) + 1, T(2n + 1) = T(n) + T(n + 1) + 1) 不容易有界由 n.
  • 这里,你忘了分配:3 + c(5 / 2 + 1)
  • 为了证明递归版本是O(n),可能更容易限制递归的深度。这个深度是&lt;= ceiling(log n) + 1。所以这棵树最多有 O(n) 个节点。
【解决方案2】:

递归还不错。将递归视为一种工具。有些问题可以通过递归很好地解决(例如斐波那契数),而其他问题则没有递归弯曲(工作列表类型算法)。

为什么人们在递归上出错是因为他们认为递归解决方案会给他们带来良好的声誉。我想说,用最简单的方法解决问题。

这种方法有时是递归,有时不是。

此外,递归方法可能更难理解。因此,您需要考虑可读性。

【讨论】:

    【解决方案3】:

    估计第二个算法的性能更容易:它在空间和时间上显然是 O(n),并且常数很小。但这并不意味着第二种算法比第一种更快。其实不然!

    在 Python 2.6.5 中,第二个算法在计算 calc(1000) 时比第一个算法慢 25 倍,在计算 calc(10000) 时慢 80 倍,在计算 calc(65535) 时慢 1712 倍。 p>

    第一种算法的最坏情况似乎是对于像 54613 = 11010101010101012 这样的数字。对于该值,第一种算法仅比第二种算法快 16 倍。

    在简单的递归算法中,calc(1000) 只涉及对calc 的 50 次调用,甚至 calc(54613) 也只需要 5166 次。

    这个 O(log n) 算法更快:

    def C(x):
        a = 1
        b = 0
        # Loop invariant: we are going to return a*C(x) + b*C(x + 1).
        while x >= 2:
            if x % 2 == 0:
                a += b
            else:
                b += a
            x //= 2
        return a + b
    

    这个算法的性能很容易评估,但它的正确性却不是。 ;-)

    【讨论】:

      【解决方案4】:

      实际上,您可以编写一个亚线性 (O(log n)) 的递归版本。

      关键是要注意,在给定 [n/2] 和 [n/2]+1 的两个值的情况下,您可以计算 n 和 n+1 的 两个 值。

      所以如果你把这两个值看作一个元组T(n) = (F(n), F(n+1)),那么给定T([n/2]) ,您可以计算 T(n)。

      所以你的函数会是这样的

      Tuple F(int n) {
          // Base cases:
      
          Tuple result;
          if (n is even) {
              Tuple t = F(n/2);
              result.First = t.First;
              result.Second =t.First + t.Second;
      
              return result;
          }
      
          if (n is odd) {
      
              Tuple t = F((n-1)/2);
              result.First = t.First + t.Second;
              result.Second = t.Second;
              return result;
          } 
      }
      

      这样你最终只进行一个递归调用,并且由于你将每个调用的输入大小减半,它是递归 O(logn)。

      练习:使用这个技巧给出一个 O(log n) 时间的斐波那契数算法。

      提示:使用身份:

      【讨论】:

      • 第二句(“钥匙...”)不清楚/不完整。您的解决方案还要求您可以从 n 和 n + 1 计算 2n + 2。
      猜你喜欢
      • 2020-10-05
      • 2010-10-15
      • 1970-01-01
      • 2020-05-03
      • 2011-10-19
      • 1970-01-01
      • 2010-10-03
      • 1970-01-01
      • 2011-05-04
      相关资源
      最近更新 更多