Python 資料結構筆記(1):Python資料結構的效能
本系列部落格是閱讀《Problem Solving with Algorithms and Data Structures using Python》的筆記,原文連結
1、列表 List
索引和賦值是兩個非常常用的操作。這個兩個操作不論列表多長,它們的時間複雜度都是 。另一個非常常用的程式操作是去擴充一個列表。這有兩種方式去生成一個更長的列表。我們可以用 append 操作或者 + 串聯運算子。這個 append 操作的時間複雜度也是 。然而,+ 串聯運算子是 ,這裡 是指正在被連線的列表的大小。瞭解它對我們非常重要,選擇正確工具可以使我們的程式更加有效。
讓我們來看一下四種不同的方法來生成從 0 到 n 的列表。首先,
- 我們嘗試用 for 迴圈體通過串聯生成一個列表,
- 然後我們可以用“ append”代替串聯操作。
- 接下來, 我們可以使用列表解析來生成一個列表。
- 最後,也許是最明顯的方法,通過列表結構體的訪問來使用 range 的功能。
下面展示了生成列表四種方法的程式碼:
def create_list_1_1():
a_list = []
for i in range(1000):
a_list = a_list + [i]
def create_list_1_2():
a_list = []
for i in range(1000):
a_list += [i]
def create_list_2():
a_list = []
for i in range(1000):
a_list.append(i)
def create_list_3():
a_list = [i for i in range(1000)]
def create_list_4():
a_list = list(range(1000))
為了計時,我們需要 timeit 模組的 Timer 計時器, Timer 預設執行次數是一百萬次。執行結束後,它將以浮點數的形式返回執行的總時間(單位:秒)。當你執行程式一次時,它返回的結果是以微秒為單位的。我們也可以在 timeit
from timeit import Timer
t1 = Timer('create_list_1_1()', 'from __main__ import create_list_1_1')
print("concat 1 ",t1.timeit(number=1000), "milliseconds")
t1 = Timer('create_list_1_2()', 'from __main__ import create_list_1_2')
print("concat 2 ",t1.timeit(number=1000), "milliseconds")
t1 = Timer('create_list_2()', 'from __main__ import create_list_2')
print("append ",t1.timeit(number=1000), "milliseconds")
t1 = Timer('create_list_3()', 'from __main__ import create_list_3')
print("comprehension ",t1.timeit(number=1000), "milliseconds")
t1 = Timer('create_list_4()', 'from __main__ import create_list_4')
print("list range ",t1.timeit(number=1000), "milliseconds")
執行結果如下: concat 1 1.3361934975348504 milliseconds concat 2 0.07323397665902576 milliseconds append 0.07901547132075848 milliseconds comprehension 0.0323528873949499 milliseconds list range 0.012585064675363355 milliseconds
我們可以看到,類表推導式確實是運算速度最快的方法。由於 a_list = a_list + [i] 會返回新的列表,它的執行速度是最慢的。下表展示了所有列表基本操作的大 效率。在你對下表進行仔細思考後,可能會對 pop 操作的兩個不同的時間複雜度感到疑惑。當 pop 操作每次從列表的最後一位刪除元素時複雜度為 ,而將列表的第一個元素或中間任意一個位置的元素刪除時,複雜度則為 。這樣迥然不同的結果是由 Python 對列表的執行方式造成的。在 Python 的執行過程中,當從列表的第一位刪除一個元素,其後的每一位元素都將向前挪動一位。你可能覺得這種操作很愚蠢,但你會發現這種執行方式會讓 index 索引操作的複雜度降為 ,顯然索引更常在程式中被使用。
Operation | Big-O Efficiency |
---|---|
index [] | O(1) |
index assignment | O(1) |
append | O(1) |
pop() | O(1) |
pop(i) | O(n) |
insert(i,item) | O(n) |
del operator | O(n) |
iteration | O(n) |
contains (in) | O(n) |
get slice [x:y] | O(k) |
del slice | O(n) |
set slice | O(n+k) |
reverse | O(n) |
concatenate | O(k) |
sort | O(n log n) |
multiply | O(nk) |
2、Dict 字典
Python 中第二個主要的資料結構是字典。回想一下,字典與列表的不同之處在於你需要通過一個鍵(key)來訪問條目,而不是通過一個座標。字典條目的訪問和賦值都是 的時間複雜度。字典的另一個重要的操作是所謂的“包含”。檢查一個鍵是否存在於字典中也只需 的時間。其他字典操作的時間效率都已經在下表中列出。需要注意的是,這裡所列出的都是平均時間複雜度。在一些罕見的情況下,包含、訪問和賦值都可能退化為
operation | Big-O Efficiency |
---|---|
copy | O(n) |
get item | O(1) |
set item | O(1) |
delete item | O(1) |
contains (in) | O(1) |
iteration | O(n) |
下一個效能實驗將會對比列表和字典的包含操作的效率。在這一過程中我們將會驗證列表的 in 操作是 ,而字典的是 。這個實驗很簡單,我們將生成一個自然數(range)的列表,然後隨機地選取一個數字,檢查其是否在列表中。如果我們之前的效率表是正確的話,列表越大,所用的時間也就越長。然後我們將在一個以數字為鍵的字典上重複這個實驗。這次我們會發現檢查一個數字是否在字典中要快得多,而且即使字典變大,查詢所用的時間也保持不變。
import timeit
import random
nums = []
lst_times = []
d_times = []
for i in range(10000,1000001,20000):
t = timeit.Timer("random.randrange(%d) in x"%i,
"from __main__ import random,x")
x = list(range(i))
lst_time = t.timeit(number=1000)
x = {j:None for j in range(i)}
d_time = t.timeit(number=1000)
nums.append(i)
lst_times.append(lst_time)
d_times.append(d_time)
print("%d,%10.3f,%10.3f" % (i, lst_time, d_time))
執行結果對比:隨著列表的增大,其 in 操作所需要的時間是線性增長的,這證實了我們之前關於其時間複雜 度是 的論斷。與此同時,字典的 in 操作用時保持不變,即使字典的大小不斷變大也是如此。