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

时间:2012-11-20 06:28:37

标签: recursion big-o complexity-theory

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

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

6 个答案:

答案 0 :(得分:269)

每个函数的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)base 5,每次除以5 在调用函数之前,它的O(log(n))(基数为5),通常称为对数,最常见的是Big O表示法和复杂性分析使用基数2。
  4. 在第四个中,它是O(2^n)指数,因为每个函数调用都会调用两次,除非它已经递归 n 次。
  5. 至于最后一个函数,for循环需要n / 2,因为我们正在增加 2,递归取n-5,因为for循环被调用 递归地因此,时间复杂度在(n-5)*(n / 2)=(2n-10)* n = 2n ^ 2~10n,由于渐近行为和最坏情况场景考虑或大O是上限为了争取,我们只关注最大的术语O(n^2)

    祝你的中期好运;)

答案 1 :(得分:114)

对于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 * a + T(0) * 2 ^ n 
     = a * 2^(n+1) - a + b * 2 ^ n
     = (2 * a + b) * 2 ^ n - a
     = O(2 ^ n)

其中a,b是常数。

5

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

我们可以通过归纳证明T(5k) >= T(5k - d)其中d = 0,1,2,3,4

n = 5m - b(m,b是整数; b = 0,1,2,3,4),然后是m = (n + b) / 5

T(n) = T(5m - b) <= T(5m)

(实际上,为了更加严谨,应该定义一个新功能T'(n)T'(5r - q) = T(5r) q = 0, 1, 2, 3, 4。我们知道T(n) <= T'(n)如上所述。当我们知道T'(n)位于O(f),这意味着存在常数a,b以便T'(n) <= a * f(n) + b,我们可以推导出T(n) <= a * f(n) + b,因此T(n)位于{ {1}}。这个步骤并不是必需的,但是当你不必处理其余部分时,更容易思考。)

扩展O(f)

T(5m)

因此,T(5m) = 5m / 2 + T(5m - 5) = (5m / 2 + 5 / 2) * m / 2 + T(0) = O(m ^ 2) = O(n ^ 2) T(n)

答案 2 :(得分:18)

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

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循环引入的函数和复杂性的递归性质引入。进行上述计算时,由于for循环~ n,函数的递归性质引入的复杂性将为n和复杂性。总复杂度为n*n

  6. 注意:这是计算复杂性的快速而肮脏的方法(没有官方!)。很想听听有关这方面的反馈。感谢。

答案 3 :(得分:11)

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

它可以戏剧性地帮助您了解如何计算任何方法。 我建议从头到尾阅读它,以完全了解该怎么做:

  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 + 1T(n) = T(n-2) + 2或换句话说,我们需要查找丢失的kT(n) = T(n-k) + k。下一步是采用n-k并声明n-k = 1,因为在递归结束时,当n<=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)的原因。让我们像在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 + 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。我们通过声明kn-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的{​​{1}}。让我们放置它:T(n-5) = T(n - 2*5) + n + 1,它是T(n) = T(n-2*5) + n + n + 1 + 1);对于k:T(n) = T(n-2*5) + 2n + 2),再次:T(n) = T(n-k*5) + kn + k),它是n-5k = 1,大约是n = 5k + 1。这将为我们提供:n = k,大致为T(n) = T(0) + n^2 + n

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

答案 4 :(得分:4)

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

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
    呼叫树在这里是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)
    呼叫树在这里是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
    呼叫树在这里是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

答案 5 :(得分:0)

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