我前一段时间采用离散数学(我在其中学习了主定理,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序列具有与合并/快速排序等算法不同的时间复杂度?
答案 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})
。
关键的区别在于:
因此,第一种情况下树中的节点数为2^O(k)
,而第二种情况下为O(n)
。与输入大小相比,Fibonacci树 更大。
你提到Master theorem;然而,该定理不能用于分析Fibonacci的复杂性,因为它仅适用于输入在每个递归级别实际划分的算法。斐波那契不划分输入;实际上,级别2^O(n)
的函数产生的输入几乎是下一级i
的两倍。
答案 1 :(得分:4)
要解决问题的核心问题,那就是为什么斐波那契而不是Mergesort",你应该专注于这个至关重要的区别:
要查看"重复计算"的含义,请查看树计算F(6):
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;
}