【问题标题】:Determining complexity for recursive functions (Big O notation)确定递归函数的复杂度(大 O 表示法)
【发布时间】:2012-11-08 04:39:05
【问题描述】:

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

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);
}

【问题讨论】:

标签: recursion big-o complexity-theory


【解决方案1】:

每个函数的时间复杂度(大 O 表示法):


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

此函数在到达基本情况之前被递归调用 n 次,因此它的 O(n),通常称为 linear


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

这个函数每次调用n-5,所以我们在调用函数之前从n中减去5,但是n-5也是O(n)。 (实际上称为 n/5 次。并且,O(n/5) = O(n) )。


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

这个函数是以 5 为底的 log(n),每次除以 5 在调用函数之前,它的 O(log(n))(base 5),通常称为 logarithmic,最常见的 Big O 表示法和复杂性分析使用 base 2。


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);
    }
}

这里是O(2^n),或指数,因为每个函数都会调用自己两次,除非它已被递归n次。



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

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

这里for循环取n/2,因为我们增加了2,递归取n/5,因为for循环是递归调用的,因此,时间复杂度在

(n/5) * (n/2) = n^2/10,

由于渐近行为和最坏情况考虑或大 O 正在争取的上限,我们只对最大项感兴趣,所以O(n^2)


祝你期中考试好运;)

【讨论】:

  • 你的权利大约是第五个,对于 for 循环,n 会减少,但对于第四个,我不认为它的 n^2 因为它就像一棵树,每次你调用递归两次所以它应该是 2^n 加上这是您在之前评论中的答案。
  • @MJGwater 让循环的运行时间为m。当递归运行 1 次时,需要 m 来执行循环。当递归运行2次时,循环也运行2次,所以需要2m......等等。所以它是'*',而不是'^'。
  • @coder 对 5 的解释似乎很奇怪。如果递增 2 会导致 for 循环的 n/2 迭代,为什么递减 5 不会导致 n/5 递归调用?这仍然会导致O(n^2),但似乎是一个更直观的解释。为什么要在做同样的事情时混合减法和除法?
  • 我可能在某处做数学题,但我对 #5 的解决方案(虽然仍然是 n^2)是不同的。基本情况:T(0) = 1,导致 T(n) = n/2 + T(n-5) 扩展后导致 T(n) = n/2 + (n/2 + T(n- 10) 进一步扩展导致 T(n) = n/2 + (n/2 + (n/2 + T(n-15) 可以描述为 T(n) = k(n/2) + T( n-5k) 所以我们然后通过 5k = n 找到 T(0) 并适当地将 k 替换为 T(n) = (n/5)(n/2) + T(n - n) 减少到 T(n) = (n^2/10) + T(0) 减少为 T(n) = (n^2/10) + 1 即 T(n) = n^2
  • 每次调用你从计数器中删除 5,所以假设 n=100;当它第二次被调用时,它变成 95,然后是 90,直到达到 0,如果你计算它被调用的次数,你会注意到它是 20 次而不是 95 次,因此它是 n/5 而不是 n-5 次跨度>
【解决方案2】:

对于n &lt;= 0T(n) = O(1)的情况。因此,时间复杂度将取决于何时n &gt;= 0

我们将在下面的部分中考虑n &gt;= 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

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 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 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)

【讨论】:

  • 我最近在一个与分析递归斐波那契函数的时间和空间复杂度有关的面试问题(并通过扩展面试)失败了。这个答案是史诗般的,它有很大帮助,我喜欢它,我希望我能投票给你两次。我知道它很旧,但你有什么类似的计算空间 - 也许是一个链接,什么?
  • 对于4号,虽然结果一样,但归纳法不应该是下面这样吗? 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)
【解决方案3】:

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

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. 1234563 /p> 1234563但由于n(2^n)面前微不足道,所以可以忽略,复杂度只能说是(2^n)
  4. 对于第五个函数,有两个元素引入了复杂性。函数的递归性质引入的复杂性和每个函数中for 循环引入的复杂性。进行上述计算,函数的递归性质引入的复杂度将是~ n,而for循环带来的复杂度将是n。总复杂度为n*n

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

【讨论】:

  • 优秀的答案!我对第四个功能有疑问。如果它有三个递归调用,答案是 (3^n)。还是你还是直接说 (2^n)?
  • @Shubham:#4 对我来说似乎不合适。如果叶子的数量是2^n,那么树的高度必须是n,而不是log n。如果n 表示树中的节点总数,则高度仅为log n。但事实并非如此。
  • @BenForsrup:它将是 3^n,因为每个节点都会有三个子节点。确定这一点的最佳方法是使用虚拟值自己绘制递归树。
  • #2 应该是 n-5 而不是 n/5
  • 一个不起作用的例子:创建一个最小堆需要 O(n) 时间,但它有 O(n/2) 个叶子和 O(log(n)) 高度。跨度>
【解决方案4】:

我们可以用数学方法证明这是我在上述答案中所缺少的。

它可以显着地帮助您了解如何计算任何方法。 我建议从上到下阅读它以完全理解如何做到这一点:

  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,或者换句话说,我们需要找到我们丢失的k:@987654333 @。下一步是获取n-k 并声明n-k = 1,因为在递归结束时,当n&lt;=0 时,它将恰好花费O(1)。从这个简单的等式我们现在知道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) 的原因。让我们在 1 中找到 T(n/5)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 = 1 也就是 n = 5^k 就像问什么是 5 的幂,会给我们 n,答案是 log5n = k(以 5 为底的对数)。让我们将我们的发现放在T(n) = T(n/5^k) + k 中,如下所示:T(n) = 1 + lognO(logn)
  4. T(n) = 2T(n-1) + 1 我们这里的内容与以前基本相同,但这次我们递归地调用该方法 2 次,因此我们将其乘以 2。让我们找到 T(n-1) = 2T(n-1-1) + 1T(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。让我们通过声明n-k = 1(即k = n - 1)来找到k。让我们将k 放置如下:T(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 :)

【讨论】:

    【解决方案5】:

    这里的关键是可视化调用树。一旦完成,复杂性是:

    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 (this is special case of a single branch tree)
    

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

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

    所以,这里是上面示例的结果:


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

    首先考虑调用树:

    n     level 1
    n-1   level 2
    n-2   level 3
    n-3   level 4
    ... ~ n levels -> L = n
    

    这里每个节点的子节点数为 C = 1,层数 L = n+1。其余函数的复杂度为 O(1)。因此总复杂度为 L * O(1) = (n+1) * O(1) = O(n)


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

    这里的调用树是:

    n
    n-5
    n-10
    n-15
    ... ~ n/5 levels -> L = n/5
    

    C = 1,但 L = n/5。其余函数的复杂度为 O(1)。因此总复杂度为 L * O(1) = (n/5) * O(1) = O(n)


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

    调用树是:

    n
    n/5
    n/5^2
    n/5^3
    ... ~ log5(n) levels -> L = log5(n)
    

    因此 C = 1,L = log(n)。其余函数的复杂度为 O(1)。因此总复杂度为 L * O(1) = log5(n) * O(1) = O(log(n))


    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);
        }
    }
    

    这里的调用树比较复杂:

                   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
    

    这里每个节点的子节点数是 C = 2,而 L = n。其余函数的复杂度为 O(1)。 这次我们使用调用树中节点数的完整公式,因为 C > 1。因此总复杂度为 (C^L-1)/(C-1) * O(1) = (2^n - 1 ) * O(1) = O(2^n).


    int recursiveFun5(int n)
    {
        for (i = 0; i < n; i += 2) {
            // do something
        }
    
        if (n <= 0)
            return 1;
        else
            return 1 + recursiveFun5(n-5);
    }
    

    同样,调用树是:

    n
    n-5
    n-10
    n-15
    ... ~ n/5 levels -> L = n/5
    

    这里 C = 1,L = n/5。其余函数的复杂度为 O(n)。因此总复杂度为 L * O(1) = (n/5) * O(n) = O(n^2)

    【讨论】:

    • 我认为n-5 不会转换为n/5i += 2 会转换为n/2。如果n 很大,例如100,则n-595,90,85..i += 22,4,6,...n/5100,20,4n/250,25,12,5。差别这么大!?!
    • @KokHowTeh 如果您在谈论recursiveFun2,您可能会混淆这里涉及的实体:n-5参数n/2碰巧执行的调用次数。由于recursiveFun2调用recursiveFun2(n-5),所以不管n有多大,调用的次数都是n/5。试着这样想:如果每次调用你跳过 5 个单位,你总共会击中多少个单位?
    • 不,您说的是L = n/5,L 是您的解释中调用树的级别数,而不是n/5。怎么可能是n/5 而不是n - 5?在recursiveFun2 中没有n/2recursiveFun5 也一样。 n-m 不是 n/m
    • @KokHowTeh,我会再试一次。显然这里没有人想说n-m IS n/m。相反,我是说使用n-m 参数递归调用的函数会导致n/m 函数调用次数。因此,对于recursiveFun2,树的级别数正好是 L=n/5,正因为如此。对于L,此处的树分叉为每个节点只有一个孩子的树这一事实无关紧要。我不知道这是否是让您感到困惑的地方。无论如何,只要数一下:说 n=20,你将有 f(20),f(15),f(10),f(5) -> 总共 20/5 次调用。
    • 您在此分享的公式的真实来源在哪里?谢谢。
    【解决方案6】:

    我看到对于已接受的答案 (recursivefn5),有些人的解释存在问题。所以我会尽力澄清我的知识。

    1. for 循环运行 n/2 次,因为在每次迭代中,我们将 i(计数器)增加 2 倍。所以说 n = 10,for 循环将运行 10/2 = 5 次,即当i 分别为 0,2,4,6 和 8。

    2. 在同样的方面,每次调用递归调用都会减少 5 倍,即运行 n/5 次。再次假设 n = 10,递归调用运行 10/5 = 2 次,即当 n 为 10 和 5 时,它达到基本情况并终止。

    3. 计算总运行时间,每次调用递归函数时,for 循环都会运行 n/2 次。由于递归 fxn 运行 n/5 次(在上面的 2 中),for 循环运行 (n/2) * (n/5) = (n^2)/10 次,这转换为整体 Big O 运行时间O(n^2) - 忽略常数 (1/10)...

    【讨论】: