時間複雜度表示代碼的執行時間隨着數據規模增長的趨勢。使用O()表示
【假設】每一行代碼執行的時間都一樣,都是unit_time。所有代碼的總執行時間 T(n) 與每行代碼的執行次數成正比。
1、只關注循環執行次數最多的一段代碼
def calc(n):
"""
分析這個代碼執行時間,(3+2n)*unit_time,去掉常量,去掉高階的係數,從而得出時間複雜度就是O(n)
"""
total = 0 # 執行1次
for i in range(0, n + 1): # 執行n+1次
total = total + i # 執行n+1次
return total
再舉一例子:
def combination(data: list):
"""
從一個列表中每次取出兩個,找出所有組合方式。
分析這個代碼執行時間,(1+n+2n*n)*unit_time,去掉常量、低階、高階的係數,從而得出時間複雜度就是O(n*n)
思考:前後順序不同的組合算同一個,比如(1,3)和(3,1)算一個組合,該如何寫?
"""
n = len(data) # 執行1次
for x in range(0, n): # 執行n次
for y in range(0, n): # 執行n*n次
print(data[x], data[y]) # 執行n*n次
2、加法法則
總複雜度等於量級最大的那段代碼的複雜度。
def addition_law(n):
"""
加法法則。綜合這三段for循環代碼的時間複雜度,我們取其中最大的量級。所以,整段代碼的時間複雜度就爲 O(n*n)。
:param n:
:return:
"""
sum_1 = 0
sum_2 = 0
sum_3 = 0
# 第一段
for p in range(100):
sum_1 = sum_1 + p # 常量的執行時間,跟 n 的規模無關
# 第二段
for q in range(n):
sum_2 = sum_2 + q # 時間複雜度是O(n)
# 第三段
for i in range(n):
for j in range(n):
sum_3 = sum_3 + i * j # 時間複雜度是O(n*n)
return sum_1 + sum_2 + sum_3
3、乘法法則
嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積。
def multiplication_law(n: int):
"""
乘法法則,嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積。T1(n) * T2(n) = O(n*n) = O(n*n)。
"""
ret = 0
def f(n):
total = 0
for j in range(n):
total = total + j # 執行n次
return total
for i in range(n):
ret = ret + f(i) # 執行n次,調用f(i) n次
return ret
4、對數階複雜度
def geometric_progression(n: int) -> int:
"""
打印等比數列
i從1開始,每次循環乘以2,當大於n時結束循環。i其實是個等比數列,公比是2。循環次數是log2n,時間複雜度就是 O(log2n)。
不管是以 2 爲底、以 3 爲底,還是以 10 爲底,我們把所有對數階的時間複雜度都記爲 O(logn)
"""
i = 1
while i <= n:
i = i * 2
print(i)
5、複雜度由兩個數據的規模來決定
def dependent_on_two_scale(m, n):
"""
從代碼中可以看出,m 和 n 是表示兩個數據規模。我們無法事先評估 m 和 n 誰的量級大,所以我們在表示複雜度的時候,就不能簡單地利用加法法則,省略掉其中一個。
代碼的複雜度由兩個數據的規模來決定m和n。時間複雜度就是 O(m+n)
"""
sum_1 = 0
sum_2 = 0
for i in range(m):
sum_1 = sum_1 + i
for i in range(n):
sum_2 = sum_2 + i
return sum_1 + sum_2
6、遞歸算法時間複雜度分析
遞歸算法是時間複雜度超高的。
def fibonacci(n):
"""
求fibonacci的第n項。
遞歸算法複雜度分析。畫遞歸樹,比如fibonacci(6)如圖。複雜度爲O(2^N)
"""
if n == 0:
return 0
if n in [1, 2]:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
7、優化遞歸算法降低時間複雜度
def fibonacci_plus(n: int) -> int:
"""
斐波那契數列也可以從左到右依次求出每一項的值,那麼通過順序計算求得第N項即可。其時間複雜度爲O(N)。
"""
a, b = 0, 1
if n < 2:
return n
for _ in range(2, n+1):
a, b = b, a + b
return b
8、最好、最差時間複雜度
代碼在不同情況下的不同時間複雜度。最好情況時間複雜度就是,在最理想的情況下,執行這段代碼的時間複雜度。最壞情況時間複雜度就是,在最糟糕的情況下,執行這段代碼的時間複雜度。
def search(data: list, x: int) -> int:
"""
從無序數組中查找某個元素的下標。最好時間複雜度是O(1),最差時間複雜度是 O(n)
要查找的變量 x 可能出現在數組的任意位置。
如果數組中第一個元素正好是要查找的變量 x,那就不需要繼續遍歷剩下的 n-1 個數據了,那時間複雜度就是 O(1)。
但如果數組中不存在變量 x,那我們就需要把整個數組都遍歷一遍,時間複雜度就成了 O(n)。
:param data: 無重複元素無序數組
:param x: 待尋找的元素
:return: x的下標
"""
length = len(data)
for i in range(length):
if data[i] == x:
return i
else:
return -1
9、加權平均時間複雜度
上面介紹了最好和最差的時間複雜度,都是比較極端的情況。那麼平均的時間複雜度是多少呢?
查找變量x在data中的位置,有n+1中可能,處於0~n-1的下標,和不在data中。我們把每種情況下,查找時需要遍歷的元素個數累加起來,然後再除以 n+1,就可以得到需要遍歷的元素個數的平均值。(1+2+3…+n+n)/(n+1),省略掉係數、低階、常量,所以,得到的平均時間複雜度就是 O(n)。
前面的推導過程中存在的最大問題就是,沒有將各種情況發生的概率考慮進去。在數組中與不在數組中的概率都爲 1/2。另外,要查找的數據出現在 0~n-1 這 n 個位置的概率也是一樣的,爲 1/n。
如果我們把每種情況發生的概率也考慮進去,那平均時間複雜度的計算過程就變成了這樣:
用大 O 表示法來表示,去掉係數和常量,這段代碼的加權平均時間複雜度仍然是 O(n)。
10、空間複雜度分析
就看額外分配了多少內存。
def space_complexity(n: int):
"""
第 1 行申請了一個大小爲 n 的 列表,除此之外,剩下的代碼都沒有佔用更多的空間,所以整段代碼的空間複雜度就是 O(n)。
"""
space = [0] * n # 申請了一個大小爲 n 列表
for i in range(n):
space[i] = i * i
print(space)
11、總結
複雜度分析的實用方法:
- 只關注循環執行次數最多的一段代碼;
- 加法法則:總複雜度等於量級最大的那段代碼的複雜度;
- 乘法法則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積
- 複雜度由兩個數據的規模來決定m和n時,時間複雜度是 O(m+n)
- 常見的時間複雜度:O(1)<O(logn)<O(n)<O(nlogn)<O(n*n)
- 空間複雜度分析比較簡單,就看額外分配了多少內存就好。