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

Python序列類型的本質

在本博客中,咱們來聊聊探討Python的各類「序列」類,內置的三大經常使用數據結構——列表類(list)、元組類(tuple)和字符串類(str)的本質。html

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

可是熟悉Python的讀者可能知道這3種數據結構又有一些不一樣:好比元組和字符串是不能修改的,列表能夠修改。shell

計算機內存中的數組結構

計算機體系結構中,咱們知道計算機主存由位信息組成,這些位一般被歸類成更大的單元,這些單元則取決於精準的系統架構。一個典型的單元就是一個字節,至關於8位。編程

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

通常來講,編程語言記錄標識符和其關聯值所存儲的地址之間的關係。好比,當咱們聲明標識符 x 就有可能和存儲器中的某一值相關聯,而標識符 y就可能和其餘的值相關聯。一組相關的變量可以一個接一個地存儲在計算機存儲器的一塊連續區域內。咱們將這種方式稱爲 數組數據結構

咱們來看Python中的例子,一個文本字符串 HELLO 是以一列有序字符的形式存儲的,假定該字符串的每一個Unicode字符須要兩個字節的存儲空間。最下面的數字就是該字符串的索引值。架構

咱們能夠看到,數組能夠存儲多個值而無需構造具備特定索引的多個變量來指定其中的每一個項目,而且幾乎在全部編程語言(例如C、Java、C#、C++)中使用,可是Python更具備優點。Python在構建列表時,熟悉的讀者可能知道,不須要預先定義數組或列表的大小,相反,在Python中,列表具備動態性質,咱們能夠不斷的往列表中添加咱們想要的數據元素。接下來,讓咱們看看Python列表的知識(已經熟悉的讀者能夠快速瀏覽或者跳過)。app

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列表的內存分配背後的基礎知識

所以,讓咱們經過編碼實踐以及內存中保存的數組的實際大小與給定大小之間的關係來查看這種額外的空間演示。

前往Jupyter notebook進行練習。或者使用本身選擇的任何編輯器或開發環境。複製下面編寫的代碼。

# 導入sys模塊能方便咱們使用gestsizeof函數
import sys

# set n
n = 20
# set empty list
list = []
for i in range(n):
    a = len(list)
    # 調用getsizeof函數用於給出Python中存儲對象的真實字節數
    b = sys.getsizeof(list)
    print('Length:{0:3d}; Size of bytes:{1:4d}'.format(a, b))
    # Increase length by one
    list.append(n)
複製代碼

運行代碼,能夠看到以下輸出:

在這裏插入圖片描述
如今,隨着咱們增長列表的長度,字節也增長了。咱們分析一下, Length:1位置的元素填入列表時,字節數從64跳到96,增長了32個字節。由於本實驗是在64位機器上運行的,這代表每一個內存地址是64位(即8個字節)。增長的32個字節即爲分配的用於存儲4個對象引用的數組大小。當增長第2個、第3個或者第4個元素時,內存佔用沒有任何改變。字節數96可以提供4個對象的引用。

96\ =\ 64\ +\ 8\ \times \ 4

Length:10時,字節數從一開始的64跳到192,能存下16個對象的引用,

192\ =\ 64\ +\ 8\ \times \ 16

一直到Length: 17後又開始跳轉,因此理論上264個字節數應該能夠存下25個對象

264\ =\ 64\ +\ 8\ \times \ 25

但由於咱們在代碼中設置n=20,而後程序就終止了。

咱們能夠看到Python內置的list類足夠智能,知道當須要額外的空間來分配數據時,它會爲它們提供額外的大小,那麼這到底是如何被實現的呢?

好吧,答案是動態數組。說到這裏,不知道你們學Python列表的時候是否是這樣想的——列表很簡單嘛,就是list()類、用中括號[]括起來,而後指導書籍或文檔上的各種方法append、insert、pop...在各類IDE一頓操做事後,是的我以爲我學會了。

但其實背後的原理真的很不簡單,好比我舉個例子: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代碼進行實現。但願你能今後以複雜的方式學會數組。 總結髮言,其實越是簡單的操做,背後實現原理可能很複雜。

相關文章
相關標籤/搜索