1. 程式人生 > >用最複雜的方式學會陣列(Python實現動態陣列)

用最複雜的方式學會陣列(Python實現動態陣列)

Python序列型別

在本部落格中,我們將學習探討Python的各種“序列”類,內建的三大常用資料結構——列表類(list)、元組類(tuple)和字串類(str)。

不知道你發現沒有,這些類都有一個很明顯的共性,都可以用來儲存多個數據元素,最主要的功能是:每個類都支援下標(索引)訪問該序列的元素,比如使用語法 Seq[i]。其實上面每個類都是使用 陣列 這種簡單的資料結構表示。

但是熟悉Python的讀者可能知道這3種資料結構又有一些不同:比如元組和字串是不能修改的,列表可以修改。

計算機記憶體中的陣列結構

計算機體系結構中,我們知道計算機主存由位資訊組成,這些位通常被歸類成更大的單元,這些單元則取決於精準的系統架構。一個典型的單元就是一個位元組,相當於8位。

計算機系統擁有龐大數量的儲存位元組,那麼如何才能找到我們的資訊存在哪個位元組呢?答案就是大家平時熟知的 儲存地址 。基於儲存地址,主存中的任何位元組都能被有效的訪問。實際上,每個儲存位元組都和一個作為其地址的唯一二進位制數字相關聯。如下圖中,每個位元組均被指定了儲存地址:

一般來說,程式語言記錄識別符號和其關聯值所儲存的地址之間的關係。比如,當我們宣告識別符號 \(x\) 就有可能和儲存器中的某一值相關聯,而識別符號 \(y\)就可能和其他的值相關聯。一組相關的變數能夠一個接一個地儲存在計算機儲存器的一塊連續區域內。我們將這種方式稱為 陣列。

我們來看Python中的例子,一個文字字串 HELLO

是以一列有序字元的形式儲存的,假定該字串的每個Unicode字元需要兩個位元組的儲存空間。最下面的數字就是該字串的索引值。

我們可以看到,陣列可以儲存多個值而無需構造具有特定索引的多個變數來指定其中的每個專案,並且幾乎在所有程式語言(例如C、Java、C#、C++)中使用,但是Python更具有優勢。Python在構建列表時,熟悉的讀者可能知道,不需要預先定義陣列或列表的大小,相反,在Python中,列表具有動態性質,我們可以不斷的往列表中新增我們想要的資料元素。接下來,讓我們看看Python列表的知識(已經熟悉的讀者可以快速瀏覽或者跳過)。

Python列表

Python列表的操作

  • 建立列表的語法格式:

[ele1, ele2, ele3, ele4, ...]

  • 建立元組的語法格式:

(ele1, ele2, ele3, ele4, ...)

元組比列表的記憶體空間利用率更高,因為元組是固定不變的,所以沒有必要建立擁有剩餘空間的動態陣列。

我們先在Python的IDE中建立一個列表,然後大致瞭解一下列表部分內建操作,我們先建立了一個名為test_list的列表,然後修改(插入或刪除)元素,反轉或清空列表,具體如下:

>>> test_list = []  # 建立名為test_list的空列表
>>> test_list.append("Hello")
>>> test_list.append("World")
>>> test_list
['Hello', 'World']
>>> test_list = ["Hello", "Array", 2019, "easy learning", "DataStructure"]  # 重新給test_list賦值
>>> len(test_list)  # 求列表的長度
5
>>> test_list[2] = 1024 # 修改列表元素
>>> test_list
['Hello', 'Array', 1024, 'easy learning', 'DataStructure']
>>>
>>> test_list.insert(1, "I love")   # 向列表中指定位置中插入一個元素
>>> test_list
['Hello', 'I love', 'Array', 1024, 'easy learning', 'DataStructure']
>>> test_list.append(2020)  # 向列表末尾增加一個元素
>>> test_list
['Hello', 'I love', 'Array', 1024, 'easy learning', 'DataStructure', 2020]
>>>
>>> test_list.pop(1)    # 刪除指定位置的元素
'I love'
>>> test_list.remove(2020)  # 刪除指定元素
>>> 
>>> test_list.index('Hello')    # 查詢某個元素的索引值
0
>>> test_list.index('hello')    # 如果查詢某個元素不在列表中,返回ValueError錯誤
Traceback (most recent call last):
  File "<pyshell#11>", line 1, in <module>
    test_list.index('hello')
ValueError: 'hello' is not in list
>>> 
>>> test_list.reverse() # 反轉整個列表
>>> test_list
['DataStructure', 'easy learning', 2019, 'Array', 'Hello']
>>> test_list.clear()   # 清空列表
>>> test_list
[]

我們看上面的程式碼,可以看到list的相關操作——增刪改查,已經很強大了,還有一些內建方法這裡並沒有做展示,留給讀者自己去發現並體驗。

那麼Python內建的list類是如何被實現的呢?

好吧,答案是動態陣列。說到這裡,不知道大家學Python列表的時候是不是這樣想的——列表很簡單嘛,就是list()類、用中括號[]括起來,然後指導書籍或文件上的各類方法append、insert、pop...在IDE或者Pycharm一頓操作過後,是的我學會了。

但其實真的很不簡單,比如我舉個例子:A[-1]這個操作怎麼實現?列表切片功能怎麼實現?如何自己寫pop()預設刪除列表最右邊的元素(popleft刪除最左邊簡單)?...這些功能用起來爽,但真的自己實現太難了(我也還在學習中,大佬們請輕噴!)如果我們能學習並理解,肯定可以加強我們對陣列這一結構的理解。

動態陣列

什麼是動態陣列

動態陣列是記憶體的連續區域,其大小隨著插入新資料而動態增長。在靜態陣列中,我們需要在分配時指定大小。在定義陣列的時候,其實計算機已經幫我們分配好了記憶體來儲存,實際上我們不能擴充套件陣列,因為它的大小是固定的。比如:我們分配一個大小為10的陣列,則不能插入超過10個專案。

但是動態陣列會在需要的時候自動調整其大小。這一點有點像我們使用的Python列表,可以儲存任意數量的專案,而無需在分配時指定大小。

所以實現一個動態陣列的實現的關鍵是——如何擴充套件陣列?當列表list1的大小已滿時,而此時有新的元素要新增進列表,我們會執行一下步驟來克服其大小限制的缺點:

  1. 分配具有更大容量的新陣列 list2
  2. 設定 list2[i] = list1[i] (i=0,1,2,...,n-1),其中n是該專案的當前編號
  3. 設定list1 = list2,也就是說,list2正在作為新的陣列來引用我們的新列表。
  4. 然後,只要將新的元素插入(新增)到我們的列表list1即可。

接下來要思考的問題是,新陣列應該多大?通常我們得做法是:新陣列的大小是已滿的舊陣列的2倍。我們將在Python中程式設計實現動態陣列的概念,並建立一個簡單的程式碼,很多功能不及Python強大。

實現動態陣列Python程式碼

在Python中,我們利用ctypes的內建庫來建立自己的動態陣列類,因為ctypes模組提供對原始陣列的支援,為了更快的對陣列進行學習,所以對ctypes的知識可以檢視官方文件進行學習。關於Python的公有方法與私有方法,我們在方法名稱前使用雙下劃線**__**使其保持隱藏狀態,程式碼如下:

# -*- coding: utf-8 -*-
# @Time      : 2019-11-01 17:10
# @Author    : yuzhou_1su
# @ContactMe : https://blog.csdn.net/yuzhou_1shu
# @File      : DynamicArray.py
# @Software  : PyCharm

import ctypes


class DynamicArray:
    """A dynamic array class akin to a simplified Python list."""

    def __init__(self):
        """Create an empty array."""
        self.n = 0             # count actual elements
        self.capacity = 1      # default array capacity
        self.A = self._make_array(self.capacity)      # low-level array

    def is_empty(self):
        """ Return True if array is empty"""
        return self.n == 0

    def __len__(self):
        """Return numbers of elements stored in the array."""
        return self.n

    def __getitem__(self, i):
        """Return element at index i."""
        if not 0 <= i < self.n:
            # Check it i index is in bounds of array
            raise ValueError('invalid index')
        return self.A[i]

    def append(self, obj):
        """Add object to end of the array."""
        if self.n == self.capacity:
            # Double capacity if not enough room
            self._resize(2 * self.capacity)
        self.A[self.n] = obj    # Set self.n index to obj
        self.n += 1

    def _resize(self, c):
        """Resize internal array to capacity c."""
        B = self._make_array(c)     # New bigger array
        for k in range(self.n):    # Reference all existing values
            B[k] = self.A[k]
        self.A = B          # Call A the new bigger array
        self.capacity = c   # Reset the capacity

    @staticmethod
    def _make_array(c):
        """Return new array with capacity c."""
        return (c * ctypes.py_object)()

    def insert(self, k, value):
        """Insert value at position k."""
        if self.n == self.capacity:
            self._resize(2 * self.capacity)
        for j in range(self.n, k, -1):
            self.A[j] = self.A[j-1]
        self.A[k] = value
        self.n += 1

    def pop(self, index=0):
        """Remove item at index (default first)."""
        if index >= self.n or index < 0:
            raise ValueError('invalid index')
        for i in range(index, self.n-1):
            self.A[i] = self.A[i+1]
        self.A[self.n - 1] = None
        self.n -= 1

    def remove(self, value):
        """Remove the first occurrence of a value in the array."""
        for k in range(self.n):
            if self.A[k] == value:
                for j in range(k, self.n - 1):
                    self.A[j] = self.A[j+1]
                self.A[self.n - 1] = None
                self.n -= 1
                return
        raise ValueError('value not found')

    def _print(self):
        """Print the array."""
        for i in range(self.n):
            print(self.A[i], end=' ')
        print()

測試動態陣列Python程式碼

上面我們已經實現了一個動態陣列的類,相信都很激動,接下來讓我們來測試一下,看能不能成功呢?在同一個檔案下,寫的測試程式碼如下:

def main():
    # Instantiate
    mylist = DynamicArray()

    # Append new element
    mylist.append(10)
    mylist.append(9)
    mylist.append(8)
    # Insert new element in given position
    mylist.insert(1, 1024)
    mylist.insert(2, 2019)
    # Check length
    print('The array length is: ', mylist.__len__())
    # Print the array
    print('Print the array:')
    mylist._print()
    # Index
    print('The element at index 1 is :', mylist[1])
    # Remove element
    print('Remove 2019 in array:')
    mylist.remove(2019)
    mylist._print()
    # Pop element in given position
    print('Pop pos 2 in array:')
    # mylist.pop()
    mylist.pop(2)
    mylist._print()


if __name__ == '__main__':
    main()

測試結果

激動人心的時刻揭曉,測試結果如下。請結合測試程式碼和陣列的結構進行理解,如果由疏漏,歡迎大家指出。

The array length is:  5
Print the array:
10 1024 2019 9 8 
The element at index 1 is : 1024
Remove 2019 in array:
10 1024 9 8 
Pop pos 2 in array:
10 1024 8 

總結

通過以上的介紹,我們知道了陣列存在靜態和動態型別。而在本部落格中,我們著重介紹了什麼是動態陣列,並通過Python程式碼進行實現。希望你能從此以複雜的方式學會陣列。
總結髮言,其實越是簡單的操作,背後實現原理可能很複雜