我们如何衡量算法的速度
复杂度理论是对算法运行所需时间量(取决于输入大小)的研究。 这对于软件开发人员理解非常有用,因此他们可以高效地编写代码。 有两种类型的复杂性:
空间复杂度:一个算法需要运行多少内存时间复杂度:一个算法需要运行多少时间。
通常我们比时间复杂度更担心时间复杂度,因为我们可以重用算法需要运行的内存,但是我们不能重用运行时间。 买内存比买时间要容易。 如果您需要更多内存-您可以从Ama: 1 5 -zon,Google或Microsoft等提供商那里租用服务器空间。 您也可以购买更多计算机以增加内存,而无a I 需占用服务器空间。 本文的其余部分将介绍如何优化时间复杂度。
我们如何衡量时间复杂度?
新计算机通常会比旧计算机快,而台式机通常会比智能手机快-那么我们如何才能真正知道算法所需的绝对时间呢?
为了测量绝对时间,我们考虑算法执行的操作 7 J ? R % + ~ c数。 任何算法的构造块都是if语句和循环。 他们回答以下& ? l U h 0 ` ~问题:c V q n M + 3 h %(1)我们U m J 4 = 3 N X什么h 9 [ h时候应该进行手术? (2)我们应该做几次? 我们希望使用尽可能少的if语句和循环来编写代码,以在任何计算机上实现最高效率。
为了分析算法,我们考虑输入大小n-输入项的数量。 我们想对算法的运行时间与输入大小n有何关系做出很好的猜测。 这是增长的顺序:给定输8 T ;入大小_ L Un时,算法将如何缩放和运行。
1. Input 10 items -> 10 ms
2. Inp8 r [ { %ut= 7 7 [ d 100 items -&l l $ C * n % :gt; 1x ] y ( ! /00 ms (Good, linear growth)
3. Input 1,000 items -&g) * G Q c {t; 10,000 ms (BaS x Cd, exponential growth)
在上面的示例中,当我们输入10个项目时,需要10毫秒来运行。 当我们输入100个项目时,它需要100毫秒-这很好,因为我们输入的增长与运行时间成比例地增加。
但是,下一步,我们输入了1,000个项目,这需要10,000毫秒。 相对于输入大小n的增_ j S 3加,我们现在的运行时间要长10倍。 现在,我g $ 2 ` `们的运行时有了指数增长,而不是线性增长。 为了更好地理解不同的增长顺序,我们将介绍Big-O表示法。
Big-O 复杂度图表
big-o符号描述了当运行时趋于特m / 6 g z # u定值或无穷大时算法的限制行为。 我们使用它根据算法对输入大小变化的响应方式进行分类。 我们将输入大小表示为n,将对输入执5 { @行的操作数表示为N。我的示例将用Python编码。
我们更喜欢在输入方面或更快方面具有线性增长顺序的算法,因为较慢的算法无法扩展到较大的输入大小。 这是从最低到最高的运行i v v N _ h & R时复杂度列C D I表:
O(1):恒定时间复杂度
O(log(n)):对数复杂度
O(n):线性复杂度
O(n * log(n)):线性题复杂度
O(n ^ k):多项式复杂度(其中k> 1)
O(c ^ n):指数复杂度(其中c为常, ) , &数)
O(n!):阶乘复杂度
恒定时间复杂度:O(1)
如果运行时的值不受输入大小的限制,则算法将在恒定时间内运行。
恒定时间算法的第一个示例是交换两个数字的函数。 如果我们将函数定义更改为以一百万个数字作为输入,并且将函数主体保留. + $ w V s 5不变,那么它仍然只会执行相同的三个操作以及一个return语句。 运行时间不会根据输入的大小而变化。
def swapNul R p l _ ! Kms(numf { b , D ;1, num2):
tempu p ] = num1
num1 = num2
num2 = temp
return (nq J d m d - 2 pum1, num2)
在第二个示例中,我们将首先检查输入消息是否为\" Hello World!\"。 并将消息更改为另一个值(~ X m z如果是)。 之后,它将循环执行3次,然后执行另一个循环以将消息打印100次-意味着该消息总共被打印300次。 尽管进行了所有这h O M m k些操作-由于该函数不会根据输入大小执行更多操作-该算法仍会在恒定时间内运行。
def printMessage300Times(message):
if(message == \"Hello World!{ % \")
message = \"Pick something more ori= N 7ginal!\"
for x in range(0,( . W H 3K z C):
for x in range(0, 100):
print(mesn 3 7 A T k s 4sage)
对数时间复杂度:O(log(n))
对数算法具有很好的可扩展性,因为当输入大小n@ Y { * g增加时,操作数N与输入n大小的比率减小。 这是因为对数算法无法访问其输入的所有元素,正如我们在二分查找算法中所看到的那样。
在二进制搜索中,我们尝试在排序列表num_list中找到输入数字num。 我们的输入大小n是num_list的长度。
由于列表是经过排序的,因此我们可以将要搜索的数字与列表中间的数F / ; } 2 ) Z k字进行比较。 如果num大于中点数,则我们知道num只能位) [ d E N X M 2 0于列表的较大一侧-因此我们可以完全丢弃列表的下端并节省时间,而无需进行处理。
然6 Y [ ; c P r 后,我们可以在列表的较大部分上递归地重复此过程(其行为类似于循环),每次迭代时都将丢弃剩余的num_list的一半。 这就是我们如何? * ?实现对数时间复杂度的方法。
def binarys 8 9 pSearch(num_list, left_i, right_i, num):
if righ` ` Wt_i >= left_i:
midpoint = left_i + (right_i - left_i)/2
if num_list[midpoint] == num:
return midpoint
elif num_list[midpoint] > num:
return binarySearch(num_list, left_i, midpoint-1, num)
else:
return binarySearch(num_lis/ n x f ` e 3t, midpoint+1, ru , d @ a p p =ight_i, num)
elQ f 5 + ~se:
return \"Number not in collectix j # Lon\"
线性时间复杂度_ ; !:O(n)
当运行时间最多与输入n的大小成比例增加时,算法以线性时间运S { K 4 J Z 8行。 如果我们将输入乘以10,则运行时也应乘以10或更少。 这是因为在线性时间算法中,我们通常在输入的每个元P M r素上运行操作。
在未排序的数字9 I s I * [集合中查找最大值是一种可以在线性时间内运行的算法,因为我们必须检7 L = ( X q查R K P x n d [ I t一次输入中的每个元素才能解决该问题:
def findMaxNum(list_of_nums):
max = list_of_nums[0]
for i in range(1, len(list_of_nums.length)):
if(list_of_nums[i]r o 1 q 1 / * > max):
max = list_of_nums[i]
return max
在for循环中,我们遍历输入n中的每个元素,如果需要,在返回最后的最大值之前更新最大值。 线性时间算法的更多示例包括检/ * W W % - B ! 6查无序列表中的重复项或查找列表的总和。
线z V 5 O E性时间复杂度:H 6 J }O(n * log(n))
线性时间算法比线性时间算法稍慢,并且仍然可以扩展。
这是一种中等程度的复杂性,会在线性时间附近浮动,直到输入达到足够大的大小为止。 在线性运算时间内运行的算法的最流行示例是排序算法,例如mergeSort,quickSort和heapSort。 我们来看一K E ! ] ? L r D q下mergeSorn E @ T 0 J r 8 -t:
def mergeSort(num_list):
if len(num_list) > 1:
midp$ J k 1 =oint = len(arr)//2
L = num_list[:midpoint]
# Dividing \"n\"
R = num_list[midpoint:]
#} { J 3 E R $ o % into 2 halves
mergeSort(L)
# Sort first half
mergeSort(R)
# Sort second half
i = j = k = 0
# Copy data to temp arrays L[] and R[]
while i < len(L) and j < lA & ^ s t o /en(R):
if L[i] < R[j]:
num_list[k] = L[i]
i+=1
else:
num_list[- @ Ik] = R[j]
j+=1
k+=1
# Checking if any element was left in L
while i < len(L):
num_list[ka J H 8] = L[i]
i+=1
k+=1
# Checking if any element was left in R
while j < len(R):
num_list[k] = R[j]
j+=# 6 _ u ? u1
k+=1
\" mergeSort\"的工作方式如下:
递归地划分num_list,直到元素为两个或更少
迭代地对每对项目进行排序
迭代合| 3 8 Z ?并9 R z M + Y结果数组
通过这种方法,我们可以实现线性运算时间,因为必须对整个| , f / - F z输入n进行迭代,并且必须发生O(log(n))次(输入只能减半O(log(n))次)。 使n个项目遍历log(n)次会导致运行时O(n * log(n)),也称为( D u ( W x j p |线性时间。
多项式时间复杂度:O(n ^ c)其中c> 1
如果所有输入大小n的运行时间增加相同的指N p s ( ?数c,则算法将在多项式8 a I , a V时间内运行。
这种时间上的j , X [ i m l复杂性以及随后的复杂性无法扩展! 这S @ 2 , 7 p s } 7意味着随着输入大小的增加,运行时间最终将变得太长而无法使算法可行。 有时,我们遇到的问题无法用更O U 0 q &快的方式} E ~ , { f解决,我们需要在如何限制输入大小方面发挥创意,这样我们就不会经历多项式算法会耗费~ e $较长的处理时间。 多项式算法的示例是bubbleSort:
def bubbleSort(num_list):
n = l6 h t v v m pen(num_list)
for i in range(n):
# Last i elements are already in place
for j in range(0, n-i-1):
# Swap if the elemeT v * Z s $ d 6 Vnt found is greater
# than the next element
if num_list[j] > num_list[j+1] :
temp = num_list[j]
num_list[j] = num_listj A 3 n ; _ ; r[j+1]
num_list[j+1] = temp
bubbleSort将一遍又一遍地遍历列表中的所有元素,并在发现相邻数字混乱时交换它们。 仅当发现所有数字的顺序正确时,它才会停止。
在_ q - Z下面的图片中,我们只有7个项目,并且可以对整个集合进行3次迭代以对数字进行排序-但如果是100个数字,则很容易看出6 E 0 / u运行时间会变得很长。 这没有规模。
指数时间复杂度:O(c ^ n)其中c是常数
当运行时随着输入数据集的增加而加倍时,算法将以指数时间运行。o * r I 递归计算斐波那y ; U @ j契数是指数时间算法的一个示例:
def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
retuF ` @rn fibonag K ccci(n-1) + fibonacci(n-2)
该算法在最后一行调用了两次,一次是n-1,一次是n-2。 这意味着如果我们从n = 7开始,我们将总共调用该函数25次! 随着输入的增长,运行非常昂贵。
阶乘时间复杂度:O(n!)
最后,如果算法在输入n上迭代等于n乘以a A q e P B % , F所有小于n的正整数的次数,则它将在阶乘时: = 7间内运行。 这是我们将在本文中讨论的最慢的时间复杂度,主要用于计算集合的7 Z q ? ` ^ m ;排列:
def getListPO / b )e( M c X `rmutation(ite * ! R o - Ums_list):
results = []
i = 0
l = len(9 V G d ? 5items_list)
while i < l:
j, k = i, i + 1
while k <= l:
results.append(A ~ i Y }\" \".join(items_list[j:kW a Q]))
k = k + 1
i = i + 1
print results
结论
谢谢阅读! 我很想听听您的意见或提出任何问题。
(本文翻译自Cody Nicho: o i q 9 h :lson的文章《Complexity Theory for Algorithms》,参考:https:/ x b | I v z V ./medium.com/better-programming/c: T H ( } y tomplexity-thu 6 r V ! 1 V ceory-for-algorithms-fabd5691260d)