昨晚我正在与另一位程序员讨论,即使O(1)可能是O(1),如果O(1)算法中存在一个较大的常数,则O(n)的运算也可能会胜过它。他不同意,所以我把它带到了这里。
是否有一些算法示例大大优于其下面的类?例如,O(n)比O(n)快或O(n 2)比O(n)快。
在数学上,当您忽略常数因子时,可以针对具有渐近上限的函数证明这一点,但是这样的算法在野外是否存在?在哪里可以找到它们的示例?它们用于什么类型的情况?
n
足够大就可以补偿常数(这是big-O表示法的要点)。
昨晚我正在与另一位程序员讨论,即使O(1)可能是O(1),如果O(1)算法中存在一个较大的常数,则O(n)的运算也可能会胜过它。他不同意,所以我把它带到了这里。
是否有一些算法示例大大优于其下面的类?例如,O(n)比O(n)快或O(n 2)比O(n)快。
在数学上,当您忽略常数因子时,可以针对具有渐近上限的函数证明这一点,但是这样的算法在野外是否存在?在哪里可以找到它们的示例?它们用于什么类型的情况?
n
足够大就可以补偿常数(这是big-O表示法的要点)。
Answers:
在很小的固定数据表中查找。优化的哈希表可能是O(1),但由于哈希计算的成本,它比二进制搜索甚至线性搜索要慢。
矩阵乘法。对于小矩阵,在实践中通常使用朴素的O(n ^ 3)算法要比Strassen的O(n ^ 2.8)更快。对于大型矩阵,使用Strassen代替O(n ^ 2.3)Coppersmith-Winograd算法。
一个简单的例子是各种排序算法之间的区别。Mergesort,Heapsort和其他一些是O(n log n)。Quicksort是O(n ^ 2)最坏的情况。但是,通常Quicksort更快,并且实际上它的平均表现像O(n log n)。更多信息。
另一个示例是生成单个斐波那契数。迭代算法为O(n),而基于矩阵的算法为O(log n)。不过,对于前几千个斐波那契数,迭代算法可能会更快。当然这也取决于执行!
渐近性能更好的算法可能包含昂贵的运算,而性能较差的算法却不需要这些运算,但是运算更简单。最后,O运算符仅在其操作的参数急剧增加(逼近无穷大)时才告诉我们有关性能的信息。
注意:请阅读下面@ back2dos和其他专家的评论,因为它们实际上比我写的要有用-感谢所有贡献者。
我认为从下面的图表(摘自:Big O表示法,搜索“算法的悲观本质:”),您可以看到O(log n)并不总是比说O(n)好。因此,我想您的论点是正确的。
y = 1
,y = log x
等和的交点y = 1
和y = x
实际上是点(1,1)
。如果这确实是正确的,那么它会告诉您,对于0到2个条目,复杂度更高的算法可能会更快,这是人们几乎不会在乎的。图表完全无法考虑的因素(以及相关的可察觉的性能差异来自何处)是恒定因素。
对于的实际值n
,可以。这在CS理论中很有用。通常,存在一种复杂的算法,该算法在技术上具有更好的big-Oh性能,但是恒定因素如此之大,以至于使其不切实际。
我曾经让我的计算几何学教授描述一种用于在线性时间内对多边形进行三角剖分的算法,但他的结论是“非常复杂。我认为没有人真正实现它”(!!)。
另外,斐波那契堆比普通堆具有更好的特性,但由于在实践中它们不如常规堆好,因此不是很受欢迎。这可以级联到其他使用堆的算法-例如,在使用斐波那契堆时,Dijkstra的最短路径在数学上更快,但实际上通常不行。
比较插入链表和插入可调整大小的数组。
为了使链表O(1)插入值得,数据量必须相当大。
链表对于下一个指针和取消引用有额外的开销。可调整大小的数组必须复制数据。该复制为O(n),但实际上速度非常快。
Big-Oh表示法用于描述函数的增长率,因此O(1)算法可能会更快,但只能达到特定点(常数因子)。
常用符号:
O(1)-迭代次数(有时可以将其称为函数所花费的用户时间)不取决于输入的大小,并且实际上是恒定的。
O(n)-迭代次数与输入大小成线性比例增长。含义-如果算法对任意输入N进行2次N次迭代,则仍视为O(n)。
O(n ^ 2)(二次)-迭代次数是输入大小的平方。
一种O(1)
算法:
def constant_time_algorithm
one_million = 1000 * 1000
sleep(one_million) # seconds
end
一种O(n)
算法:
def linear_time_algorithm(n)
sleep(n) # seconds
end
显然,对于任何值n
,其中n < one_million
,所述O(n)
实例中所给出的算法将是更快比O(1)
算法。
尽管此示例有些麻烦,但其实质与以下示例等效:
def constant_time_algorithm
do_a_truckload_of_work_that_takes_forever_and_a_day
end
def linear_time_algorithm(n)
i = 0
while i < n
i += 1
do_a_minute_amount_of_work_that_takes_nanoseconds
end
end
你必须知道你的常数和系数O
表达,你必须知道的预期范围n
,以确定先验哪种算法最终会被更快。
否则,您必须使用n
期望范围内的值对这两种算法进行基准测试,以确定后验哪种算法最终会更快。
当程序被交换到磁盘上时,或进行比较时需要从磁盘读取每个项目时,内存中的Bubblesort的性能可能会优于quicksort。
这应该是他可以举例说明的一个例子。
通常,更高级的算法假定一定数量的(昂贵)设置。如果只需要运行一次,则使用蛮力方法可能会更好。
例如:二进制搜索和哈希表查找每次查找都比线性查找快得多,但是它们分别要求您对列表进行排序或构建哈希表。
排序将花费您N log(N),而哈希表将至少花费N。现在,如果您要进行数百或数千次查找,那仍然是摊销的节省。但是,如果您只需要进行一两次查找,则只进行线性搜索并节省启动成本就有意义。
解密通常为0(1)。例如,DES的密钥空间为2 ^ 56,因此解密任何消息都是恒定时间操作。只是您的因子为2 ^ 56,所以它的常数很大。
我想到了set的不同实现。最幼稚的方法之一是在向量上实现它,这意味着remove
也contains
因此add
都取O(N)。
一种替代方法是在一些通用哈希上实现它,该哈希将输入哈希映射到输入值。这样的一组实施执行与O(1) add
,contains
和remove
。
如果我们假设N大约为10,那么第一个实现可能会更快。查找元素所需要做的就是将10个值与1个值进行比较。
另一个实现将必须启动各种巧妙的转换,这比进行10次比较可能要昂贵得多。在所有开销的情况下,您甚至可能会遇到高速缓存未命中的问题,那么从理论上讲解决方案的速度实际上并不重要。
这并不意味着,如果N足够小,您能想到的最差的实现将胜过体面的实现。这仅意味着对于足够小的N,与将可扩展性放在首位的实现相比,具有低占用空间和开销的天真的实现实际上可以需要更少的指令并导致更少的缓存丢失。
您无法真正知道现实世界中的事物有多快,直到您将其整合并进行简单测量。通常结果令人惊讶(至少对我而言)。
是的,对于适当小的N。将始终存在N,在该N之上,您将始终具有以下顺序:O(1)<O(lg N)<O(N)<O(N log N)<O(N ^ c )<O(c ^ N)(其中O(1)<O(lg N)表示在O(1)上,当N适当大且c是大于1的某个固定常数时,算法将执行较少的运算)。
假设特定的O(1)算法正好执行f(N)= 10 ^ 100(googol)个运算,而O(N)算法正好执行g(N)= 2 N + 5个运算。O(N)算法将提供更好的性能,直到您将N近似为googol(实际上是当N>(10 ^ 100-5)/ 2时),因此,如果您仅期望N在1000到十亿之间,使用O(1)算法将遭受重大损失。
或进行实际比较,例如您要将n位数字相乘。所述karatsuba算法是至多3 N ^(LG 3)的操作(即大致为O(n ^ 1.585)),而Schönhage-Strassen的算法是O(N为log N log日志N)这是一种速度更快次序,但是要报价维基百科:
实际上,对于超过2 ^ 2 ^ 15到2 ^ 2 ^ 17(10,000到40,000个十进制数字)的数字,Schönhage–Strassen算法开始优于旧方法(例如Karatsuba和Toom-Cook乘法)。[4] [5] [6 ]
因此,如果将500位数字相乘,则使用大O参数“更快”的算法没有意义。
编辑:通过取极限N-> f(N)/ g(N)的无穷大,可以找到确定g(N)与f(N)的关系。如果极限为0,则f(N)<g(N);如果极限为无穷大,则f(N)> g(N);如果极限为其他常数,则f(N)〜g(N)在大O表示法方面。
对于任何明确定义的问题,总会有最快和最短的算法。不过,从理论上讲,它只是(渐近地)最快的算法。
鉴于问题的任何说明P和实例对这个问题我,它列举所有可能的算法,一个和证明镨,检查每对是否镨是一个有效的证明,一个是渐近最快的算法P。如果找到这样的证据,则它执行一个对我。
搜索该问题对对的复杂度为O(1)(对于固定的问题P),因此对于该问题,您始终使用渐近最快的算法。但是,由于该常数几乎在所有情况下都非常巨大,因此该方法在实践中完全没有用。
许多语言/框架使用朴素的模式匹配来匹配字符串,而不是KMP。我们寻找纽约的汤姆(Tom)之类的字符串,而不是ababaabababababaabaabaabababababab。