ChatGPT解决这个技术问题 Extra ChatGPT

确定递归函数的复杂度(大 O 表示法)

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

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);
}
如果不想每次都经过分析,有一种黑盒技术,叫做 Master 方法。但是假设输入的所有递归拆分在每个实例中都具有相同的大小。
描述 5 : O(f(n)) = T(n/2) ... T((n-5)/2) ... T((n-10)/2)...1 所以树的高度为 n/5。所以这会给你 O(f(n)) 需要 T((n/5 * n/2) - (5/2 *n/5)) 所以绑定在输入 n 上,在最坏的情况下递归情况会取 O(2^N)。在最好的情况和一般的情况下也是如此。

c
coder

每个函数的时间复杂度,用大 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))(以 5 为底),通常称为 logarithmic,最常见的是 Big 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 正在争取的上限,我们只对 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 次
n
nhahtdh

对于 n <= 0 的情况,T(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)

我最近在一个与分析递归斐波那契函数的时间和空间复杂度有关的面试问题(并通过扩展面试)失败了。这个答案是史诗般的,它有很大帮助,我喜欢它,我希望我能投票给你两次。我知道它很旧,但你有什么类似的计算空间 - 也许是一个链接,什么?
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)
S
Shubham

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

Complexity = length of tree from root node to leaf node * number of leaf nodes

第一个函数的长度为 n,叶节点数为 1,因此复杂度为 n*1 = n 第二个函数的长度为 n/5,叶节点数再次为 1,因此复杂度为 n/5 * 1 = n/5。它应该近似为 n 对于第三个函数,由于 n 在每次递归调用时都被 5 除,递归树的长度将为 log(n)(base 5),叶节点的数量再次为 1,因此复杂度将为 log (n)(base 5) * 1 = log(n)(base 5) 对于第四个函数,因为每个节点都有两个子节点,所以叶节点的数量将等于 (2^n) 和递归的长度树将为 n,因此复杂度将为 (2^n) * n。但是由于n在(2^n)前面是微不足道的,所以可以忽略不计,复杂度只能说是(2^n)。对于第五个功能,有两个元素引入了复杂性。函数的递归性质引入的复杂性以及每个函数中 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)) 高度。
O
OhadM

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

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

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:T(n) = T(nk) + k。下一步是取 nk 并声明 nk = 1,因为在递归结束时,当 n<=0 时,它将恰好花费 O(1)。从这个简单的等式,我们现在知道 k = n - 1。让我们将 k 放在我们的最终方法中: T(n) = T(nk) + k 这将给我们: T(n) = 1 + n - 1 这是正好是 n 或 O(n)。与 1 相同。您可以自己测试一下,看看是否得到 O(n)。 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 + logn 即 O(logn) 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(nk) + k。让我们通过声明 nk = 1 来找到 k,即 k = n - 1。让我们将 k 放置如下: T(n) = 2^(n-1) + n - 1 大致为 O(2^n) 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 (nk*5) + kn + k) 再次:n-5k = 1,即 n = 5k + 1,大致为 n = k。这将给我们:T(n) = T(0) + n^2 + n,大致为 O(n^2)。

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


h
higlu

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

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 级 1 n-1 级 2 n-2 级 3 n-3 级 4 ... ~ n 级 -> 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 级 -> 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);
}

调用树是:

nn/5 n/5^2 n/5^3 ... ~ log5(n) 级别 -> 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 1级 n-1 n-1 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 级 -> 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 级 -> 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 - 5recursiveFun2 中没有 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 个电话。
你在这里分享的公式的真实来源在哪里?谢谢。
D
Dharman

我看到对于接受的答案(recursivefn5),有些人对解释有疑问。所以我会尽我所能澄清。

for 循环运行 n/2 次,因为在每次迭代中,我们将 i(计数器)增加 2 倍。所以说 n = 10,for 循环将运行 10/2 = 5 次,即当 i 为 0 ,2,4,6 和 8。同样,递归调用每被调用一次就会减少 5 倍,即运行 n/5 次。再次假设 n = 10,递归调用运行 10/5 = 2 次,即当 n 为 10 和 5 时,它遇到基本情况并终止。计算总运行时间,每次调用递归函数时,for 循环都会运行 n/2 次。由于递归 fxn 运行 n/5 次(在上面的 2 中),for 循环运行 (n/2) * (n/5) = (n^2)/10 次,这转换为整体 Big O 运行时间O(n^2) - 忽略常数 (1/10)...


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅