在Big-O示例部分的第一部分,我们将介绍各种Big-O函数的各种迭代。
让我们从一些简单的例子开始,探索大O是什么。
O(1) Constant
def func_constant(values):
'''
Prints first item in a list of values.
'''
print(values[0])
func_constant([1,2,3])
# 1
请注意,这个函数是恒定的,因为不管列表大小如何,该函数将只采用恒定的步长,在本例中为1,打印列表中的第一个值。因此,我们可以在这里看到,一个100个值的输入列表将只打印1个项目,一个10,000个值的列表将只打印1个项目,一个n个值的列表将只打印1个项目!
O(n) Linear
def func_lin(lst):
'''
Takes in list and prints out all values
'''
for val in lst:
print(val)
func_lin([1,2,3])
'''
1
2
3
'''
这个函数的运行时间为O(n)。这意味着发生的操作数量与n成线性比例,所以我们可以在这里看到,一个100个值的输入列表将打印100次,一个10,000个值的列表将打印10,000次,一个n个值的列表将打印n次。
O(n^2) Quadratic
def func_quad(lst):
'''
Prints pairs for every item in list.
'''
for item_1 in lst:
for item_2 in lst:
print(item_1,item_2)
lst = [0, 1, 2, 3]
func_quad(lst)
'''
0 0
0 1
0 2
0 3
1 0
1 1
1 2
1 3
2 0
2 1
2 2
2 3
3 0
3 1
3 2
3 3
'''
注意我们现在有两个循环,一个嵌套在另一个内部。这意味着对于n个项目的列表,我们必须对列表中的每个项目执行n个操作!这意味着我们总共将执行n次n次赋值,即n^2。
因此,一个包含10个项目的列表将有10^2或100个操作。你可以看到这对于非常大的输入来说是多么危险!这就是为什么大O是如此重要的意识!
Big-O的规模计算
在本节中,我们将讨论不重要的术语是如何从Big-O符号中消失的。
当涉及到大O符号时,我们只关心最重要的项,记住,随着输入量的增加,只有增长最快的项才重要。如果你以前上过微积分课,这会提醒你对无穷大的极限。让我们看看如何删除常量的示例:
def print_once(lst):
'''
Prints all items once
'''
for val in lst:
print(val)
print_once(lst)
'''
0
1
2
3
'''
print_once()函数是O(n),因为它将随着输入线性缩放。下一个例子呢?
def print_3(lst):
'''
Prints all items three times
'''
for val in lst:
print(val)
for val in lst:
print (val)
for val in lst:
print (val)
print_3(lst)
'''
0
1
2
3
0
1
2
3
0
1
2
3
'''
我们可以看到第一个函数将打印O(n)个项,第二个函数将打印O(3n)个项。但是对于n去极小化,常数可以被删除,因为它不会有很大的影响,所以两个函数都是O(n)。
让我们看一个更复杂的例子:
def comp(lst):
'''
This function prints the first item O(1)
Then is prints the first 1/2 of the list O(n/2)
Then prints a string 10 times O(10)
'''
print(lst[0])
midpoint = len(lst) // 2
for val in lst[:midpoint]:
print(val)
for x in range(10):
print('number')
lst = [1,2,3,4,5,6,7,8,9,10]
comp(lst)
'''
1
1
2
3
4
5
number
number
number
number
number
number
number
number
number
number
'''
让我们来分解一下这里的操作。我们可以联合每个操作来得到函数的总Big-O:
我们可以看到,当n变大时,1和10项变得无关紧要,当n走向无穷大时,1/2项乘以n也不会有太大的影响。这个函数是O(n)!
最差情况vs最佳情况
很多时候,我们只关心算法可能出现的最坏情况,但在面试中,重要的是要记住,最坏情况和最好情况可能是完全不同的Big-O时间。例如,考虑以下函数:
def matcher(lst,match):
'''
Given a list lst, return a boolean indicating if match item is in the list
'''
for item in lst:
if item == match:
return True
return False
lst
'''
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
'''
matcher(lst,1)
'''
True
'''
matcher(lst,11)
'''
False
'''
注意,在第一个场景中,最好的情况实际上是O(1),因为匹配是在第一个元素处找到的。在没有匹配的情况下,必须检查每个元素,这导致最坏情况下的时间为O(n)。稍后,我们还将讨论平均案例时间。
最后,让我们引入空间复杂度的概念。
空间复杂度
很多时候,我们也关心算法使用了多少内存/空间。空间复杂度的符号是相同的,但我们不是检查操作的时间,而是检查内存分配的大小。
让我们看几个例子:
def printer(n=10):
'''
Prints "hello world!" n times
'''
for x in range(n):
print('Hello World!')
printer()
'''
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
'''
请注意我们如何只分配’hello world!'变量一次,不是每次打印时。该算法的空间复杂度为O(1),时间复杂度为O(n)。
这是一个复杂度为O(n)的例子:
def create_list(n):
new_list = []
for num in range(n):
new_list.append('new')
return new_list
print(create_list(5))
'''
['new', 'new', 'new', 'new', 'new']
'''
注意new_list对象的大小如何随着输入n而缩放,这表明它是一个关于空间复杂度的O(n)算法。
列表
在Python中,列表充当动态数组,并通过调用它们的方法支持许多常见操作。对列表执行的两个最常见的操作是索引和分配索引位置。这些操作都被设计为在恒定时间O(1)内运行。
让我们想象一下,您想要测试不同的方法来构造一个列表,该列表是[0,1,2… 10000]。让我们继续比较各种方法,比如在列表末尾追加、连接列表,或者使用诸如强制转换和列表理解之类的工具。
例如:
def method1():
l = []
for n in range(10000):
l = l + [n]
def method2():
l = []
for n in range(10000):
l.append(n)
def method3():
l = [n for n in range(10000)]
def method4():
l = list(range(10000))
现在让我们使用timeit魔术函数测试这些方法:
%timeit method1()
%timeit method2()
%timeit method3()
%timeit method4()
'''
184 ms ± 9.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
829 µs ± 125 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
574 µs ± 134 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
194 µs ± 11.8 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
'''
我们可以清楚地看到,最有效的方法是Python中内置的range()函数!
在编写高效的代码时,记住这些因素很重要。更重要的是开始思考如何使用O(1)索引。我们将在讨论数组的一般性时更详细地讨论这一点。现在,请查看下表,了解Big-O效率的概述。
用于常见列表操作的Big-O表
字典
Python中的字典是哈希表的实现。它们使用键和值进行操作,例如:
d = {'k1':1,'k2':2}
d['k1']
# 1
令人惊讶的是,在字典中获取和设置项的时间复杂度是O(1)!哈希表的设计考虑到了效率,我们将在课程的后面更详细地探讨它们,作为最重要的数据结构之一。同时,请参阅下表了解常见字典操作的Big-O效率:
结论
在本节结束时,您应该了解Big-O是如何在算法分析中使用的,并且能够计算出您开发的算法的Big-O。