1. 程式人生 > >Python 資料結構筆記(1):Python資料結構的效能

Python 資料結構筆記(1):Python資料結構的效能

本系列部落格是閱讀《Problem Solving with Algorithms and Data Structures using Python》的筆記,原文連結

1、列表 List

索引和賦值是兩個非常常用的操作。這個兩個操作不論列表多長,它們的時間複雜度都是 O(1)O(1)。另一個非常常用的程式操作是去擴充一個列表。這有兩種方式去生成一個更長的列表。我們可以用 append 操作或者 + 串聯運算子。這個 append 操作的時間複雜度也是 O(1)O(1)。然而,+ 串聯運算子是 O(k)O(k),這裡 kk 是指正在被連線的列表的大小。瞭解它對我們非常重要,選擇正確工具可以使我們的程式更加有效。

讓我們來看一下四種不同的方法來生成從 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

中附上一個名叫 number 的引數,這樣我們就可以指定程式被執行的次數。 下面將展示對每一個程式執行 1000 次所需要花費的時間

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] 會返回新的列表,它的執行速度是最慢的。下表展示了所有列表基本操作的大 OO 效率。在你對下表進行仔細思考後,可能會對 pop 操作的兩個不同的時間複雜度感到疑惑。當 pop 操作每次從列表的最後一位刪除元素時複雜度為 O(1)O(1),而將列表的第一個元素或中間任意一個位置的元素刪除時,複雜度則為 O(n)O(n)。這樣迥然不同的結果是由 Python 對列表的執行方式造成的。在 Python 的執行過程中,當從列表的第一位刪除一個元素,其後的每一位元素都將向前挪動一位。你可能覺得這種操作很愚蠢,但你會發現這種執行方式會讓 index 索引操作的複雜度降為 O(1)O(1),顯然索引更常在程式中被使用。

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)來訪問條目,而不是通過一個座標。字典條目的訪問和賦值都是 O(1)O(1) 的時間複雜度。字典的另一個重要的操作是所謂的“包含”。檢查一個鍵是否存在於字典中也只需 O(1)O(1) 的時間。其他字典操作的時間效率都已經在下表中列出。需要注意的是,這裡所列出的都是平均時間複雜度。在一些罕見的情況下,包含、訪問和賦值都可能退化為 O(n)O(n)

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 操作是 O(n)O(n),而字典的是 O(1)O(1)。這個實驗很簡單,我們將生成一個自然數(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 操作所需要的時間是線性增長的,這證實了我們之前關於其時間複雜 度是 O(n)O(n) 的論斷。與此同時,字典的 in 操作用時保持不變,即使字典的大小不斷變大也是如此。

在這裡插入圖片描述在這裡插入圖片描述