363

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

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

6 回答 6

499

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


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

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


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))(以 5 为底),通常称为对数,最常见的大 O 表示法和复杂性分析使用以 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 正在努力争取的上限,我们只对最大项 so 感兴趣O(n^2)


祝你期中考试好运;)

于 2012-11-20T06:37:40.307 回答
144

对于 的情况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)
于 2012-11-20T07:55:00.517 回答
42

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

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.

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

于 2017-05-16T01:29:31.107 回答
20

我们可以在数学上证明这一点,这是我在上述答案中所缺少的。

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

  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。看起来我们现在可以形成一个可以给我们某种重复的函数,这样我们就可以完全理解了。我们将把右边的inT(n-1) = ...而不是T(n-1)放在T(n) = ...将给我们的方法中:T(n) = T(n-1-1) + 1 + 1或者T(n) = T(n-2) + 2换句话说,我们需要找到我们缺少的k: T(n) = T(n-k) + k。下一步是采取n-k并声称,n-k = 1因为在递归结束时,当n<=0. 从这个简单的方程我们现在知道了k = n - 1。让我们放置k在我们的最终方法中:T(n) = T(n-k) + kwhich 将给我们:T(n) = 1 + n - 1which is exact nor O(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) + 1T(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 + lognO(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 + 1T(n) = 2^2T(n-2) + 2给了我们T(n) = 2^kT(n-k) + k. 让我们k通过声称是n-k = 1来查找k = n - 1。让我们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 :)

于 2020-02-22T16:41:18.880 回答
8

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

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)

于 2020-04-18T22:18:38.350 回答
2

我看到对于接受的答案(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)...

于 2020-12-30T04:35:08.517 回答