确定递归函数的复杂度(Big O表示法)


267

明天我有计算机科学中期课程,我需要帮助确定这些递归函数的复杂性。我知道如何解决简单的案例,但我仍在尝试学习如何解决这些较困难的案例。这些只是我无法弄清楚的一些示例问题。任何帮助将不胜感激,将极大地帮助我的学习,谢谢!

int recursiveFun1(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun1(n-1);
}

int recursiveFun2(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun2(n-5);
}

int recursiveFun3(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun3(n/5);
}

void recursiveFun4(int n, int m, int o)
{
    if (n <= 0)
    {
        printf("%d, %d\n",m, o);
    }
    else
    {
        recursiveFun4(n-1, m+1, o);
        recursiveFun4(n-1, m, o+1);
    }
}

int recursiveFun5(int n)
{
    for (i = 0; i < n; i += 2) {
        // do something
    }

    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun5(n-5);
}

4
如果您不想每次都进行分析,可以使用一种称为“ Master”方法的黑盒技术。但是,假设所有输入的递归拆分在每种情况下的大小均相等。
Vivek克里希纳


Answers:


345

每个函数用Big O表示法表示的时间复杂度按数字顺序排列:

  1. 第一个函数在达到基本情况之前被递归调用n次,因此O(n)通常称为线性函数
  2. 每次都将第二个函数称为n-5,因此我们在调用函数之前先从n中减去5,但是n-5也是O(n)。(实际上称为n / 5次的阶。并且,O(n / 5)= O(n))。
  3. 此函数是以log(n)为底数5,每次调用该函数前我们除以5,因此其O(log(n))(以5为底数)(通常称为对数),通常是Big O表示法和复杂度分析都以2为底数。
  4. 在第四个中,它是O(2^n)指数,因为每个函数调用自身都会调用两次,除非它已被递归n次。
  5. 至于最后一个函数,因为我们增加了2,所以for循环需要n / 2,而递归需要n-5,并且因为for循环是递归调用的,因此时间复杂度为(n-5)*(n / 2)=(2n-10)* n = 2n ^ 2-10n,由于渐近行为和最坏情况的考虑或大O争取的上限,我们只对最大项感兴趣O(n^2)

    祝你中期顺利;)


您的右数大约是5,则for循环的n会减少,但是对于第四点,我不认为每次调用两次递归时它的n ^ 2就象一棵树,因此它应该是2 ^ n加早些时候在评论中回答。
编码器2012年

2
@MJGwater循环的运行时间为m。递归运行1次后,需要m才能执行循环。当递归运行2次时,循环也运行2次,因此需要2m ...依此类推。因此它是“ *”,而不是“ ^”。
bjc

3
@coder 5的解释似乎很奇怪。如果增加2导致n/2循环迭代for,为什么减少5不会导致n/5递归调用?这仍然会导致,O(n^2)但看起来更直观。当他们必须要做相同的事情时,为什么还要混合使用减法和除法?
杰克

1
@coder,因此对于#4,如果函数定义中有3个递归调用,那么它的时间复杂度为O(3 ^ n)吗?对于5个递归调用,它将是O(5 ^ n),对吗?
rmutalik

1
@杰克是的,我也想知道同样的事情。应该n/5不会n-5。最终,整体将归结为O(N^2)
Anuj

128

对于的情况n <= 0T(n) = O(1)。因此,时间复杂度取决于何时n >= 0

我们将n >= 0在下面的部分中考虑这种情况。

1。

T(n) = a + T(n - 1)

其中a是一些常数。

通过归纳:

T(n) = n * a + T(0) = n * a + b = O(n)

其中a,b是一些常数。

2。

T(n) = a + T(n - 5)

其中a是一些常数

通过归纳:

T(n) = ceil(n / 5) * a + T(k) = ceil(n / 5) * a + b = O(n)

其中a,b为常数且k <= 0

3。

T(n) = a + T(n / 5)

其中a是一些常数

通过归纳:

T(n) = a * log5(n) + T(0) = a * log5(n) + b = O(log n)

其中a,b是常数

4。

T(n) = a + 2 * T(n - 1)

其中a是一些常数

通过归纳:

T(n) = a + 2a + 4a + ... + 2^(n-1) * a + T(0) * 2^n 
     = a * 2^n - a + b * 2^n
     = (a + b) * 2^n - a
     = O(2 ^ n)

其中a,b是一些常数。

5,

T(n) = n / 2 + T(n - 5)

其中n是一些常数

重写n = 5q + r其中q和r为整数且r = 0、1、2、3、4

T(5q + r) = (5q + r) / 2 + T(5 * (q - 1) + r)

我们有q = (n - r) / 5,由于r <5,我们可以认为它是一个常数,所以q = O(n)

通过归纳:

T(n) = T(5q + r)
     = (5q + r) / 2 + (5 * (q - 1) + r) / 2 + ... + r / 2 +  T(r)
     = 5 / 2 * (q + (q - 1) + ... + 1) +  1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * (q + 1) * q + 1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * q^2 + 5 / 4 * q + 1 / 2 * q * r + 1 / 2 * r + T(r)

由于r <4,我们可以找到一些常数b,这样 b >= T(r)

T(n) = T(5q + r)
     = 5 / 2 * q^2 + (5 / 4 + 1 / 2 * r) * q + 1 / 2 * r + b
     = 5 / 2 * O(n ^ 2) + (5 / 4 + 1 / 2 * r) * O(n) + 1 / 2 * r + b
     = O(n ^ 2)

1
我最近未能通过与递归斐波那契函数的时间和空间复杂度有关的面试问题(并通过扩展面试)。这个答案是史诗般的,对我很有帮助,我希望我能对你投两次票。我知道它已经很旧了,但是您有什么类似的计算空间的方法吗?也许是链接,什么东西?
Dimitar Dimitrov

对于第四号,即使结果是相同的,归纳法也不应该如下吗?T(n)= a + 2T(n-1)= a + 2a + 4T(n-1)= 3a + 4a + 8T(n-1)= a *(2 ^ n-1)+ 2 ^ n * T(0)= a *(2 ^ n-1)+ b * 2 ^ n =(a + b)* 2 ^ n-a = O(2 ^ n)
雪鱼

27

我发现近似于递归算法复杂性的最佳方法之一是绘制递归树。一旦有了递归树:

Complexity = length of tree from root node to leaf node * number of leaf nodes
  1. 第一个函数n的叶节点长度为,叶节点的数量为,1因此复杂度为n*1 = n
  2. 第二个功能将n/5再次具有叶节点的长度和数量,1因此复杂度将为n/5 * 1 = n/5。它应该近似为n

  3. 对于第三个函数,由于n在每个递归调用中都将其除以5,因此递归树的长度将为log(n)(base 5),叶节点的数量将再次为1,因此复杂度将为log(n)(base 5) * 1 = log(n)(base 5)

  4. 对于第四个功能,由于每个节点将有两个子节点,叶节点的数量将等于,(2^n)而递归树的长度将变为,n因此复杂度将为(2^n) * n。但是由于n前面无关紧要(2^n),因此可以忽略不计,只能说是复杂(2^n)

  5. 对于第五个功能,有两个元素介绍了复杂性。函数的递归性质引入了复杂性,for而每个函数中的循环引入了复杂性。进行上述计算后,~ n由于函数的递归性质而导致的复杂度将是,而由于for循环而导致的复杂度n。总复杂度为n*n

注意:这是一种计算复杂度的快速而肮脏的方法(尚无官方!)。很想听听对此的反馈。谢谢。


很好的答案!我对第四个职能有疑问。如果它将进行三个递归调用,答案将是(3 ^ n)。还是您仍会说(2 ^ n)?
本·福斯鲁普

@Shubham:#4对我来说似乎不合适。如果叶子的数量是2^n树的高度,则n不是log n。仅log nn表示树中节点的总数时,高度才为。但事实并非如此。
朱利安·A

@BenForsrup:它将是3 ^ n,因为每个节点将具有三个子节点。最好的确定方法是自己绘制带有伪值的递归树。
Shubham

#2应该是n-5而不是n / 5
Fintasys

7

我们可以从数学上证明它,而上述答案正是我所缺少的。

它可以极大地帮助您了解如何计算任何方法。我建议从头到尾阅读它,以全面了解如何做:

  1. T(n) = T(n-1) + 1这意味着该方法完成所需的时间等于同一方法,但n-1等于,T(n-1)现在我们添加,+ 1因为这是完成常规操作所花费的时间(除外T(n-1))。现在,我们将找到T(n-1)以下内容:T(n-1) = T(n-1-1) + 1。看来我们现在可以形成一个可以给我们某种重复的函数,以便我们可以完全理解。我们将在的右侧放置,T(n-1) = ...而不是T(n-1)T(n) = ...将提供给我们的方法内:T(n) = T(n-1-1) + 1 + 1这是T(n) = T(n-2) + 2或换句话说,我们需要找到我们所缺少的kT(n) = T(n-k) + k。下一步是n-k声明,n-k = 1因为在递归结束时,当递归结束时它将精确地为O(1)n<=0。从这个简单的方程式我们现在知道k = n - 1。让我们放置k最后一个方法:T(n) = T(n-k) + k这将给我们:T(n) = 1 + n - 1恰好是nO(n)
  2. 与1.相同。您可以对其进行自我测试,然后看得到O(n)
  3. T(n) = T(n/5) + 1和以前一样,此方法完成的时间等于相同方法的时间,但这n/5就是它受其约束的原因T(n/5)。让我们T(n/5)在1中找到T(n/5) = T(n/5/5) + 1,即T(n/5) = T(n/5^2) + 1。让我们放入T(n/5)内部T(n)进行最终计算:T(n) = T(n/5^k) + k。还是和以前一样,n/5^k = 1n = 5^k恰好是问5的幂次将给我们n,答案是log5n = k(以5为底的对数)。让我们把我们的调查结果T(n) = T(n/5^k) + k如下:T(n) = 1 + logn这是O(logn)
  4. T(n) = 2T(n-1) + 1我们这里有一个基本上和以前一样,但这次我们是递归调用方法的2倍。因此,我们多次通过它2.让我们来看看T(n-1) = 2T(n-1-1) + 1这是T(n-1) = 2T(n-2) + 1。我们的下一个地方像以前一样,让我们把我们的发现:T(n) = 2(2T(n-2)) + 1 + 1这是T(n) = 2^2T(n-2) + 2给我们T(n) = 2^kT(n-k) + k。让我们k声明那n-k = 1k = n - 1。让我们如下放置kT(n) = 2^(n-1) + n - 1大致O(2^n)
  5. T(n) = T(n-5) + n + 1它几乎与4相同,但是现在我们添加了,n因为我们有一个for循环。让我们找到T(n-5) = T(n-5-5) + n + 1哪个是T(n-5) = T(n - 2*5) + n + 1。让我们的地方吧:T(n) = T(n-2*5) + n + n + 1 + 1)这是T(n) = T(n-2*5) + 2n + 2)和第k T(n) = T(n-k*5) + kn + k)再次:n-5k = 1n = 5k + 1是大致n = k。这将给我们:T(n) = T(0) + n^2 + n大致而言O(n^2)

我现在建议阅读其余的答案,这将使您有一个更好的视角。赢得那些大O的好运:)


1

此处的关键是可视化调用树。完成后,复杂度为:

nodes of the call tree * complexity of other code in the function

可以使用与普通迭代函数相同的方式来计算后一项。

相反,完整树的总节点计算如下

                  C^L - 1
                  -------  , when C>1
               /   C - 1
              /
 # of nodes =
              \    
               \ 
                  L        , when C=1

其中C是每个节点的子代数,L是树的级别数(包括根)。

很容易将树形象化。从第一个调用(根节点)开始,然后绘制与该函数中的递归调用数相同的子级数。将传递给子调用的参数写为“节点的值”也很有用。

因此,在上面的示例中:

  1. 此处的调用树为C = 1,L = n + 1。其余函数的复杂度为O(1)。因此总复杂度为L * O(1)=(n + 1)* O(1)= O(n)
n     level 1
n-1   level 2
n-2   level 3
n-3   level 4
... ~ n levels -> L = n
  1. 呼叫树在这里是C = 1,L = n / 5。其余函数的复杂度为O(1)。因此总复杂度为L * O(1)=(n / 5)* O(1)= O(n)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
  1. 调用树在这里是C = 1,L = log(n)。其余函数的复杂度为O(1)。因此总复杂度为L * O(1)= log5(n)* O(1)= O(log(n))
n
n/5
n/5^2
n/5^3
... ~ log5(n) levels -> L = log5(n)
  1. 调用树在这里是C = 2,L = n。其余函数的复杂度为O(1)。这次我们使用完整的公式作为呼叫树中节点的数目,因为C>1。因此总复杂度为(C ^ L-1)/(C-1)* O(1)=(2 ^ n-1 )* O(1)= O(2 ^ n)
               n                   level 1
      n-1             n-1          level 2
  n-2     n-2     n-2     n-2      ...
n-3 n-3 n-3 n-3 n-3 n-3 n-3 n-3    ...     
              ...                ~ n levels -> L = n
  1. 呼叫树在这里是C = 1,L = n / 5。其余函数的复杂度为O(n)。因此总复杂度为L * O(1)=(n / 5)* O(n)= O(n ^ 2)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.