Python 基於陣列、連結串列實現的包介面
包介面
包介面的以下操作很有用:知道一個包是否為空,用一次操作就清空一個包,判斷一個給定的項是否在包中,以及檢視包中的每一項而不用清空包等等。
使用者的包操作 | Bag類中的方法 |
---|---|
b = <class name>(<optional collection>) | __init__(self,sourceCollection = None) |
b.isEmpty() | isEmpty(self) |
len(b) | __len__(self) |
str(b) | __str__(self) |
item in b | __contains__(self,item); 如果包含了__iter__,就不需要該方法 |
b1 + b2 | __add__(self,other) |
b == anyObject | __eq__(self,other) |
b.clear() | clear(self) |
b.add(item) | add(self,item) |
b.remove(item) | remove(self,item) |
開發一個基於陣列的實現
由於這是一個基於陣列的實現,所以ArrayBag型別的每一個物件,都包含了該包中的一個數組。這個陣列可以是已經實現的Array類的一個例項,也可以是另一個基於陣列的集合(例如Python的list型別)。
Array類
""" File: arrays.py An Array is a restricted list whose clients can use only [], len, iter, and str. To instantiate, use <variable> = array(<capacity>, <optional fill value>) The fill value is None by default. """ class Array(object): """Represents an array.""" def __init__(self, capacity, fillValue = None): """Capacity is the static size of the array. fillValue is placed at each position.""" self._items = list() for count in range(capacity): self._items.append(fillValue) def __len__(self): """-> The capacity of the array.""" return len(self._items) def __str__(self): """-> The string representation of the array.""" return str(self._items) def __iter__(self): """Supports iteration over a view of an array.""" return iter(self._items) def __getitem__(self, index): """Subscript operator for access at index.""" return self._items[index] def __setitem__(self, index, newItem): """Subscript operator for replacement at index.""" self._items[index] = newItem
ArrayBag類
__init__方法負責設定集合的初始狀態。該方法以一個初始的、預設的容量(DEFAULT_CAPACITY)建立了一個數組,並且將這個陣列賦給一個名為self._items的例項變數。由於包的邏輯大小可能和陣列的容量有所不同,每個ArrayBag物件必須在一個單獨的例項變數中記錄其邏輯大小。因此,__init__方法將這個名為self._size的變數初始設定為0。
在初始化了兩個例項變數之後,__init__方法必須處理一種可能發生的情況,即呼叫者提供了源集合引數,我們必須把源集合的所有資料新增到ArrayBag物件中。
from arrays import Array class ArrayBag(object): """An array-base bag implementation.""" # Class variable DEFAULT_CAPACITY = 30 #注意:這裡設定10的話,之後testbench的b+b會導致超出範圍 # Constructor def __init__(self,sourceCollection = None): """Sets the initial state of self, which includes the contents of sourceCollection, if it's present.""" self._items = Array(ArrayBag.DEFAULT_CAPACITY) self._size = 0 if sourceCollection: for item in sourceCollection: self.add(item)
這個介面中最簡單的方法是isEmpty、__len__和clear。如果暫時忽略陣列變滿的問題,add方法也很簡單。
# Accessor methods 訪問方法
def isEmpty(self):
"""Return True if len(self) == 0, or False otherwise."""
return len(self) == 0
def __len__(self):
"""Returns the number of items in self."""
return self._size
# Mutator methods 改變物件屬性方法
def clear(self):
"""Makes self become empty."""
self._size = 0
self._items = Array(ArrayBag.DEFAULT_CAPACITY)
def add(self,item):
"""Adds item to self."""
# check array memory here and increase it if necessary
self._items[len(self)] = item
self._size += 1
迭代器
當Python出現for迴圈被用於一個可迭代的物件的時候,它會執行物件的__iter__方法。根據前面的Array類的__iter__方法,你會發現它只是在底層的列表物件上呼叫iter函式,並且返回結果。然而,這可能會導致一個很嚴重的錯誤,陣列可能並不是填滿的,但是其迭代器會訪問所有的位置,包括那些包含了垃圾值的位置。
為了解決這個問題,新的__iter__方法維護了一個遊標,當遊標達到了包的長度的時候,__iter__方法的while迴圈終止。
def __iter__(self):
"""Supports iteration over a view of self."""
cursor = 0
while cursor < len(self):
yield self._items[cursor]
cursor += 1
以下是使用迭代器的方法
__add__方法將兩個集合連線起來,__str__方法使用map和join操作來構建字串,__eq__方法判斷兩個物件是否相同。這裡的每一個方法,都依賴於包物件是可迭代的這一事實。
def __str__(self):
"""Returns the string representation of self."""
return "{" + ", ".join(map(str,self)) + "}"
def __add__(self,other):
"""Returns a new bag containing the contents of self and other."""
result = ArrayBag(self)
for item in other:
result.add(item)
return result
def __eq__(self,other):
"""Returns True if self equals other, or False otherwise."""
if self is other:
return True
if type(self) != type(other) or len(self) != len(other):
return False
for item in self:
if not item in other:
return False
return True
in運算子和__contains__方法
當Python識別到集合中使用in運算子的時候,它會執行集合類中的__contains__方法。然而,如果這個類的編寫者沒有包含這個方法,Python會自動生成一個預設的方法,這個方法在self上使用for迴圈,針對目標項執行一次簡單的順序搜尋,由於對包的搜尋的平均效能可能不比線性好,所以我們這裡就用__contains__的預設實現。
remove方法
首先檢查先驗條件,如果不滿足就返回一個異常。然後,搜尋底層陣列以查詢目標,最後,將陣列中的項向左移動,以填補刪除的那項留下的空間,將包的大小減小1。如果有必要的話,還要調整陣列的大小(節約空間)。
def remove(self,item):
"""Precondition: item is in self.
Raises: KeyError if item is not in self.
Postcondition: item is removed from self."""
# Check precondition and raise if necessary
if not item in self:
raise KeyError(str(item) + " not in the bag")
# Search for index of target item
targetIndex = 0
for targetItem in self:
if targetItem == item:
break
targetIndex += 1
# Shift items to the left of target up by one position
for i in range(targetIndex,len(self) - 1):
self._items[i] = self._items[i+1]
# Decrement logical size
self._size -= 1
# Check array memory here and decrease it if necessary
開發一個基於連結串列的實現
看一下ArrayBag類,其中isEmpty、__len__、__add__、__eq__和__str__方法都沒有直接訪問陣列變數,這樣在基於連結串列的實現的時候也不需要做出任何修改,如果一個方法沒有訪問陣列變數,它也就不必訪問連結串列結構變數。所以我們應該嘗試將資料結構隱藏在所要實現的物件的方法呼叫中。
Node類
"""
File: node.py
"""
class Node(object):
"""Represents a singly linked node."""
def __init__(self, data, next = None):
self.data = data
self.next = next
LinkedBag類
初始化資料結構
這裡的兩個變數是連結串列結構和邏輯大小,而不是陣列和邏輯大小,為了保持一致性,可以使用和前面相同的變數,然而,self._items現在是外部指標而不是陣列。預設容量的類變數被忽略了。
from node import Node
class LinkedBag(object):
"""An link-based bag implementation."""
# Constructor
def __init__(self,sourceCollection = None):
"""Sets the initial state of self, which includes the contents of sourceCollection, if it's present."""
self._items = None
self._size = 0
if sourceCollection:
for item in sourceCollection:
self.add(item)
迭代器
LinkedBag類和ArrayBag類的__iter__方法都使用了基於遊標的迴圈來生成項,不一樣的是,遊標現在是指向連結串列結構中節點的一個指標,遊標最初設定為外部指標self._items,並且當其變為None的時候停止迴圈。
def __iter__(self):
"""Supports iteration over a view of self."""
cursor = self._items
while not cursor is None:
yield cursor.data
cursor = cursor.next
clear和add方法
ArrayBag中的add方法利用了以常數時間訪問陣列的邏輯末尾項的優點,如果需要的話,在方法執行之後它會調整陣列的大小。LinkedBag中的add方法也是通過將新的項放置在連結串列結構的頭部,從而利用了常數訪問時間的優點。
# Mutator methods
def clear(self):
"""Makes self become empty."""
self._size = 0
self._items = None
def add(self,item):
"""Adds item to self."""
self._items = Node(item,self._items)
self._size += 1
remove方法
類似於ArrayBag中的remove方法,LinkedBag的remove方法必須首先處理先驗條件,然後針對目標項進行順序搜尋,當找到了包含目標項的節點的時候,需要考慮以下兩種情況:
1.它是位於連結串列結構的頭部的節點,在這種情況下,必須將self._items變數重置為這個節點的next連結。
2.這是第一個節點之後的某個節點。在這種情況下,其之前的節點的next連結必須重置為目標項的節點的next連結。
def remove(self,item):
"""Precondition: item is in self.
Raises: KeyError if item is not in self.
Postcondition: item is removed from self."""
# Check precondition and raise if necessary
if not item in self:
raise KeyError(str(item) + " not in the bag")
# Search for index of target item
# probe will point to the target node, and trailer will point to the one before it, if it exists
probe = self._items
trailer = None
for targetItem in self:
if targetItem == item:
break
trailer = probe
probe = probe.next
# unlock the node to be detected, either the first one or the one thereafter
if probe == self._items:
self._items = self._items.next
else:
trailer.next = probe.next
# decrement logical size
self._size -= 1
無需更改的幾種方法
# Accessor methods
def isEmpty(self):
"""Return True if len(self) == 0, or False otherwise."""
return len(self) == 0
def __len__(self):
"""Returns the number of items in self."""
return self._size
def __str__(self):
"""Returns the string representation of self."""
return "{" + ", ".join(map(str,self)) + "}"
def __add__(self,other):
"""Returns a new bag containing the contents of self and other."""
result = LinkedBag(self)
for item in other:
result.add(item)
return result
def __eq__(self,other):
"""Returns True if self equals other, or False otherwise."""
if self is other:
return True
if type(self) != type(other) or len(self) != len(other):
return False
for item in self:
if not item in other:
return False
return True
測試兩個包的實現
from ArrayBag import ArrayBag
from LinkedBag import LinkedBag
def test(bagType):
"""Expects a bag type as an argument and runs some tests on objects of that type."""
lyst = [2013,61,1973]
print("The list of items added is:",lyst)
b1 = bagType(lyst)
print("Expect 3:",len(b1))
print("Expect the bag's string:",b1)
print("Expect True:",2013 in b1)
print("Expect False:",2012 in b1)
print("Expect the items on seperate lines:")
for item in b1:
print(item)
b1.clear()
print("Expect {}:",b1)
b1.add(25)
b1.remove(25)
print("Expect {}:",b1)
b1 = bagType(lyst)
b2 = bagType(b1)
print("Expect True:",b1 == b2)
print("Expect False:",b1 is b2)
print("Expect two of each item:",b1 + b2)
for item in lyst:
b1.remove(item)
print("Expect {}:",b1)
print("Expect crash with KeyError:")
b2.remove(99)
if __name__ == '__main__':
#test(ArrayBag)
test(LinkedBag)
注意:在測試程式中,你可以對任何的包型別執行相同的方法(那些位於包介面中的方法)。這是介面的要旨所在:儘管實現可以變化,但是它保持不變。
兩個包執行時效能
這兩個包的實現上的操作的執行時間是十分相似的。
in和remove兩個操作在實現上都需要線性時間,因為它們都加入了一個順序搜尋。ArrayBag上的remove操作,還必須完成在陣列中移動資料項的額外工作,但是總的效果不會比線性階還差。+、str和iter操作是線性的,==操作的執行時間有幾種不同的情況,剩下的操作都是常數時間的(其中ArrayBag的add偶爾因為調整陣列大小而達到了線性時間水平)。
兩個實現都有預期的記憶體權衡
當ArrayBag中的陣列要好於填滿一半的情況的時候,它使用的記憶體比相同邏輯大小的LinkedBag所使用的記憶體要小。相比於LinkedBag上對應的操作,ArrayBag的新增操作通常要更快一點,但是刪除操作則會慢一些。
本篇文章來自於:資料結構(Python語言描述)這本書的第5章.介面、實現和多型。