这个用于计算指数的递归代码的运行时间是多少?

Posted

技术标签:

【中文标题】这个用于计算指数的递归代码的运行时间是多少?【英文标题】:What is the runtime of this recursive code for computing exponents? 【发布时间】:2017-03-06 23:43:52 【问题描述】:

以下函数的运行时间复杂度是O(1)吗?

int pow(int a, int n) 
    if (n == 0) 
        return 1;
    
    if (n % 2 == 1) 
        return pow(a, n / 2) * pow(a, n / 2) * a;
     else 
        return pow(a, n / 2) * pow(a, n / 2);
    

我有这样的印象,因为代码中只有 if 语句,没有循环。我以前从未使用过 Big-O 和递归,我在网上找不到任何好的资源。

【问题讨论】:

嗯,如果 n 为 0,它需要恒定的时间,否则无论 pow(a,n/2) 需要多少操作,它至少需要两倍。 啊,好吧。所以最好的情况是 O(n) 和 O(1)? 【参考方案1】:

您的函数的运行时间为 O(n),但可以轻松修改为在 O(log n) 时间内运行。

我们可以通过多种方式看到这一点。首先,我们可以计算递归调用的总数,因为每个递归调用的工作时间为 O(1)。想象一下,例如,我们为某个数字 a 调用 pow(a, 8)。那么

pow(a, 8) 调用 pow(a, 4) 两次。 pow(a, 4) 调用 pow(a, 2) 两次。 pow(a, 2) 调用 pow(a, 1) 两次。 pow(a, 1) 调用 pow(a, 0) 两次。

这意味着有

1 次调用 pow(a, 8), 2 次调用 pow(a, 4), 4 次调用 pow(a, 2), 8 次调用 pow(a, 1) 和 16 次调用 pow(a, 0)。

总的来说,这是 1 + 2 + 4 + 8 + 16 = 31 次调用。

现在,假设我们调用 pow(a, 16)。这将触发对 pow(a, 8) 的两次调用(总共进行了 62 次递归调用),加上对 pow(a, 16) 的初始调用一次,总共进行了 63 次递归调用。如果我们调用 pow(a, 32),我们将对 pow(a, 16) 进行两次调用(总共 126 次递归调用),再加上一次对 pow(a, 32) 的初始调用,总共 127 次递归调用.更一般地说,如果我们调用 pow(a, n),我们会得到 4n - 1 次调用,这将是 O(n)。

我们实际上可以正式证明这一点。令 C(n) 为对大小为 n 的输入进行的调用次数。请注意

C(0) = 1。 C(n) = 2C(n / 2) + 1

通过主定理,此递归求解为 O(n)。

请注意,每个单独的递归调用所做的工作很少。杀死我们的是这样一个事实,即进行了如此多的总递归调用,以至于工作在这些调用中加起来。但是,虽然有很多 total 递归调用正在进行,但很少有 unique 递归调用正在进行。所以考虑一下代码的这种变化:

int pow(int a, int n) 
    if (n == 0) return 1;

    int halfPow = pow(a, n / 2);
    if (n % 2 == 0) return halfPow * halfPow;
    else return halfPow * halfPow * a;

此代码缓存正在执行的递归调用的值,因此它每次都会触发一个调用。结果,每次调用完成的工作仍然是 O(1),但递归中不再有分支。然后,由于每个递归调用的大小是原始调用的一半,并且由于每个级别只有一个调用,因此运行时间为 O(log n),您可以使用 Master 确认定理。

一般来说,请注意“我们不断将事物切成两半,因此整体工作最终为 O(log n)”形式的论点。这可能是真的,但是您在每个步骤中所做的工作量对于确定运行时间也非常非常重要,正如您在此处看到的那样。

【讨论】:

【参考方案2】:

让我们分解这里发生的事情。实际上是 O(n)。对于 pow() 的每次调用,都可能发生两种选择:

    返回 0 -- 这是一个单一的操作。 使用 n/2 对 pow() 执行两次调用。

因此,当您在每次调用 pow() 时将 n 减少 n/2 时,您的问题空间在每一步都会成倍增加。递归的第一步是两次调用 pow,第二次是 2^2,第三次是 2^3,依此类推。对于 n = 16 将有 pow(..., 16), pow(..., 8), pow( ..., 4), pow(..., 2), pow(..., 1), pow(..., 0) 对于 n = 16 调用 pow 有 2^0 次,对于 n 调用 2^1 = 8, 2^2 代表 n = 4, 2^3 代表 n = 2, 2^4 代表 n = 1, 2^5 代表 n = 0。

因此,我们调用 pow log(n) 次,但每次迭代 pow 调用的次数是上一步中调用次数的两倍。这意味着我们有 O(n)

【讨论】:

不会兑现pow(a,n/2)的结果,所以是O(n) 在每次调用 pow(a, n/2) 时都会递归。它们被添加到堆栈中,直到它们达到 n == 0 的终止条件。到达末尾所需的计算次数为 O(log(n)) 它不会兑现pow(a,n/2) 的结果,并且每次迭代都会计算两次。在迭代结束时,pow 被称为 O(n) 次。 @DannyuNDos "cash" -> "cache" @user4581301 抱歉打错了,但不,我会用图表证明我是对的。【参考方案3】:

不,因为它涉及递归和分支。其时间复杂度为O(n),空间复杂度为O(log n)

你会得到O(log n)的时间复杂度:

int pow (int a, int n) 
    if(n == 0)  return 1; 
    int halfpow = pow(a,n/2);
    return halfpow * halfpow * (n % 2 == 1 ? a : 1);

【讨论】:

O(n) 意味着对于 n=100 将有 100 次迭代。在这种情况下,只有 8 (100, 50, 25, 12, 6, 3, 1, 0) 次迭代。 @user4581301 否。它将是 (100, 50, 50, 25, 25, 25, 25, 12, 12, 12, 12, 12, 12, 12, 12, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1、1、1、1、1、1、1、1、1、1 和 128 个 0)。 @PeteBecker 这满足O(n)。如果n=8,最后会有(0,0,0,0,0,0,0,0)。如果n=16,最后会有(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)。以此类推。 @user4581301 - “O(n) 意味着 n=100 将有 100 次迭代。”不,这不是 O(n) 的意思。这意味着,对于较大的 n 值,如果将 n 的值加倍,则计算结果所需的操作数会加倍。它没有说明所需的实际操作数量。 (从原文编辑以澄清这是对什么的回应) 你对时间和空间复杂度的分析是对的,但是“因为它涉及递归”的理由是错误的。造成伤害的是递归中的分支,而不是递归本身。

以上是关于这个用于计算指数的递归代码的运行时间是多少?的主要内容,如果未能解决你的问题,请参考以下文章

这个递归函数的运行时间是多少?

如何用Python计算BMI值?

如何查看Win10体验指数

计算给定递归函数的精确运行时间(时间复杂度)

如何计算这个函数的运行时间?

如何使用递归计算幂的幂? [关闭]