1. 程式人生 > >Python 基於陣列、連結串列實現的包介面

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章.介面、實現和多型。