【问题标题】:Time complexity of a function with recursive calls f(n / 2) and f(n - 2)?具有递归调用 f(n / 2) 和 f(n - 2) 的函数的时间复杂度?
【发布时间】:2013-09-17 17:06:58
【问题描述】:

我得到了这个函数,想知道时间复杂度:

int f2(int n) {
    if(n<2) return 1;
    return f2(n/2)+f2(n-2);
}

我使用伸缩膨胀法计算出它的运行时间为 O(n2)。这是正确的吗?

编辑:重新考虑后发现这个函数的结构和mergesort类似,复杂度Θ(n log n)。对吗?

【问题讨论】:

  • 你试过归纳证明吗?
  • 如果你使用动态规划,它可能是线性的。
  • @DennisMeng 不,我没有。
  • @Nick 假设我不是。
  • 我不明白这与合并排序有什么关系,也不明白为什么会是 Theta(n log n)。你能详细说明一下吗?

标签: c big-o time-complexity


【解决方案1】:

假设它是 n 中的多项式,即对于某些常量 ab,运行时间 T(n) 最多为 a*nb。然后,我们有:

T(n) = T(n/2) + T(n-2)
     = a*(n/2)^b + a*(n-2)^b
     = a/2^b * n^b + a*n^b + (lower-order terms)
     = a*n^b * (1/2^b + 1) + (lower-order terms)

其中(lower-order terms) 由严格小于bn 的和和差组成。对于足够大的n,a*nb * (1/2b + 1) 项将支配低阶项,并且总是更大比 a*nb,所以对于任何常量 b,T(n) 不等于 O(nb)。

现在假设它是 n 的指数,即对于某些常量 ab,T(n) 最多为 a*bn。那么:

T(n) = T(n/2) + T(n-2)
     = a*b^(n/2) + a*b^(n-2)
     = a*b^n * (b^(-n/2) + 1/b^2)

现在如果 b > 1 并且 n >= -2 log(1 - 1/b2) / log(b),那么:

n >= -2 log(1 - 1/b^2) / log(b)
-n/2 <= log(1 - 1/b^2) / log(b) = log_b(1 - 1/b^2)
b^(-n/2) <= 1 - 1/b^2
b^(-n/2) + 1/b^2 <= 1
T(n) = a*b^n * (b^(-n/2) + 1/b^2) <= a*b^n * 1 = a*b^n

因此,对于任何 a > 0 和 b > 1,存在一个 N(等于 -2 log(1 - 1/b2) / log(b)),使得对于所有n >= N, T(n)

那么这说明了什么? T(n) 大于多项式(对于 任何 多项式;还包括多项式函数,例如任何多项式函数都是 o(更大的非对数多项式)),但 T(n) 小于任何指数。我不确定是否有确切结果的简单公式,但这些是一些有用的界限。

编辑

正如 nhahtdh 指出的那样,这使得运行时间称为 quasi-polynomial time

【解决方案2】:

TL;DR:我认为递归是 nO(log n),这是超多项式但次指数。我没有匹配的下限,我怀疑这很严格,但这是进步!

我想我可以得到一个次指数但超多项式的上限。结合@Adam Rosenfield 的回答表明没有多项式界限,这表明

  • 递归没有多项式界限,但是
  • 任何指数界限都必须是弱的。

我们要解决的递归是

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

T(1) = T(2) = 1

我的一个想法是,我们也许可以按特定顺序评估这种重复。假设您要评估 T(n)。这样做一步会产生

T(n - 2) + T(n / 2) + 1

现在,假设我们扩展 T(n - 2) 项。这给了

T(n - 4) + T(n / 2 - 1) + T(n / 2) + 2

现在,展开 n - 4 项。这样做会给

T(n - 6) + T(n / 2 - 2) + T(n / 2 - 1) + T(n / 2) + 3

我们可以一遍又一遍地重复扩展减法项(形式为 T(n - 2k) 的项)的过程。那么,如果我们将它扩展到 n - 2k 大约为 n / 2 的程度会发生什么?这将发生大约 n / 4 次。如果我们这样做,我们就会得到那个

T(n) = T(n / 2) + T(n / 2) + T(n / 2 - 1) + T(n / 2 - 2) + ... + T(n / 2 - n / 4) + n / 4

我将假设 T(n) 不减。如果我们这样做,我们就会得到那个

T(n) ≤ T(n / 2) + T(n / 2) + T(n / 2) + ... + T(n / 2) + n / 4

T(n) ≤ (n / 4 + 1) T(n / 2) + n / 4

我将在这里稍微粗略地计算数学并进行另一个简化:不是让 T(n / 2) 的系数为 (n / 4 + 1),而是我将拥有它为 n / 4。我知道这不安全,但我猜想它不会对结果造成太大影响。不过,最好仔细检查其余部分是否仍然有效!

这意味着我们可以(粗略地)简化上面的递归来得到它

T(n) ≤ (n / 4) T(n / 2) + n / 4

这是非常更容易处理的事情,我们现在可以尝试直接解决它。我们将使用迭代方法,并一遍又一遍地将其插入自身,直到我们发现一个模式。如果我们这样做,我们会看到以下模式:

T(n) ≤ (n / 4) T(n / 2) + n / 4

≤ (n / 4)((n / 8) T(n / 4) + n / 8) + n / 4

= (n2 / 32) T(n / 4) + n / 4 + n2 / 32

≤ (n2 / 32) ((n / 16) T(n / 8) + n / 16) + n / 4 + n2 / 32

= (n3 / 512) T(n / 8) + n / 4 + n2 / 32 + n3 / 512

≤ (n3 / 512) ((n / 32) T(n / 16) + n / 32) + n / 4 + n2 / 32 + n3 / 512

= (n4 / 16384) T(n / 16) + n / 4 + n2 / 32 + n3 / 512 + n4 / 16384

让我们看看正在出现的模式。这样做 k 次后,T(n / 2k) 的系数为 nk 除以 2 的某个幂。到目前为止,2 的幂是 4、32、512、16384。这些数字似乎没有任何明显的规律,但如果我们将它们重写为 22, 25 , 29, 214 我们看到它们遵循模式 0, 2, 5, 9, 14, ... 。这比三角形数 1, 3, 6, 10, 15, ... 小一。这不是巧合:如果你仔细想想,每次我们展开递归式时,分母都会不断地乘以 2 的下一个幂,这会将 2 的幂的指数从一个三角形数增加到下一个三角形数。因此,我们期望 k 次迭代后的分母由下式给出

2(k + 1)(k + 2) / 2 - 1

在此之后,剩下的项是 n 的幂的总和除以 2 的幂的各种三角数。因此,如果我们将事物扩展 k 次,我们得到

T(n) ≤ (nk / 2(k + 1)(k + 2) / 2 - 1) T(n / 2 k) + Σi = 0k (ni / 2(i + 1)(i + 2 ) / 2 - 1)

好的!所以这是一团糟,但并不是那么糟糕。我们知道递归在 n / 2k = 1 时停止,这发生在 k = lg n 时。为了使事情更简单,我将更改基本情况,以便在 n / 2k = 2 时停止递归,这在 k = lg n - 1 时发生。这不会改变任何东西都不止一个常数因素。因此,如果我们将 k = lg n - 1 代入上述公式,我们可以化简为上界的封闭式表达式。这样做会得到以下结果:

T(n) ≤ (nk / 2(k + 1)(k + 2) / 2 - 1) T(n / 2 k) + Σi = 0k (ni / 2(i + 1)(i + 2 ) / 2 - 1)

= (nlg n - 1 / 2(lg n)(lg n + 1) / 2 - 1) T(2) + Σ i = 0lg n - 1 (ni / 2(i + 1)(i + 2) / 2 - 1)

哎呀,这不漂亮。然而,它并没有看起来那么糟糕。我们来看看第一项的分母:

2(lg n)(lg n + 1) / 2 - 1

使用基本的指数定律,我们得到了

2(lg n)(lg n + 1) / 2 - 1

= (2lg n)(lg n + 1) / 2 - 1

= n(lg n + 1) / 2 - 1

这还不错!因此,如果我们采用表达式

nlg n - 1 / 2(lg n)(lg n + 1) / 2 - 1

我们看到它简化如下:

nlg n - 1 / 2(lg n)(lg n + 1) / 2 - 1

= nlg n - 1 / n(lg n + 1) / 2 - 1

= n(lg n - 1 - ((lg n + 1) / 2 - 1))

= n(lg n - (lg n + 1) / 2)

= n(2 lg n - lg n - 1) / 2)

= n(lg n - 1) / 2)

= nO(lg n)

漂亮!该主导词最终的形式为 nO(lg n)!

那么剩下的呢?好吧,我们有这个可怕的总结要对付:

Σi = 0lg n - 1 (ni / 2(i + 1)(i + 2) / 2 - 1)

幸运的是,我们可以注意到一件事

ni / 2(i + 1)(i + 2) / 2 - 1

≤ni / 2(i + 1)(i + 2)

≤ni / 2(i + 1)

≤ni / 2i

= (n / 2)i

因此,如果我们想要做的只是求和的上限,我们可以使用这个更简单的求和来实现:

Σi = 0lg n - 1 (n / 2)i

由于 n / 2 > 0,这个和又是上限

Σi = 0lg n - 1 (n / 2)lg n - 1

= (lg n)(nlg n - 1) / (2lg n - 1)

= (2 lg n)(nlg n - 1) / n

= (2 lg n)nlg n - 2

= nO(lg n)

还有宾果游戏,这个可怕总和的第二项也是 nO(lg n)!这意味着我们有 T(n) = nO(log n)。这个界限不一定很严格,但它确实表明递归绝对是次指数的,因为 nO(log n) 的增长速度比任何指数函数都慢!

希望这会有所帮助!

【讨论】:

  • 谢谢你的回答,为了回答你之前的问题,伸缩膨胀的方法基本上就是你在这个答案中所做的,它是将公式插入自身,直到出现模式:)跨度>
【解决方案3】:

我想是O(2^sqrt(n))

你有T(n) = T(n/2) + T(n-2)

插入解决方案,除以2^sqrt(n),给定n → ∞

2^sqrt(n) = 2^sqrt(n/2)  +  2^sqrt(n-2)
---------   -----------     -----------
2^sqrt(n)    2^sqrt(n)       2^sqrt(n)

1 = 2^sqrt(-n/2) + 1

还有2^sqrt(-n/2) → 0

【讨论】:

  • 不,我认为这不准确。例如,对于 n=10,您将获得一个有 14 个叶子的调用树,而您的解决方案建议它不应通过大约 9
  • 我从未指定常量。我认为T(n)/2^sqrt(n) → C,但我不知道C,我必须检查它。
【解决方案4】:

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

我们知道 T(n-2) > T(n/2)

我们可以这样说:

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

所以 T(n-2) = 2T(n-4) + 1

T(n) = 4T(n-4) + 2

T(n) = 8T(n-6) + 3

T(n) = (2^k)T(n-2k) + k

因为我们知道 T(1) = 1

那么 n-2k = 1 -> k = (n-1)/2

T(n) = 2^((n-1)/2)T(1) + (n-1)/2

T(n) = 2^((n-1)/2) + (n-1)/2

T(n) = 2^((n-1)/2)

但一定有更好的解决方案。

【讨论】:

    【解决方案5】:

    总是有经验的方法 - 时间它......

    #include "timer.h"
    #include <stdio.h>
    
    static
    int f2(int n)
    {
        if (n < 2)
            return 1;
        return f2(n/2) + f2(n-2);
    }
    
    int main(void)
    {
    
        printf("Fast:\n");
        for (int i = 1; i <= 512; i *= 2)
        {
            Clock clk;
            clk_init(&clk);
            clk_start(&clk);
            long sum = 0;
            for (int j = 0; j < 100; j++)
                sum += f2(i);
            clk_stop(&clk);
            char buffer[32];
            printf("%8d: elapsed %10s s; sum = %12ld\n",
                   i, clk_elapsed_us(&clk, buffer, sizeof(buffer)), sum/100);
        }
    
        printf("\nSlow:\n");
        for (int i = 512; i < 1024; i += 16)
        {
            Clock clk;
            clk_init(&clk);
            clk_start(&clk);
            long sum = 0;
            for (int j = 0; j < 100; j++)
                sum += f2(i);
            clk_stop(&clk);
            char buffer[32];
            printf("%8d: elapsed %7s s; sum = %12ld\n",
                   i, clk_elapsed_ms(&clk, buffer, sizeof(buffer)), sum/100);
        }
    
        return 0;
    }
    

    结果:

    作为记录,测试是在运行 Mac OS X 10.8.5、2.3 GHz 的 Intel Core i7 和 16 GiB RAM 的 MacBook Pro 上运行的。

    Fast:
           1: elapsed   0.000000 s; sum =            1
           2: elapsed   0.000001 s; sum =            2
           4: elapsed   0.000001 s; sum =            4
           8: elapsed   0.000002 s; sum =           10
          16: elapsed   0.000008 s; sum =           36
          32: elapsed   0.000045 s; sum =          202
          64: elapsed   0.000408 s; sum =         1828
         128: elapsed   0.005985 s; sum =        27338
         256: elapsed   0.139971 s; sum =       692004
         512: elapsed   5.273834 s; sum =     30251722
    
    Slow:
         512: elapsed   5.286 s; sum =     30251722
         528: elapsed   6.370 s; sum =     36243644
         544: elapsed   7.609 s; sum =     43234278
         560: elapsed   8.949 s; sum =     51361196
         576: elapsed  10.397 s; sum =     60777212
         592: elapsed  12.171 s; sum =     71651844
         608: elapsed  14.394 s; sum =     84172786
         624: elapsed  16.716 s; sum =     98547380
         640: elapsed  19.176 s; sum =    115004102
         656: elapsed  21.985 s; sum =    133794468
         672: elapsed  25.295 s; sum =    155194954
         688: elapsed  29.170 s; sum =    179508916
         704: elapsed  33.456 s; sum =    207068524
         720: elapsed  38.317 s; sum =    238237116
         736: elapsed  43.776 s; sum =    273411566
         752: elapsed  49.878 s; sum =    313024652
         768: elapsed  56.979 s; sum =    357547444
         784: elapsed  64.456 s; sum =    407492292
         800: elapsed  72.980 s; sum =    463415834
         816: elapsed  82.535 s; sum =    525922004
         832: elapsed  93.062 s; sum =    595665060
         848: elapsed 104.886 s; sum =    673353212
         864: elapsed 118.038 s; sum =    759752270
         880: elapsed 132.569 s; sum =    855689292
         896: elapsed 150.487 s; sum =    962056258
         912: elapsed 166.065 s; sum =   1079814524
         928: elapsed 185.616 s; sum =   1209999302
         944: elapsed 206.875 s; sum =   1353724140
         960: elapsed 230.670 s; sum =   1512185428
         976: elapsed 256.718 s; sum =   1686667684
         992: elapsed 285.158 s; sum =   1878548866
        1008: elapsed 316.281 s; sum =   2089305684
    

    我没有对这些数据进行曲线拟合。显然,您可以减少或消除较大尺寸的迭代,依此类推(这样可以避免 1024 发生的溢出......)。

    如果有人在绘制图表方面有一定的专业知识,它可能会显示结果(当时的对数刻度)。

    【讨论】:

      【解决方案6】:

      我认为这是指数级的。为什么?每个 f(n) 导致调用 f(n/2) 和 f(n-2)。

      查看此问题了解详情:

      Computational complexity of Fibonacci Sequence

      这是斐波那契订单复杂度。你的功能结构是相似的。

      【讨论】:

      • 大小为 n-1 和 n-2 的两个子调用与大小为 n-2 和 n/2 的两个子调用之间存在巨大差异。重复大小 n/2 会导致多项式运行时间;有关一些示例,请参见主定理。我认为您需要为您的主张提供更多证据。
      猜你喜欢
      • 1970-01-01
      • 2015-08-13
      • 2021-05-05
      • 2014-03-12
      • 1970-01-01
      • 1970-01-01
      • 2015-05-08
      • 2015-10-08
      • 1970-01-01
      相关资源
      最近更新 更多