为什么Fibonacci序列大O(2 ^ n)而不是O(logn)?

时间:2016-01-09 20:57:08

标签: algorithm sorting recursion tree

我前一段时间采用离散数学(我在其中学习了主定理,Big Theta / Omega / O),我似乎忘记了O(logn)和O(2 ^ n)之间的区别(不是在理论意义上的大哦)。我通常理解合并和快速排序等算法是O(nlogn),因为它们重复将初始输入数组划分为子数组,直到每个子数组的大小为1,然后再递归树,给出一个高度为logn的递归树+ 1.但是如果使用n / b ^ x = 1计算递归树的高度(当子问题的大小变为1时,如答案here中所述),似乎总是得到树的高度是log(n)。

如果使用递归解决Fibonacci序列,我认为你也会得到一个logn大小的树,但由于某种原因,算法的Big O是O(2 ^ n)。我在想,也许差别是因为你必须记住每个子问题的所有fib数,以获得实际的fib数,这意味着每个节点的值必须被调用,但似乎在合并排序中,值每个节点的数量也必须使用(或至少排序)。这与二进制搜索不同,但是,您只能根据在树的每个级别进行的比较来访问某些节点,所以我认为这是混乱的来源。

具体来说,是什么导致Fibonacci序列具有与合并/快速排序等算法不同的时间复杂度?

6 个答案:

答案 0 :(得分:14)

其他答案是正确的,但不清楚 - 斐波那契算法和分而治之算法之间的巨大差异来自何处?实际上,两类函数的递归树的形状是相同的 - 它是二叉树。

理解的技巧实际上非常简单:将递归树的 size 视为输入大小n的函数。

首先回顾一些关于binary trees的基本事实:

  • 叶子n的数量是二叉树,等于非叶子节点的数量加一。因此,二叉树的大小为2n-1。
  • 完美二叉树中,所有非叶节点都有两个子节点。
  • 对于随机二叉树,具有h叶子的完美二叉树的高度n等于log(n)h = O(log(n)),以及退化二叉树h = n-1 {1}}。

直观地:

  • 为了使用递归算法对n元素数组进行排序,递归树具有n 。因此,树的宽度 n ,树的高度 O(log(n)) 平均而言 O(n)

  • 为了使用递归算法计算Fibonacci序列元素k,递归树具有k 级别(要了解原因,请考虑{{1} }调用fib(k),调用fib(k-1),依此类推)。因此,树的高度 fib(k-2) 。要估计递归树中节点宽度和数量的下限,请考虑因为k也调用fib(k),因此有一个完美高度的二叉树fib(k-2)作为递归树的一部分。如果被提取,那么该完美子树将具有2个 k / 2 叶节点。因此,递归树的 width 至少为k/2,或等效地 O(2^{k/2})

关键的区别在于:

  • 对于分而治之算法,输入大小是二叉树的宽度
  • 对于Fibonnaci算法,输入大小是树的高度

因此,第一种情况下树中的节点数为2^O(k),而第二种情况下为O(n)。与输入大小相比,Fibonacci树 更大。

你提到Master theorem;然而,该定理不能用于分析Fibonacci的复杂性,因为它仅适用于输入在每个递归级别实际划分的算法。斐波那契划分输入;实际上,级别2^O(n)的函数产生的输入几乎是下一级i的两倍。

答案 1 :(得分:4)

要解决问题的核心问题,那就是为什么斐波那契而不是Mergesort",你应该专注于这个至关重要的区别:

  • 从Mergesort获得的树每个级别都有N个元素,并且有log(n)级别。
  • 你从Fibonacci得到的树有N级,因为F(N)的公式中存在F(n-1),并且每个级别的元素数量可以变化很大:它可以非常低(靠近根部,或靠近最低的叶子)或非常高。当然,这是因为重复计算相同的值。

要查看"重复计算"的含义,请查看树计算F(6):

fib(6) computation tree
Fibonacci树图片来自:http://composingprograms.com/pages/28-efficiency.html

你看到F(3)被计算了多少次?

答案 2 :(得分:3)

考虑以下实现

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

让我们用T(n)表示fib执行计算fib(n)的操作次数。由于fib(n)正在调用fib(n-1)fib(n-2),这意味着T(n)至少为T(n-1) + T(n-2)。这反过来意味着T(n) > fib(n)。有fib(n)的直接公式,它与n的幂相等。因此T(n)至少是指数的。 QED。

答案 3 :(得分:2)

根据我的理解,你的推理中的错误是使用递归实现来评估f,其中f(n) = f (n-1) + f(n-2) 表示Fibonacci序列,输入大小减少了2倍(或其他一些因素),情况并非如此。每次调用(“基本情况”0和1除外)使用正好2次递归调用,因为不可能重新使用先前计算的值。根据{{​​3}}上的主定理的呈现,重现

var select = document.querySelector('#select');

selectByValue( select, '1' );

function selectByValue( select, value ){
  // use array prototype to filter down to a single value
  var option = Array.prototype.filter.call( select, function( option ){
    option.removeAttribute('selected');
    return option.value == value;
  })
  console.log( option[0] );
  // if there is an option that matches set it's selected attribute
  option[0] && option[0].setAttribute('selected', 'selected');
}

是无法应用主定理的情况。

答案 4 :(得分:2)

使用递归算法,您对斐波那契(N)有大约2 ^ N个运算(加法)。 然后是O(2 ^ N)。

使用缓存(memoization),您有大约N个操作,然后它是O(N)。

复杂度 O(N log N)的算法通常是迭代每个项目(O(N)),拆分递归和合并的结合...拆分2 =&gt;你记录N次递归。

答案 5 :(得分:2)

合并排序时间复杂度为O(n log(n))。快速排序最佳情况是O(n log(n)),最坏情况是O(n ^ 2)。

其他答案解释了为什么天真的递归Fibonacci是O(2 ^ n)。

如果你读到Fibonacci(n)可以是O(log(n)),如果使用迭代和使用矩阵方法或lucas序列方法重复平方计算,这是可能的。 lucas序列方法的示例代码(注意每个循环的n除以2):

/* lucas sequence method */
int fib(int n) {
    int a, b, p, q, qq, aq;
    a = q = 1;
    b = p = 0;
    while(1) {
        if(n & 1) {
            aq = a*q;
            a = b*q + aq + a*p;
            b = b*p + aq;
        }
        n /= 2;
        if(n == 0)
            break;
        qq = q*q;
        q = 2*p*q + qq;
        p = p*p + qq;
    }
    return b;
}