一本正經的聊數據結構(2):數組與向量


前文傳送門:

一本正經的聊數據結構(1):時間復雜度

引言

這個系列沒有死,我還在更新。

最近事情太多了,這篇文章也是斷斷續續寫了好幾天才湊完。

上一篇我們介紹了一個基礎概念「時間復雜度」,這篇我們來看第一個真正意義上的數據結構「數組」。

那為什么題目中還會有一個向量呢?這個是什么東西?

不要急,且聽我慢慢道來。

內存

在聊數組之前,需要先了解一個硬件,這個就是我們電腦上內存。

先了解一下內存的物理構造,一般內存的外形都長這樣:

上面那一個一個的小黑塊就是內存顆粒,我們的數據就是存放在那個內存顆粒里面的。

每一個內存顆粒叫做一個 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 ,但是我們在實際的使用中是很少會直接使用數組的,基本上都是使用數組的超進化體。

參考

https://zhuanlan.zhihu.com/p/83449008


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM