大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。
但我很好奇,你是如何计算或近似你的算法的复杂性的?
大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。
但我很好奇,你是如何计算或近似你的算法的复杂性的?
当前回答
除了使用主方法(或其专门化之一)之外,我还通过实验测试了我的算法。这不能证明达到了任何特定的复杂度等级,但它可以保证数学分析是适当的。为了保证这一点,我将代码覆盖工具与我的实验结合起来使用,以确保我使用了所有的案例。
作为一个非常简单的例子,假设你想要对. net框架的列表排序的速度进行完整性检查。你可以像下面这样写,然后在Excel中分析结果,以确保它们不超过n*log(n)曲线。
在这个例子中,我测量了比较的数量,但也要谨慎地检查每个样本量所需的实际时间。然而,您必须更加小心,因为您只是在度量算法,而不包括来自测试基础结构的工件。
int nCmp = 0;
System.Random rnd = new System.Random();
// measure the time required to sort a list of n integers
void DoTest(int n)
{
List<int> lst = new List<int>(n);
for( int i=0; i<n; i++ )
lst[i] = rnd.Next(0,1000);
// as we sort, keep track of the number of comparisons performed!
nCmp = 0;
lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }
System.Console.Writeline( "{0},{1}", n, nCmp );
}
// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
DoTest(n);
其他回答
我想从另一个角度来解释Big-O。
Big-O只是用来比较程序的复杂性,也就是当输入增加时它们的增长速度有多快,而不是花在执行操作上的确切时间。
恕我直言,在大o公式中,你最好不要使用更复杂的方程(你可以坚持使用下图中的方程)。然而,你仍然可以使用其他更精确的公式(如3^n, n^3,…),但有时会误导!所以还是尽量简单为好。
我想再次强调,这里我们不想得到一个精确的算法公式。我们只想展示当输入增加时它是如何增长的并在这方面与其他算法进行比较。否则,您最好使用不同的方法,如基准测试。
我从信息的角度来考虑。任何问题都包括学习一定数量的比特。
你的基本工具是决策点及其熵的概念。一个决策点的熵是它会给你的平均信息。例如,如果一个程序包含一个有两个分支的决策点,它的熵是每个分支的概率乘以该分支的逆概率的log2的和。这就是你从执行决策中学到的东西。
例如,一个if语句有两个分支,都是等可能的,其熵为1/2 * log(2/1) + 1/2 * log(2/1) = 1/2 * 1 + 1/2 * 1 = 1。所以它的熵是1比特。
假设您正在搜索一个包含N个条目的表,例如N=1024。这是一个10位问题,因为log(1024) = 10位。所以如果你可以用if语句搜索结果的可能性相等,它应该需要10个决定。
这就是二分搜索的结果。
假设你在做线性搜索。您查看第一个元素并询问它是否是您想要的元素。是的概率是1/1024,不是的概率是1023/1024。该决策的熵为1/1024*log(1024/1) + 1023/1024 *log(1024/1023) = 1/1024* 10 + 1023/1024 * about 0 =约0.01 bit。你学得太少了!第二个决定也好不到哪里去。这就是为什么线性搜索这么慢。事实上,你需要学习的比特数是指数级的。
假设你在做索引。假设表被预先排序到许多箱子中,并且您使用键中的所有位中的一些位直接索引到表项。如果有1024个箱子,熵为1/1024 * log(1024) + 1/1024 * log(1024) +…对于所有1024个可能的结果。这是1/1024 * 10乘以1024个结果,或者对一个索引操作来说是10比特的熵。这就是为什么索引搜索是快速的。
现在想想排序。你有N个项目,你有一个列表。对于每个项目,您必须搜索项目在列表中的位置,然后将其添加到列表中。排序大约需要N倍于底层搜索的步数。
基于二元决策的排序结果都是等概率的都需要O(N log N)步。基于索引搜索的O(N)排序算法是可行的。
我发现几乎所有的算法性能问题都可以用这种方式来看待。
小提示:大O符号是用来表示渐近复杂度的(也就是说,当问题的大小增长到无穷大时),它隐藏了一个常数。
这意味着在O(n)和O(n2)的算法之间,最快的并不总是第一个算法(尽管总是存在一个值n,这样对于大小为>n的问题,第一个算法是最快的)。
注意,隐藏常数很大程度上取决于实现!
此外,在某些情况下,运行时并不是输入大小为n的确定函数。以快速排序为例:对n个元素的数组进行排序所需的时间不是一个常数,而是取决于数组的初始配置。
有不同的时间复杂度:
最坏的情况(通常是最简单的,但并不总是很有意义) 一般情况下(通常很难弄清楚…) ...
一个很好的介绍是R. Sedgewick和P. Flajolet的《算法分析导论》。
正如你所说,过早的优化是万恶之源,(如果可能的话)在优化代码时真的应该总是使用分析。它甚至可以帮助您确定算法的复杂性。
基本上90%的情况下都是分析循环。你有单、双、三重嵌套循环吗?你有O(n) O(n²)O(n³)的运行时间。
很少(除非你正在编写一个具有广泛基库的平台(例如,.NET BCL或c++的STL),你会遇到比查看循环(for语句,while, goto等…)更困难的事情。
好问题!
免责声明:这个答案包含虚假陈述,见下面的评论。
如果您正在使用大O,那么您正在谈论的是最坏的情况(后面将详细介绍它的含义)。此外,在平均情况下有大写的theta,在最佳情况下有大的omega。
你可以在这个网站上找到大O的正式定义:https://xlinux.nist.gov/dads/HTML/bigOnotation.html
f(n) = O(g(n))表示存在正常数c和k,使得当n≥k时0≤f(n)≤cg(n)。对于函数f, c和k的值必须是固定的,且不依赖于n。
好的,那么我们所说的"最佳情况"和"最坏情况"是什么意思呢?
这一点可以通过例子得到最清楚的说明。例如,如果我们使用线性搜索在一个排序数组中查找一个数字,那么最坏的情况是我们决定搜索数组的最后一个元素,因为这将花费与数组中有多少项一样多的步骤。最好的情况是当我们搜索第一个元素时,因为我们将在第一次检查之后完成。
The point of all these adjective-case complexities is that we're looking for a way to graph the amount of time a hypothetical program runs to completion in terms of the size of particular variables. However for many algorithms you can argue that there is not a single time for a particular size of input. Notice that this contradicts with the fundamental requirement of a function, any input should have no more than one output. So we come up with multiple functions to describe an algorithm's complexity. Now, even though searching an array of size n may take varying amounts of time depending on what you're looking for in the array and depending proportionally to n, we can create an informative description of the algorithm using best-case, average-case, and worst-case classes.
抱歉,这是如此糟糕的写作和缺乏太多的技术信息。但希望这能让时间复杂度类更容易理解。一旦你熟悉了这些,你就可以很简单地解析你的程序,寻找像for-loops这样依赖于数组大小的东西,并根据你的数据结构推理什么样的输入会导致简单的情况,什么样的输入会导致最坏的情况。