前文傳送門:
引言
這個系列沒有死,我還在更新。
最近事情太多了,這篇文章也是斷斷續續寫了好幾天才湊完。
上一篇我們介紹了一個基礎概念「時間復雜度」,這篇我們來看第一個真正意義上的數據結構「數組」。
那為什么題目中還會有一個向量呢?這個是什么東西?
不要急,且聽我慢慢道來。
內存
在聊數組之前,需要先了解一個硬件,這個就是我們電腦上內存。
先了解一下內存的物理構造,一般內存的外形都長這樣:
上面那一個一個的小黑塊就是內存顆粒,我們的數據就是存放在那個內存顆粒里面的。
每一個內存顆粒叫做一個 chip 。每個 chip 內部,是由 8 個 bank 組成的。大致長這樣(靈魂畫手挖掘機老師開始上線):
而每一個 bank 是一個二維平面上的矩陣,矩陣中每一個元素中都是保存了 1 個字節,也就是 8 個 bit ,比如這樣:
所以准確的來講,我們的數據就是放在那一個一個的元素中的。
數組
在 C 、C++ 、Java ,都將數組作為一個基礎的數據類型內置。
很可惜,在 Python 中未將數組作為一個基礎的數據類型,雖然 Python 中的 list 和數組很像,但是我更願意叫它列表,而不是數組。
那么數組究竟是啥呢,我這里借用下鄧俊輝老師「數據結構」中的定義:
若集合 S 由 n 個元素組成,且各元素之間具有一個線性次序,則可將它們存放於起始於地址 A 、物理位置連續的一段存儲空間,並統稱作數組( array ) ,通常以 A 作為該數組的標識。具體地,數組 A[] 中的每一元素都唯一對應於某一下標編號,在多數高級程序設計語言中,一般都是從 0 開始編號,依次是 0 號、 1 號、 2 號、 ...、 n - 1 號元素。
其中,對於任何 0 <= i < j < n ,
A[i]
都是A[j]
的前驅( predecessor ) ,A[j]
都是A[i]
的后繼( successor ) 。特別地,對於任何 i >= 1,A[i - 1]
稱作A[i]
的直接前驅( intermediatep redecessor ) ;對於任何 i <= n - 2 ,A[i + 1]
稱作A[i]
的直接后繼( intermediate successor ) 。 任一元素的所有前驅構成其前綴( prefix ) ,所有后繼構成其后綴( suffix ) 。
概念永遠都是這么的枯燥、乏味以及看不懂。
沒關系,繼續我的非本職工作「靈魂畫手」。
首先了解第一個知識點,數組是放在內存里的,內存的結構我們前面介紹過了,那么數組簡單理解就是放在一個一個格子里的,就像這樣:
實際上不是這么放的哈,簡單理解可以先這么理解,這個涉及到內存對齊的知識,有興趣的同學可以度娘了解下。
便於理解,我在字母 A 后面加上了數字,這個數字理解為編號,並且是從 0 開始的。
這個結構讓我想起了糖葫蘆:
數組中的數字就像是穿在棍子上的山楂,大晚上的看的有點流口水。
前驅和后繼可以看下面這張圖:
A3 是 A4 的直接前驅, A5 是 A4 的直接后繼,而排在 A4 前面的都叫 A4 的前驅,排在 A4 后面的都叫 A4 的后繼。
向量
那么啥是向量( vector )呢?
這個東西可以簡單理解為數組的升級版,在各種編程語言中,大多數都對向量的實現做了內置,不過很可惜,在 Python 中,向量未成為一個基礎的數據結構。
那么我就只能對照着 Java 來聊了,向量這個數據結構在 Java 中的實現是:java.util.Vector
,用過 Vector 的應該都是上古程序員了,這個工具類是伴隨 JDK1.0 就有的,但是后面逐漸的棄用,原因我們后面有機會再聊,就不在這多說了。
第一個要聊的問題是,我們在使用數組的時候有什么不方便的地方?這個問題換一種問法,其實就是為什么我們需要使用向量(vector)。
首先,我們在使用數組的時候,需要聲明數組的大小,因為程序需要根據我們聲明的數組大小去向內存申請空間,這就很尷尬了,如果在一個我並不清楚后續可能會用多少空間的場景中,我就沒有辦法去聲明一個數組,因為數組是定長的。
然后就是數組需要是同一數據類型,比如我一個數組如果放入了數組,那么就不能再放入字符串,就不提更加復雜的數據結構,這完全都是因為數組的特性決定的,因為數組在內存上是連續的。
那么遇到了上面的問題怎么辦,當然是解決問題咯,這樣,數組的第一個升級版 Vector 就出來了,在創建的時候不需要聲明大小,然后放入的數據不一定都要是同一個數據類型,比如我想放數字就放數字,想放集合就放集合,想放字符串就放字符串。
聲明一點,向量( Vector )的實現是通過數組來實現的。
在 Java 的 Vector 的源代碼中可以很清楚的找到證據:
protected Object[] elementData;
動態擴容
這里就會出現第一個問題,數組是定長的,那么向量為什么可以不定長?
當然是向量會自動擴容啦~~~~~
說到自動擴容,不禁讓我想起來上面那個糖葫蘆,當棍子放不下山楂還想往上放怎么辦,當然是把棍子變長點咯:
當然,我們在程序中擴容並不是直接把原有數組加長,因為數組的要求是物理空間必須地址連續,而我們卻無法保證,其尾部總是預留了足夠空間可供拓展。
所以能想到的做法就是另行申請一個容量更大的數組,並將原數組中的成員集體搬遷至新的空間,比如這樣:
那么,問題又產生一個,每次變長(擴容)的時候,變長(擴容)多少合適呢?
因為每次擴容的時候,元素的搬遷都需要花費額外的時間,這對性能是一個損耗,我們並不希望這個損耗過大,那么是不是可以一次擴容擴的足夠大呢?當然也不行,這樣會造成內存的浪費,雖然內存便宜,但也不是這么浪費的。
比如初始長度是 10 ,第一次擴容直接擴容到 100 ,第二次到 1000 ,這個就太誇張了,這里可以直接參考 JDK 的源碼,看下大神是怎么擴容的。
篇幅原因我就不帶大家在這看 Java 的源碼了,直接說結果,照顧下學 Python 的同學:
在 Java 中 Vector 的初始長度定義為 10 ,當元素個數超過原有容量長度 1 時,進行擴容,每次擴容的大小是原容量的 1 倍,那么就是第一次擴容是從 10 變成了 20 。
至於為什么是擴大 1 倍而不是其他這里就不展開討論了,實際上 Java 在另一個數據結構列表( List )中擴容的大小是 0.5 倍 + 1 。
不同數據類型
還是拿上面的糖葫蘆舉例子,如果一個桿上只能穿山楂,那么它是一個糖葫蘆(數組),但是,只想穿山楂的糖葫蘆不是一個好糖葫蘆。
但我在吃山楂的同時還想吃臭豆腐,小龍蝦,扇貝,生蚝,帝王蟹,成年人的世界,就是我全都要這么朴實無華。
然后糖葫蘆開啟了超進化模式:
變成了下面這玩意:
這個功能在 Java 中的實現是通過 Object
數組來實現的,因為 Object
在 Java 中是所有類的超類,這個接觸過 Java 的同學應該都清楚,如果沒有接觸過 Java ,可以這么理解,借用道德經里一句話:
道生一,一生二,二生三,三生萬物
而這個 Object
就是那個一,由這個一才產生了豐富多彩的 Java 世界,所以 Object
數組是可以轉化為任何類型的。
小結
小結一下吧,我們聊了一個最簡單的數據結構,數組,還從數組中引申出來了向量( Vector ),很遺憾,Python 中沒有這兩個基礎數據結構。
在向量( Vector )中,我們介紹了向量( Vector )的兩個不同於數組的特性,一個是動態擴容,還有一個是向量中可以放不同的數據類型,這對比數組極大的方便了我們日常的使用。
順便說一句,雖然很多數據結構都是基於數組的,包括本文介紹的 Vector ,還有后面會介紹到的列表 List ,但是我們在實際的使用中是很少會直接使用數組的,基本上都是使用數組的超進化體。