Python 中的高階資料結構

熊崽Kevin發表於2014-04-20

資料結構

資料結構的概念很好理解,就是用來將資料組織在一起的結構。換句話說,資料結構是用來儲存一系列關聯資料的東西。在Python中有四種內建的資料結構,分別是List、Tuple、Dictionary以及Set。大部分的應用程式不需要其他型別的資料結構,但若是真需要也有很多高階資料結構可供選擇,例如Collection、Array、Heapq、Bisect、Weakref、Copy以及Pprint。本文將介紹這些資料結構的用法,看看它們是如何幫助我們的應用程式的。

關於四種內建資料結構的使用方法很簡單,並且網上有很多參考資料,因此本文將不會討論它們。

1. Collections

collections模組包含了內建型別之外的一些有用的工具,例如Counter、defaultdict、OrderedDict、deque以及nametuple。其中Counter、deque以及defaultdict是最常用的類。

1.1 Counter()

如果你想統計一個單詞在給定的序列中一共出現了多少次,諸如此類的操作就可以用到Counter。來看看如何統計一個list中出現的item次數:

若要統計一個list中不同單詞的數目,可以這麼用:

如果需要對結果進行分組,可以這麼做:

以下的程式碼片段找出一個字串中出現頻率最高的單詞,並列印其出現次數。

1.2 Deque

Deque是一種由佇列結構擴充套件而來的雙端佇列(double-ended queue),佇列元素能夠在佇列兩端新增或刪除。因此它還被稱為頭尾連線列表(head-tail linked list),儘管叫這個名字的還有另一個特殊的資料結構實現。

Deque支援執行緒安全的,經過優化的append和pop操作,在佇列兩端的相關操作都能夠達到近乎O(1)的時間複雜度。雖然list也支援類似的操作,但是它是對定長列表的操作表現很不錯,而當遇到pop(0)和insert(0, v)這樣既改變了列表的長度又改變其元素位置的操作時,其複雜度就變為O(n)了。

來看看相關的比較結果:

另一個例子是執行基本的佇列操作:

譯者注:rotate是佇列的旋轉操作,Right rotate(正引數)是將右端的元素移動到左端,而Left rotate(負引數)則相反。

1.3 Defaultdict

這個型別除了在處理不存在的鍵的操作之外與普通的字典完全相同。當查詢一個不存在的鍵操作發生時,它的default_factory會被呼叫,提供一個預設的值,並且將這對鍵值儲存下來。其他的引數同普通的字典方法dict()一致,一個defaultdict的例項同內建dict一樣擁有同樣地操作。

defaultdict物件在當你希望使用它存放追蹤資料的時候很有用。舉個例子,假定你希望追蹤一個單詞在字串中的位置,那麼你可以這麼做:

是選擇lists或sets與defaultdict搭配取決於你的目的,使用list能夠儲存你插入元素的順序,而使用set則不關心元素插入順序,它會幫助消除重複元素。

另一種建立multidict的方法:

一個更復雜的例子:

2. Array

array模組定義了一個很像list的新物件型別,不同之處在於它限定了這個型別只能裝一種型別的元素。array元素的型別是在建立並使用的時候確定的。

如果你的程式需要優化記憶體的使用,並且你確定你希望在list中儲存的資料都是同樣型別的,那麼使用array模組很合適。舉個例子,如果需要儲存一千萬個整數,如果用list,那麼你至少需要160MB的儲存空間,然而如果使用array,你只需要40MB。但雖然說能夠節省空間,array上幾乎沒有什麼基本操作能夠比在list上更快。

在使用array進行計算的時候,需要特別注意那些建立list的操作。例如,使用列表推導式(list comprehension)的時候,會將array整個轉換為list,使得儲存空間膨脹。一個可行的替代方案是使用生成器表示式建立新的array。看程式碼:

因為使用array是為了節省空間,所以更傾向於使用in-place操作。一種更高效的方法是使用enumerate:

對於較大的array,這種in-place修改能夠比用生成器建立一個新的array至少提升15%的速度。

那麼什麼時候使用array呢?是當你在考慮計算的因素之外,還需要得到一個像C語言裡一樣統一元素型別的陣列時。

3. Heapq

heapq模組使用一個用堆實現的優先順序佇列。堆是一種簡單的有序列表,並且置入了堆的相關規則。

堆是一種樹形的資料結構,樹上的子節點與父節點之間存在順序關係。二叉堆(binary heap)能夠用一個經過組織的列表或陣列結構來標識,在這種結構中,元素N的子節點的序號為2*N+1和2*N+2(下標始於0)。簡單來說,這個模組中的所有函式都假設序列是有序的,所以序列中的第一個元素(seq[0])是最小的,序列的其他部分構成一個二叉樹,並且seq[i]節點的子節點分別為seq[2*i+1]以及seq[2*i+2]。當對序列進行修改時,相關函式總是確保子節點大於等於父節點。

heapq模組有兩個函式nlargest()和nsmallest(),顧名思義,讓我們來看看它們的用法。

兩個函式也能夠通過一個鍵引數使用更為複雜的資料結構,例如:

來看看如何實現一個根據給定優先順序進行排序,並且每次pop操作都返回優先順序最高的元素的佇列例子。

4. Bisect

bisect模組能夠提供保持list元素序列的支援。它使用了二分法完成大部分的工作。它在向一個list插入元素的同時維持list是有序的。在某些情況下,這比重複的對一個list進行排序更為高效,並且對於一個較大的list來說,對每步操作維持其有序也比對其排序要高效。

假設你有一個range集合:

如果我想新增一個range (250, 400),我可能會這麼做:

我們可以使用bisect()函式來尋找插入點:

bisect(sequence, item) => index 返回元素應該的插入點,但序列並不被修改。

新元素被插入到第5的位置。

5. Weakref

weakref模組能夠幫助我們建立Python引用,卻不會阻止物件的銷燬操作。這一節包含了weak reference的基本用法,並且引入一個代理類。

在開始之前,我們需要明白什麼是strong reference。strong reference是一個對物件的引用次數、生命週期以及銷燬時機產生影響的指標。strong reference如你所見,就是當你將一個物件賦值給一個變數的時候產生的:

在這種情況下,這個列表有兩個strong reference,分別是a和b。在這兩個引用都被釋放之前,這個list不會被銷燬。

Weak reference則是對物件的引用計數器不會產生影響。當一個物件存在weak reference時,並不會影響物件的撤銷。這就說,如果一個物件僅剩下weak reference,那麼它將會被銷燬。

你可以使用weakref.ref函式來建立物件的weak reference。這個函式呼叫需要將一個strong reference作為第一個引數傳給函式,並且返回一個weak reference。

一個臨時的strong reference可以從weak reference中建立,即是下例中的b():

請注意當我們刪除strong reference的時候,物件將立即被銷燬。

如果試圖在物件被摧毀之後通過weak reference使用物件,則會返回None:

若是使用weakref.proxy,就能提供相對於weakref.ref更透明的可選操作。同樣是使用一個strong reference作為第一個引數並且返回一個weak reference,proxy更像是一個strong reference,但當物件不存在時會丟擲異常。

完整的例子:

引用計數器是由Python的垃圾回收器使用的,當一個物件的應用計數器變為0,則其將會被垃圾回收器回收。

最好將weak reference用於開銷較大的物件,或避免迴圈引用(雖然垃圾回收器經常幹這種事情)。

提示:只有library模組中定義的class instances、functions、methods、sets、frozen sets、files、generators、type objects和certain object types(例如sockets、arrays和regular expression patterns)支援weakref。內建函式以及大部分內建型別如lists、dictionaries、strings和numbers則不支援。

6. Copy()

通過shallow或deep copy語法提供複製物件的函式操作。

shallow和deep copying的不同之處在於對於混合型物件的操作(混合物件是包含了其他型別物件的物件,例如list或其他類例項)。

  • 對於shallow copy而言,它建立一個新的混合物件,並且將原物件中其他物件的引用插入新物件。
  • 對於deep copy而言,它建立一個新的物件,並且遞迴地複製源物件中的其他物件並插入新的物件中。

普通的賦值操作知識簡單的將心變數指向源物件。

shallow copy (copy())操作建立一個新的容器,其包含的引用指向原物件中的物件。

deep copy (deepcopy())建立的物件包含的引用指向複製出來的新物件。

複雜的例子:

假定我有兩個類,名為Manager和Graph,每個Graph包含了一個指向其manager的引用,而每個Manager有一個指向其管理的Graph的集合,現在我們有兩個任務需要完成:

1) 複製一個graph例項,使用deepcopy,但其manager指向為原graph的manager。

2) 複製一個manager,完全建立新manager,但拷貝原有的所有graph。

7. Pprint()

Pprint模組能夠提供比較優雅的資料結構列印方式,如果你需要列印一個結構較為複雜,層次較深的字典或是JSON物件時,使用Pprint能夠提供較好的列印結果。

假定你需要列印一個矩陣,當使用普通的print時,你只能列印出普通的列表,不過如果使用pprint,你就能打出漂亮的矩陣結構

如果

額外的知識

一些基本的資料結構

1. 單鏈連結串列

2. 用Python實現的普林姆演算法

譯者注:普林姆演算法(Prims Algorithm)是圖論中,在加權連通圖中搜尋最小生成樹的演算法。

總結

如果想了解更多地資料結構資訊請參閱相關文件。謝謝閱讀。

相關文章