[英]How can I find the time complexity of an algorithm?
我已经通过Google和Stack Overflow进行了搜索,但我无法在任何地方找到关于如何计算时间复杂度的清晰直接的解释。
对于像下面这样简单的代码来说:
char h = 'y'; // This will be executed 1 time
int abc = 0; // This will be executed 1 time
说一个像下面这样的循环:
for (int i = 0; i < N; i++) {
Console.Write('Hello, World!!');
}
int i=0;
这只会执行一次。 时间实际上是计算到i=0
而不是声明。
i < N;
这将执行N+1次i++
这将被执行N次所以这个循环需要的操作数是{1+(N+1)+N} = 2N+2 。 (但这仍然可能是错误的,因为我对自己的理解没有信心。)
好的,所以我想我知道这些小的基本计算,但在大多数情况下,我已经看到时间复杂度为O(N)、O(n^2)、O(log n)、O(n!)和许多其他.
如何找到算法的时间复杂度
您将根据其输入大小的函数将执行多少机器指令相加,然后将表达式简化为最大(当 N 非常大时)项,并且可以包含任何简化常数因子。
例如,让我们看看我们如何简化2N + 2
条机器指令以将其描述为O(N)
。
为什么我们要删除两个2
s ?
当 N 变大时,我们对算法的性能感兴趣。
考虑两个术语 2N 和 2。
当 N 变大时,这两项的相对影响是什么? 假设 N 是一百万。
那么第一期是200万,第二期只有2。
出于这个原因,我们删除了除大 N 的最大项之外的所有项。
所以,现在我们已经从2N + 2
变成了2N
。
传统上,我们只对常数因子的性能感兴趣。
这意味着当 N 很大时,我们并不真正关心性能差异是否存在恒定倍数。 无论如何,2N 的单位一开始就没有明确定义。 所以我们可以乘以或除以一个常数因子来得到最简单的表达式。
所以2N
变成了N
。
这是一篇很棒的文章: 算法的时间复杂度
下面的答案是从上面复制的(以防优秀的链接失效)
计算时间复杂度的最常用指标是大 O 表示法。 这消除了所有常数因素,以便当 N 接近无穷大时,可以相对于 N 估计运行时间。 一般来说,你可以这样想:
statement;
是恒定的。 语句的运行时间不会相对于 N 改变。
for ( i = 0; i < N; i++ )
statement;
是线性的。 循环的运行时间与 N 成正比。当 N 加倍时,运行时间也加倍。
for ( i = 0; i < N; i++ ) {
for ( j = 0; j < N; j++ )
statement;
}
是二次方的。 两个循环的运行时间与N的平方成正比。当N翻倍时,运行时间增加N * N。
while ( low <= high ) {
mid = ( low + high ) / 2;
if ( target < list[mid] )
high = mid - 1;
else if ( target > list[mid] )
low = mid + 1;
else break;
}
是对数的。 该算法的运行时间与 N 可以除以 2 的次数成正比。这是因为该算法在每次迭代时将工作区域分成两半。
void quicksort (int list[], int left, int right)
{
int pivot = partition (list, left, right);
quicksort(list, left, pivot - 1);
quicksort(list, pivot + 1, right);
}
是 N * log (N)。 运行时间由对数的 N 个循环(迭代或递归)组成,因此该算法是线性和对数的组合。
一般来说,对一维中的每个项目做某事是线性的,对二维中的每个项目做某事是二次的,将工作区域分成两半是对数的。 还有其他大 O 度量,例如三次、指数和平方根,但它们并不常见。 大 O 表示法被描述为O ( <type> )
,其中<type>
是度量。 快速排序算法将被描述为O (N * log(N ))
。
请注意,这些都没有考虑到最佳、平均和最坏情况的措施。 每个都有自己的大 O 符号。 另请注意,这是一个非常简单的解释。 Big O 是最常见的,但也比我展示的更复杂。 还有其他符号,例如大 omega、小 o 和大 theta。 您可能不会在算法分析课程之外遇到它们。 ;)
取自这里 - 算法的时间复杂度简介
在计算机科学中,算法的时间复杂度量化了算法运行所花费的时间,作为表示输入的字符串长度的函数。
算法的时间复杂度通常用大 O 表示法表示,它不包括系数和低阶项。 当以这种方式表达时,时间复杂度被认为是渐近描述的,即,随着输入大小趋于无穷大。
例如,如果算法对大小为 n 的所有输入所需的时间最多为 5n 3 + 3n,则渐近时间复杂度为 O(n 3 )。 稍后再谈。
再举几个例子:
如果无论输入大小如何,算法都需要相同的时间,则称该算法以恒定时间运行。
例子:
如果算法的执行时间与输入大小成正比,则称算法以线性时间运行,即时间随着输入大小的增加呈线性增长。
考虑以下示例。 下面我线性搜索一个元素,它的时间复杂度为 O(n)。
int find = 66;
var numbers = new int[] { 33, 435, 36, 37, 43, 45, 66, 656, 2232 };
for (int i = 0; i < numbers.Length - 1; i++)
{
if(find == numbers[i])
{
return;
}
}
更多示例:
如果算法的执行时间与输入大小的对数成正比,则称该算法以对数时间运行。
示例:二分搜索
回想一下“二十题”游戏——任务是在一个区间内猜测一个隐藏数字的值。 每次您进行猜测时,系统都会告诉您您的猜测是太高还是太低。 二十个问题游戏意味着使用您的猜测数将间隔大小减半的策略。 这是称为二进制搜索的一般问题解决方法的示例。
如果算法的执行时间与输入大小的平方成正比,则称该算法以二次时间运行。
例子:
循环的几个例子。
如果循环变量以恒定量递增/递减,则循环的O(n)时间复杂度被视为O(n) 。 例如以下函数具有O(n)时间复杂度。
// Here c is a positive integer constant for (int i = 1; i <= n; i += c) { // some O(1) expressions } for (int i = n; i > 0; i -= c) { // some O(1) expressions }
嵌套循环的O(n c )时间复杂度等于执行最内层语句的次数。 例如,以下示例循环具有O(n 2 )时间复杂度
for (int i = 1; i <=n; i += c) { for (int j = 1; j <=n; j += c) { // some O(1) expressions } } for (int i = n; i > 0; i += c) { for (int j = i+1; j <=n; j += c) { // some O(1) expressions }
如果循环变量除以/乘以一个常数,则循环的O(log n)时间复杂度被视为O(log n) 。
for (int i = 1; i <=n; i *= c) { // some O(1) expressions } for (int i = n; i > 0; i /= c) { // some O(1) expressions } For example, [binary search][3] has _O(log n)_ time complexity.
如果循环变量以恒定的量以指数方式减少/增加,则循环的O(log log n)时间复杂度被视为O(log log n) 。
// Here c is a constant greater than 1 for (int i = 2; i <=n; i = pow(i, c)) { // some O(1) expressions } //Here fun is sqrt or cuberoot or any other constant root for (int i = n; i > 0; i = fun(i)) { // some O(1) expressions }
时间复杂度分析的一个例子
int fun(int n)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < n; j += i)
{
// Some O(1) task
}
}
}
分析:
For i = 1, the inner loop is executed n times.
For i = 2, the inner loop is executed approximately n/2 times.
For i = 3, the inner loop is executed approximately n/3 times.
For i = 4, the inner loop is executed approximately n/4 times.
…………………………………………………….
For i = n, the inner loop is executed approximately n/n times.
所以上述算法的总时间复杂度为(n + n/2 + n/3 + … + n/n)
,变为n * (1/1 + 1/2 + 1/3 + … + 1/n)
系列(1/1 + 1/2 + 1/3 + … + 1/n)
的重要之处在于O(log n) 。 所以上面代码的时间复杂度是O(n·log n) 。
参考:
1 - 基本操作(算术、比较、访问数组元素、赋值):运行时间始终为常数 O(1)
例子:
read(x) // O(1)
a = 10; // O(1)
a = 1,000,000,000,000,000,000 // O(1)
2 - If then else 语句:仅从两个或多个可能的语句中获取最大运行时间。
例子:
age = read(x) // (1+1) = 2
if age < 17 then begin // 1
status = "Not allowed!"; // 1
end else begin
status = "Welcome! Please come in"; // 1
visitors = visitors + 1; // 1+1 = 2
end;
因此,上述伪代码的复杂度为 T(n) = 2 + 1 + max(1, 1+2) = 6。因此,它的大 oh 仍然是常数 T(n) = O(1)。
3 - 循环( for 、 while 、 repeat ):此语句的运行时间是循环数乘以该循环内的操作数。
例子:
total = 0; // 1
for i = 1 to n do begin // (1+1)*n = 2n
total = total + i; // (1+1)*n = 2n
end;
writeln(total); // 1
因此,它的复杂度为 T(n) = 1+4n+1 = 4n + 2。因此,T(n) = O(n)。
4 - 嵌套循环(循环内循环):由于主循环内至少有一个循环,因此该语句的运行时间使用 O(n^2) 或 O(n^3)。
例子:
for i = 1 to n do begin // (1+1)*n = 2n
for j = 1 to n do begin // (1+1)n*n = 2n^2
x = x + 1; // (1+1)n*n = 2n^2
print(x); // (n*n) = n^2
end;
end;
分析算法时有一些常见的运行时间:
O(1) – 恒定时间
恒定时间意味着运行时间是恒定的,它不受输入大小的影响。
O(n) - 线性时间
当一个算法接受 n 个输入大小时,它也会执行 n 个操作。
O(log n) – 对数时间
运行时间为 O(log n) 的算法比 O(n) 稍快。 通常,算法将问题分成大小相同的子问题。 示例:二分查找算法、二分转换算法。
O(n log n) –线性时间
这种运行时间经常出现在“分治算法”中,该算法将问题递归地划分为子问题,然后在 n 时间内将它们合并。 示例:合并排序算法。
O(n 2 ) – 二次时间
看冒泡排序算法!
O(n 3 ) – 立方时间
它与 O(n 2 ) 具有相同的原理。
O(2 n ) – 指数时间
当输入变大时它非常慢,如果 n = 1,000,000,T(n) 将是 21,000,000。 蛮力算法有这个运行时间。
O(n!) – 阶乘时间
最慢!!! 示例:旅行商问题(TSP)
它取自这篇文章。 它解释得很好,你应该读一读。
当您分析代码时,您必须逐行分析它,计算每个操作/识别时间复杂度。 最后,你必须总结它才能得到全貌。
例如,您可以有一个具有线性复杂度的简单循环,但稍后在同一程序中您可以有一个具有三次复杂度的三重循环,因此您的程序将具有三次复杂度。 增长的功能顺序在这里发挥作用。
让我们看看算法的时间复杂度有哪些可能性,你可以看到我上面提到的增长顺序:
常数时间的增长顺序为1 ,例如: a = b + c 。
对数时间的增长顺序为log N 。 它通常发生在您将某物分成两半(二分搜索、树,甚至循环)或以相同方式相乘时。
线性。 增长的顺序是N ,例如
int p = 0; for (int i = 1; i < N; i++) p = p + 2;
立方。 增长的顺序是N 3 。 一个经典的例子是一个三重循环,您可以在其中检查所有三元组:
int x = 0; for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) for (int k = 0; k < N; k++) x = x + 2
指数。 增长的顺序是 2 N 。 它通常发生在您进行详尽搜索时,例如,检查某个集合的子集。
松散地说,时间复杂度是一种总结算法的操作数量或运行时间如何随着输入大小的增加而增长的方式。
就像生活中的大多数事情一样,鸡尾酒会可以帮助我们理解。
上)
当你到达派对时,你必须和每个人握手(对每个项目进行操作)。 随着参加者人数N
的增加,您与每个人握手所需的时间/工作量会随着O(N)
的增加而增加。
为什么是O(N)
而不是cN
?
与人握手所需的时间有所不同。 您可以将其平均并以常数c
捕获。 但是这里的基本操作——与所有人握手——总是与O(N)
成正比,不管c
是什么。 在讨论是否应该参加鸡尾酒会时,我们通常更感兴趣的是我们必须与每个人会面这一事实,而不是那些会议的细节。
O(N^2)
鸡尾酒会的主人想让你玩一个愚蠢的游戏,每个人都会遇到其他人。 因此,您必须会见N-1
其他人,并且因为下一个人已经遇到过您,所以他们必须会见N-2
个人,以此类推。 这个系列的总和是x^2/2+x/2
。 随着参加者人数的增加, x^2
术语会快速变大,所以我们放弃其他所有内容。
O(N^3)
您必须会见其他所有人,并且在每次会议期间,您必须谈论房间里的其他所有人。
O(1)
主持人想要宣布一些事情。 他们敲响酒杯,大声说话。 每个人都听到他们的声音。 事实证明,无论有多少参加者,此操作总是花费相同的时间。
O(log N)
主持人按字母顺序将每个人都安排在了餐桌上。 丹在哪里? 你推断他一定介于亚当和曼迪之间(当然不是在曼迪和扎克之间!)。 鉴于此,他在乔治和曼迪之间吗? 不,他必须在亚当和弗雷德之间,在辛迪和弗雷德之间。 依此类推……我们可以通过查看一半的集合然后查看该集合的一半来有效地定位 Dan。 最终,我们看O(log_2 N)个个体。
O(N log N)
您可以使用上面的算法找到在桌子上坐下的位置。 如果有大量的人来到餐桌旁,一次一个,并且所有人都这样做,那将花费O(N log N)时间。 事实证明,当必须对任何项目集合进行比较时,对它们进行排序需要多长时间。
最好/最坏情况
你到了派对,需要找到 Inigo - 需要多长时间? 这取决于你什么时候到达。 如果每个人都在你身边,你已经遇到了最坏的情况:这将需要O(N)
时间。 但是,如果每个人都坐在桌旁,则只需O(log N)
时间。 或者,也许你可以利用主人的酒杯喊叫能力,它只需要O(1)
时间。
假设主机不可用,我们可以说 Inigo-finding 算法的下限为O(log N)
和上限为O(N)
,具体取决于您到达时聚会的状态。
空间与通讯
相同的想法可以应用于理解算法如何使用空间或通信。
Knuth 写了一篇关于前者的好论文,题为“歌曲的复杂性” 。
定理 2:存在任意长的复杂度 O(1) 的歌曲。
证明:(由于凯西和阳光乐队)。 考虑由 (15) 定义的歌曲 Sk,但是
V_k = 'That's the way,' U 'I like it, ' U
U = 'uh huh,' 'uh huh'
对于所有 k。
对于有数学头脑的人: 主定理是研究复杂性时另一个有用的知识。
O(n) 是用于编写算法时间复杂度的大 O 表示法。 当您将算法中的执行次数相加时,您将在结果中得到一个表达式,例如 2N+2。 在这个表达式中,N 是主导项(如果其值增加或减少,则对表达式影响最大的项)。 现在 O(N) 是时间复杂度,而 N 是主导项。
For i = 1 to n;
j = 0;
while(j <= n);
j = j + 1;
这里内循环的总执行次数是n+1,外循环的总执行次数是n(n+1)/2,所以整个算法的总执行次数是n+1+n (n+1/2) = (n 2 + 3n)/2。 这里 n^2 是主导项,因此该算法的时间复杂度为 O(n 2 )。
其他答案集中在大 O 符号和实际示例上。 我想通过强调理论观点来回答这个问题。 下面的解释必然缺乏细节; 学习计算复杂性理论的一个极好的来源是 Michael Sipser的《计算理论导论》 。
研究任何计算问题的最广泛模型是图灵机。 图灵机有一个由符号组成的一维磁带,用作存储设备。 它有一个磁头,用于写入和读取磁带。 它有一个确定机器行为的转换表,这是一个固定的硬件组件,在创建机器时决定。 图灵机以离散时间步长工作,执行以下操作:
它读取磁头下的符号。 根据符号及其只能取有限多个值的内部状态,它从其转换表中读取三个值 s、σ 和 X,其中 s 是内部状态,σ 是符号,X 是右或左边。
它将其内部状态更改为 s。
它将读取的符号更改为 σ。
它根据 X 方向将磁头移动一步。
图灵机是强大的计算模型。 他们可以做您的数字计算机可以做的一切。 它们是在现代数字计算机出现之前由理论计算机科学之父和数学家艾伦·图灵引入的。
很难定义单个问题的时间复杂度,比如“白方在国际象棋中有必胜策略吗?” 因为有一台运行一步的机器给出了正确的答案:要么是直接说“否”的机器,要么是直接说“是”的机器。 为了让它起作用,我们改为定义一系列问题的时间复杂度L ,每个问题都有一个大小,通常是问题描述的长度。 然后我们采用图灵机M ,它正确地解决了该系列中的每个问题。 当给M一个大小为n的族的问题时,它会在有限多步内解决它。 让我们称f(n)为M解决大小为n的问题所花费的最长时间。 然后我们说L的时间复杂度是O(f(n)) ,这意味着有一个图灵机最多可以在C.f(n)时间内解决大小为n的实例,其中C是独立的常数的n 。
是的,有些问题可以通过其他计算模型更快地解决。 例如,两个磁带图灵机解决某些问题的速度比使用单个磁带的图灵机快,这就是为什么理论家更喜欢使用稳健的复杂性类,例如 NL、P、NP、PSPACE、EXPTIME。 等等,例如,P 是一类时间复杂度为O(p(n))的决策问题,其中p是多项式。 即使你给你的图灵机增加一万条磁带,或者使用其他类型的理论模型,比如随机存取机,P 类也不会改变。
通常假设整数加法的时间复杂度为O(1)。 这个假设在实践中是有意义的,因为计算机使用固定数量的位数来存储许多应用程序的数字。 理论上没有理由假设这样的事情,所以加法的时间复杂度是 O(k),其中 k 是表示整数所需的位数。
显示问题时间复杂度的直接方法是O(f(n))是构造一个图灵机,它可以在O(f(n))时间内解决它。 为复杂问题创建图灵机并非易事; 人们需要对它们有所了解。 很少给出图灵机的转换表,并且在高层次上进行了描述。 随着人们对机器的熟悉,就会更容易看出机器需要多长时间才能停止。
表明问题不是O(f(n))时间复杂度是另一回事……即使有一些结果,如时间层次定理,这里也有许多未解决的问题。 例如NP问题是否在P中,即多项式时间内可解,是数学界七大千禧年奖问题之一,解决者将获得100万美元的奖金。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.