(三)陣列
定義
陣列(Array)是一種線性表資料結構。它用一組連續的記憶體空間,來儲存一組具有相同型別的資料。
- 線性表
陣列、連結串列、佇列、棧
- 非線性表
二叉樹、堆、圖
隨機訪問
連續的記憶體空間和相同型別的資料。正是因為這兩個限制,它才有了一個堪稱“殺手鐗”的特性:“隨機訪問”。但有利就有弊,這兩個限制也讓陣列的很多操作變得非常低效,比如要想在陣列中刪除、插入一個資料,為了保證連續性,就需要做大量的資料搬移工作。
- short 2位元組 16位整數
- int 4 位元組 32位整數
- long 8位元組 64位整數
int[] a = new int[10],分配了一塊連續記憶體空間 1000~1039,其中,記憶體塊的首地址為 base_address = 1000。
定址公式
a[i]_address = base_address + i * data_type_size
陣列和連結串列的區別:
- 連結串列適合插入、刪除,時間複雜度 O(1)
- 陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為 O(1)
注:陣列是適合查詢操作,但是查詢的時間複雜度並不為 O(1)。即便是排好序的陣列,你用二分查詢,時間複雜度也是 O(logn)。
低效的“插入”和“刪除”
插入
假設陣列的長度為 n,現在,如果我們需要將一個資料插入到陣列中的第 k 個位置。為了把第 k 個位置騰出來,給新來的資料,我們需要將第 k~n 這部分的元素都順序地往後挪一位。
- 在陣列末尾插入元素,不需要移動資料,時間複雜度為O(1)。
- 在陣列開頭插入元素,所有的資料都需要依次往後移動一位,所以最壞時間複雜度是 O(n)。
在每個位置插入元素的概率是一樣,平均時間複雜度:(1+2+…n)/n=O(n)。 - 如果陣列資料有序,需要按照剛才的方法搬移 k 之後的資料。
- 如果陣列中儲存的資料並沒有任何規律,陣列只是被當作一個儲存資料的集合。
直接將第 k 位的資料搬移到陣列元素的最後,把新的元素直接放入第 k 個位置。
利用這種處理技巧,在特定場景下,在第 k 個位置插入一個元素的時間複雜度就會降為 O(1)。
刪除
跟插入資料類似,如果我們要刪除第 k 個位置的資料,為了記憶體的連續性,也需要搬移資料,不然中間就會出現空洞,記憶體就不連續了。
- 刪除陣列末尾的資料,則最好情況時間複雜度為 O(1)
- 如果刪除開頭的資料,則最壞情況時間複雜度為 O(n)
- 平均情況時間複雜度也為 O(n)
在某些特殊場景下,我們並不一定非得追求陣列中資料的連續性。將多次刪除操作集中在一起執行,來提高刪除的效率。
為了避免 d,e,f,g,h 這幾個資料會被搬移三次,我們可以先記錄下已經刪除的資料。
每次的刪除操作並不是真正地搬移資料,只是記錄資料已經被刪除。
當陣列沒有更多空間儲存資料時,我們再觸發執行一次真正的刪除操作,這樣就大大減少了刪除操作導致的資料搬移。
陣列越界
int main(int argc, char* argv[]){
int i = 0;
int arr[3] = {0};
for(; i<=3; i++){
arr[i] = 0;
printf("hello world\n");
}
return 0;
}
i = 3 訪問越界。陣列越界在 C 語言中是一種未決行為,並沒有規定陣列訪問越界時編譯器應該如何處理。
因為,訪問陣列的本質就是訪問一段連續記憶體,只要陣列通過偏移計算得到的記憶體地址是可用的,那麼程式就可能不會報任何錯誤。
容器
針對陣列型別,很多語言都提供了容器類,比如 Java 中的 ArrayList、C++ STL 中的 vector。
ArrayList 最大的優勢就是可以將很多陣列操作的細節封裝起來。比如前面提到的陣列插入、刪除資料時需要搬移其他資料等。另外,它還有一個優勢,就是支援動態擴容。
陣列本身在定義的時候需要預先指定大小,因為需要分配連續的記憶體空間。如果我們申請了大小為 10 的陣列,當第 11 個資料需要儲存到陣列中時,我們就需要重新分配一塊更大的空間,將原來的資料複製過去,然後再將新的資料插入。
使用 ArrayList,不需要關心底層的擴容邏輯,ArrayList 已經實現好了。每次儲存空間不夠的時候,它都會將空間自動擴容為 1.5 倍大小。
注意:擴容操作涉及記憶體申請和資料搬移,是比較耗時的。所以,如果事先能確定需要儲存的資料大小,最好在建立 ArrayList 的時候事先指定資料大小。
更適合使用陣列的地方:
- Java ArrayList 無法儲存基本型別,比如 int、long,需要封裝為 Integer、Long 類,而 Autoboxing、Unboxing 則有一定的效能消耗,所以如果特別關注效能,或者希望使用基本型別,就可以選用陣列。
- 如果資料大小事先已知,並且對資料的操作非常簡單,用不到 ArrayList 提供的大部分方法,也可以直接使用陣列。
- 表示多維陣列時,用陣列往往會更加直觀。比如 Object[][] array;而用容器的話則需要這樣定義:
ArrayList<ArrayList<object>> array
。
陣列下標從0開始
- 從陣列儲存的記憶體模型上來看,“下標”最確切的定義應該是“偏移(offset)”
計算 a[k]的記憶體地址:
a[k]_address = base_address + k * type_size
如果陣列從 1 開始計數,那我們計算陣列元素 a[k]的記憶體地址就會變為:
a[k]_address = base_address + (k-1)*type_size
從 1 開始編號,每次隨機訪問陣列元素都多了一次減法運算,對於 CPU 來說,就是多了一次減法指令。
更多的是歷史原因:
C 語言設計者用 0 開始計數陣列下標,之後的 Java、JavaScript 等高階語言都效仿了 C 語言,或者說,為了在一定程度上減少 C 語言程式設計師學習 Java 的學習成本,因此繼續沿用了從 0 開始計數的習慣。實際上,很多語言中陣列也並不是從 0 開始計數的,比如 Matlab。甚至還有一些語言支援負數下標,比如 Python。
小結
陣列用一塊連續的記憶體空間,來儲存相同型別的一組資料,最大的特點就是支援隨機訪問,但插入、刪除操作也因此變得比較低效,平均情況時間複雜度為 O(n)。在平時的業務開發中,我們可以直接使用程式語言提供的容器類,但是,如果是特別底層的開發,直接使用陣列可能會更合適。
思考
JVM 標記清楚垃圾回收演算法
大多數主流虛擬機器採用可達性分析演算法來判斷物件是否存活,在標記階段,會遍歷所有 GC ROOTS,將所有 GC ROOTS 可達的物件標記為存活。只有當標記工作完成後,清理工作才會開始。
不足:
- 效率問題。標記和清理效率都不高,但是當知道只有少量垃圾產生時會很高效。
- 空間問題。會產生不連續的記憶體空間碎片。
二維陣列記憶體定址
對於 m * n 的陣列,a [ i ][ j ] (i < m,j < n)
的地址為:
address = base_address + ( i * n + j) * type_size
死迴圈問題
待解決
相關文章
- 陣列累加和問題三連陣列
- [陣列]Leetcode15三數之和陣列LeetCode
- c語言中三維陣列C語言陣列
- 【陣列】1550. 存在連續三個奇數的陣列(簡單)陣列
- 《Java從入門到失業》第三章:基礎語法及基本程式結構(3.9):陣列(陣列基本使用、陣列的迴圈、陣列拷貝、陣列排序、多維陣列)Java陣列排序
- JavaSE 陣列:一維陣列&二維陣列Java陣列
- Java陣列初始化三種方式Java陣列
- 演算法之陣列——三數之和演算法陣列
- 陣列的三種初始化方式陣列
- Rust與Java程式碼比較:將二維陣列轉為三維陣列RustJava陣列
- 陣列,陣列類,SyStem類陣列
- Java陣列03:陣列使用Java陣列
- 名將列陣!《戰三國 八陣奇謀》今日公測!
- JavaScript基礎總結(三)——陣列總結JavaScript陣列
- Go語言系列(三)之陣列和切片Go陣列
- 【JS】JS陣列新增元素的三種方法JS陣列
- c語言中陣列的三種型別C語言陣列型別
- 陣列--移除陣列中指定的元素,不改變原陣列和改變原陣列陣列
- 陣列二:使用陣列可變函式為陣列排序陣列函式排序
- 指標陣列和陣列指標與二維陣列指標陣列
- 陣列指標,指標陣列陣列指標
- Javascript - 陣列和陣列的方法JavaScript陣列
- 陣列[簡單]1550. 存在連續三個奇數的陣列2020/11/14(6)陣列
- 記一次陣列操作:陣列 A 根據陣列 B 排序陣列排序
- [求解]陣列,分成倆個陣列,陣列值之和的相差最小。陣列
- 陣列陣列
- systemtap 探祕(三)- 型別、變數和陣列型別變數陣列
- go 陣列傳遞給函式三種方式Go陣列函式
- 【IDL】遠取文字中三維陣列的方法陣列
- C語言陣列實現三子棋C語言陣列
- 在PHP中陣列遍歷的三種方法PHP陣列
- 矩陣和陣列矩陣陣列
- iOS 字典轉陣列,陣列轉字典iOS陣列
- PHP陣列轉換為js陣列PHP陣列JS
- 【陣列】977. 有序陣列的平方陣列
- 指標陣列與陣列指標指標陣列
- 2-7 陣列:動態陣列陣列
- 陣列演算法-差分陣列陣列演算法