定義:
陣列(Array)是一種線性表資料結構。它用一組連續的記憶體空間,來儲存一組具有相同型別的資料。
該定義有幾個關鍵點:
1.線性表。
顧名思義,線性表就是資料排成像一條線一樣的結構。這種結構具有下列特點:存在一個唯一的沒有前驅的(頭)資料元素;存在一個唯一的沒有後繼的(尾)資料元素;此外,每一個資料元素均有一個直接前驅和一個直接後繼資料元素。
常見的線性表還有連結串列、佇列、棧等等.
2.一組連續的記憶體空間和相同型別的資料。
連續記憶體空間,說明在宣告陣列時需要預先指定大小。
相同型別資料很簡單,這裡和容器作區分,更容易瞭解,以java ArrayList容器為例,首先陣列只能儲存基本型別,比如int、long,ArrayList則需要在進一步,使用基本型別的包裝類,儲存Integer,Long。這裡要提一下Autoboxing、Unboxing 則有一定的效能消耗,所以如果特別關注效能,或者希望使用基本型別,就可以選用陣列。
這也就是說,儲存陣列時,對記憶體要求比較高,這就涉及到記憶體的分配了。假設宣告一個1M大小的陣列時,也就說不能是零散的記憶體塊不能連線成一個大的空間,而必須要一整塊連續的記憶體空間才能申請成功。
因為在申請和釋放許多小的塊可能會產生如下狀態:在已用塊之間存在很多小的空閒塊。進而申請大塊記憶體失敗,雖然空閒塊的總和足夠,但是空閒的小塊是零散的,不能滿足申請的大小。
特性:隨機訪問。
優點:陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為 O(1)。
說到隨機訪問,提一下定址公式:
a[i]_address = base_address + i * data_type_size
對於 m * n 的二維陣列,a [ i ][ j ] (i < m,j < n)的地址為:
address = base_address + ( i * n + j) * type_size
(data_type_size 表示陣列中每個元素的大小)
在這裡在說一下定址公式。以一個int[10]的陣列為例,計算機給陣列分配了一塊連續記憶體空間 1000~1039,其中,記憶體塊的首地址為 base_address = 1000,因為存的是int型,這裡data_type_size便是4個位元組。
套入定址公式可得 a[0] 對應 1000~1003,a[1]對應1004~1007,,,以此類推。
缺點:增刪低效,因為陣列中的資料是有序的,每一次的增刪(除了頭部和尾部)都伴隨著陣列的移動。
簡單羅列一下時間複雜度:
1) 插入:最好O(1) 最壞O(n) 平均O(n)
小技巧:陣列若無序,插入新的元素時,可以將第K個位置元素移動到陣列末尾,把心的元素,插入到第k個位置,此處複雜度為O(1)。
3) 刪除:最好O(1) 最壞O(n) 平均O(n)
小技巧:多次刪除集中在一起,提高刪除效率(JVM 標記清除垃圾回收演算法的核心思想)。簡單地說,就是每次的刪除不是真正的刪除,而是新增標記,什麼時候等存滿了,統一集中在一起刪。
注意:
下標越界
一定要主要陣列的下標是從0開始,一定要注意最大的訪問值是size-1。
在 C 語言中,只要不是訪問受限的記憶體,所有的記憶體空間都是可以自由訪問的。根據我們前面講的陣列定址公式,a[6] 也會被定位到某塊不屬於陣列的記憶體地址上,而這個地址正好是儲存變數 i 的記憶體地址,那麼 a[6]=0 就相當於 i=0,所以就會導致程式碼無限迴圈。
陣列越界在 C 語言中是一種未決行為,並沒有規定陣列訪問越界時編譯器應該如何處理。因為,訪問陣列的本質就是訪問一段連續記憶體,只要陣列通過偏移計算得到的記憶體地址是可用的,那麼程式就可能不會報任何錯誤。
JAVA本身還好,自身會做越界檢查,如果越界則會丟擲 java.lang.ArrayIndexOutOfBoundsException
疑問:
陣列和容器之間有什麼區別,哪個更好一些?
以Java ArrayList為例:
ArrayList是Java集合框架類的一員,可以稱它為一個動態陣列。它還有一個優勢就是將陣列的操作封裝起來,比如增刪查,以及動態擴容。
重點來了,動態擴容:
當大小為n的陣列此時已經存了n個資料,當n+1的資料需要儲存的時候,陣列就需要重新分配一塊更大的連續的記憶體,然後將之前的資料複製過去,然後再進行操作,費時費力。
如果使用ArrayList的話,就完全不需要考慮底層了,因為它已經將底層擴容邏輯封裝好了,當空間不夠的時候,它會自動擴充為原來的1.5倍大小。
總結:對於業務開發,直接使用容器就足夠了,省時省力。雖然損耗一丟丟效能,但是完全不會影響到系統整體的效能。但如果做一些非常底層的開發,比如開發網路框架,效能的優化需要做到極致,這個時候陣列就會優於容器,成為首選。
end
您的點贊和關注是對我最大的支援,謝謝!