大多数拥有计算机科学学位的人肯定知道大O代表什么。 它帮助我们衡量一个算法的可扩展性。

但我很好奇,你是如何计算或近似你的算法的复杂性的?


当前回答

大O符号很有用,因为它很容易使用,并且隐藏了不必要的复杂性和细节(对于一些不必要的定义)。求解分治算法复杂性的一种好方法是树法。假设你有一个带有中值过程的快速排序版本,所以你每次都将数组分割成完美平衡的子数组。

现在,构建一个与所使用的所有数组对应的树。根结点有原始数组,根结点有两个子数组。重复此步骤,直到底部有单个元素数组。

由于我们可以在O(n)时间内找到中位数,并在O(n)时间内将数组分成两部分,因此在每个节点上所做的功为O(k),其中k是数组的大小。树的每一层都包含(最多)整个数组,所以每层的功是O(n)(子数组的大小加起来是n,因为每层有O(k),我们可以把它加起来)。树中只有log(n)层,因为每次我们将输入减半。

因此,我们可以将功的上限设为O(n*log(n))。

然而,大O隐藏着一些我们有时不能忽视的细节。考虑计算斐波那契数列

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

假设a和b在Java中是biginteger或者其他可以处理任意大数字的东西。大多数人会毫不犹豫地说这是一个O(n)算法。理由是,在for循环中有n次迭代,而O(1)工作在循环的一侧。

但是斐波那契数列很大,第n个斐波那契数列是n的指数级,所以仅仅是存储它就需要n个字节。对大整数执行加法将花费O(n)个工作量。所以在这个过程中所做的总功是

一加二加三……+ n = n(n-1)/2 = O(n)

所以这个算法在二次时间内运行!

其他回答

如果你的成本是一个多项式,只保留最高次项,而不保留它的乘数。例如:

(O (n / 2) + 1) * (n / 2)) = O (n2/4 = O (n / 2) + n2/4) = O (n2)

注意,这对无穷级数不成立。对于一般情况,没有单一的方法,但对于一些常见情况,适用以下不等式:

O(log N) < O(N) < O(N log N) < O(N2) < O(Nk) < O(en) < O(n!)

虽然知道如何计算出特定问题的大O时间是有用的,但了解一些一般情况可以在很大程度上帮助您在算法中做出决策。

以下是一些最常见的案例,摘自http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions:

O(1) -确定一个数字是偶数还是奇数;使用常量大小的查找表或哈希表

O(logn) -用二分搜索在排序数组中查找一个项

O(n) -在未排序的列表中查找一个项;两个n位数相加

O(n2) -用一个简单的算法乘以两个n位数字;添加两个n×n矩阵;冒泡排序或插入排序

O(n3) -用简单的算法乘以两个n×n矩阵

O(cn) -使用动态规划找到旅行商问题的(精确)解;使用蛮力判断两个逻辑语句是否等效

O(n!) -通过暴力搜索解决旅行推销员问题

O(nn) -通常用来代替O(n!)来推导更简单的渐近复杂度公式

让我们从头说起。

首先,接受这样一个原则:对数据的某些简单操作可以在O(1)时间内完成,即在与输入大小无关的时间内完成。C语言中的这些基本操作由

算术运算(例如+或%)。 逻辑操作(如&&)。 比较操作(例如,<=)。 结构访问操作(例如A[i]这样的数组索引,或指针后跟 使用->操作符降低)。 简单的赋值,例如将值复制到变量中。 调用库函数(例如,scanf, printf)。

要证明这一原理,需要对典型计算机的机器指令(基本步骤)进行详细研究。所描述的每一个操作都可以用少量的机器指令来完成;通常只需要一个或两个指令。 因此,C语言中的几种语句可以在O(1)时间内执行,也就是说,在与输入无关的某个常数时间内执行。这些简单的包括

表达式中不涉及函数调用的赋值语句。 读语句。 编写不需要调用函数来计算参数的语句。 跳转语句有break、continue、goto和return表达式 表达式不包含函数调用。

在C语言中,许多for循环是通过将索引变量初始化为某个值和来形成的 在每次循环中对该变量加1。for循环结束于 指数达到某个极限。例如,For循环

for (i = 0; i < n-1; i++) 
{
    small = i;
    for (j = i+1; j < n; j++)
        if (A[j] < A[small])
            small = j;
    temp = A[small];
    A[small] = A[i];
    A[i] = temp;
}

使用索引变量i。它在循环和迭代中每一次都使i增加1 当I达到n−1时停止。

然而,目前,我们只关注for循环的简单形式,其中最终值和初始值之间的差值除以索引变量的增量,告诉我们循环了多少次。这个计数是准确的,除非有办法通过跳转语句退出循环;在任何情况下,它都是迭代次数的上限。

例如,For循环迭代((n−1)−0)/1 = n−1次, 由于0是i的初始值,n−1是i达到的最大值(即当i 到达n−1时,循环停止,当I = n−1)时不发生迭代,并添加1 在循环的每一次迭代中。

In the simplest case, where the time spent in the loop body is the same for each iteration, we can multiply the big-oh upper bound for the body by the number of times around the loop. Strictly speaking, we must then add O(1) time to initialize the loop index and O(1) time for the first comparison of the loop index with the limit, because we test one more time than we go around the loop. However, unless it is possible to execute the loop zero times, the time to initialize the loop and test the limit once is a low-order term that can be dropped by the summation rule.


现在想想这个例子:

(1) for (j = 0; j < n; j++)
(2)   A[i][j] = 0;

我们知道直线(1)花费O(1)时间。显然,我们循环了n次 我们可以用在线上得到的上限减去下限来确定 (1)再加1。由于主体,行(2),花费O(1)时间,我们可以忽略 增加j的时间和比较j与n的时间,两者都是O(1)。 因此,行(1)和行(2)的运行时间是n和O(1)的乘积,即O(n)。

类似地,我们可以限制由行组成的外部循环的运行时间 (2)到(4),即

(2) for (i = 0; i < n; i++)
(3)     for (j = 0; j < n; j++)
(4)         A[i][j] = 0;

我们已经建立了行(3)和行(4)的循环花费O(n)时间。 因此,我们可以忽略O(1)时间来增加i,并测试i是否< n in 每次迭代,得出每次外循环迭代花费O(n)时间。

外部循环的初始化i = 0和条件的(n + 1)st检验 i < n同样需要O(1)次,可以忽略。最后,我们观察到我们走了 绕外循环n圈,每次迭代花费O(n)时间,得到总数 O(n²)运行时间。


一个更实际的例子。

我不知道如何通过编程来解决这个问题,但人们做的第一件事是我们对算法的特定模式进行抽样,比如4n²+ 2n + 1我们有两个规则:

如果我们有一个项的和,增长率最大的项被保留,其他项被省略。 如果我们有几个因数的乘积,常数因数就省略了。

如果我们化简f(x),其中f(x)是所做操作数量的公式,(上文解释的4n²+ 2n + 1),我们得到大O值[在这种情况下是O(n²)]。但这必须考虑到程序中的拉格朗日插值,这可能很难实现。如果真正的大O值是O(2^n)我们可能有O(x^n)这样的东西,那么这个算法可能是不可编程的。但如果有人证明我错了,给我代码. . . .

大O符号很有用,因为它很容易使用,并且隐藏了不必要的复杂性和细节(对于一些不必要的定义)。求解分治算法复杂性的一种好方法是树法。假设你有一个带有中值过程的快速排序版本,所以你每次都将数组分割成完美平衡的子数组。

现在,构建一个与所使用的所有数组对应的树。根结点有原始数组,根结点有两个子数组。重复此步骤,直到底部有单个元素数组。

由于我们可以在O(n)时间内找到中位数,并在O(n)时间内将数组分成两部分,因此在每个节点上所做的功为O(k),其中k是数组的大小。树的每一层都包含(最多)整个数组,所以每层的功是O(n)(子数组的大小加起来是n,因为每层有O(k),我们可以把它加起来)。树中只有log(n)层,因为每次我们将输入减半。

因此,我们可以将功的上限设为O(n*log(n))。

然而,大O隐藏着一些我们有时不能忽视的细节。考虑计算斐波那契数列

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

假设a和b在Java中是biginteger或者其他可以处理任意大数字的东西。大多数人会毫不犹豫地说这是一个O(n)算法。理由是,在for循环中有n次迭代,而O(1)工作在循环的一侧。

但是斐波那契数列很大,第n个斐波那契数列是n的指数级,所以仅仅是存储它就需要n个字节。对大整数执行加法将花费O(n)个工作量。所以在这个过程中所做的总功是

一加二加三……+ n = n(n-1)/2 = O(n)

所以这个算法在二次时间内运行!