【資料結構與演算法】二、陣列
一、線性表
1、定義
線性表(Linear List):零個或多個數據元素的有限序列。
序列(有序):若元素存在多個,則第一個元素無前驅,最後一個無後繼,其他每個元素都有且只有一個前驅和後繼
2、數學表示
線性表:(a1, a2, a3, ..., ai-1, ai, ai+1, ..., an )
ai-1 是 ai 的直接前驅元素, ai+1 是 ai 的直接後繼元素。線性表元素的個數為n(n≥0)定義為線性表的長度,當 n = 0 時,稱為空表
3、線性表的抽象資料型別
ADT 線性表(List) Data 線性表的資料物件集合為{a1, a2, ......, an},每個元素的型別均為DataType。 其中,除第一個元素a1外,每一個元素有且只有一個直接前驅元素, 除了最後一個元素an外,每一個元素有且只有一個直接後繼元素。 資料元素之間的關係是一對一的關係。 Operation InitList(*L): 初始化操作,建立一個空的線性表L。 ListEmpty(L): 若線性表為空,返回true,否則返回false。 ClearList(*L): 將線性表清空。 GetElem(L, i, *e): 將線性表L中的第i個位置元素值返回給e。 LocateElem(L, e): 線上性表L中查詢與給定值e相等的元素, 如果查詢成功,返回該元素在表中序號表示成功; ListInsert(*L,i,e): 在L的第i個位置插入新元素e。 ListDelete(*L,i,*e): 刪除L中的第i個元素,並用e返回其值。 ListLength(L): 返回L中的元素個數 endADT
二、陣列(Array)概述
1、定義
陣列是一種線性表資料結構。用一組連續的記憶體空間來儲存一組具有相同型別的資料
解讀:
- 線性表:eg:陣列、佇列、棧、連結串列
- 非線性表:eg:樹、堆、圖等
- 連續記憶體空間 + 相同型別資料 =》隨機訪問
2、儲存
==》元素儲存的記憶體地址:
a[i]_address = base_address + i * data_type_size
其中, data_type_size 表示陣列中每個元素的大小。
==》擴充套件:二維陣列的記憶體定址公式 對於 m*n 的陣列,a[i][j] ( i < m, j < n )的地址為:
a[i][j]_address = base_address + ( i * n + j ) * type_size
三、陣列的相關操作
低效的“插入”和“刪除”
1、插入
(1)傳統過程
將一個數據插入到陣列中的第 k 個位置。為了把第 k 個位置騰出來,給新來的資料,需要將第 k~n 這部分的元素都順序地往後挪一位。
==》
最好情況時間複雜度為 O(1)
最壞情況時間複雜度為 O(n)
平均情況時間複雜度為 (1+2+…n)/n=O(n)
(2)特殊場景
情況: 如果陣列中儲存的資料並沒有任何規律,陣列只是被當作一個儲存資料的集合。 方法: 將第 k 位的資料搬移到陣列元素的最後,把新的元素直接放入第 k 個位置。
==》複雜度為 O(1)
目標:將 x 插入第 3 個位置 a, b, c, d, e ==》a,b,x,d,e,c
2、刪除
(1)傳統過程
要刪除第 k 個位置的資料,為了記憶體的連續性,也需要搬移資料。
==》
最好情況時間複雜度為 O(1)
最壞情況時間複雜度為 O(n)
平均情況時間複雜度為 (1+2+…n)/n=O(n)
(2)特殊場景
情況: 不一定非得追求陣列中資料的連續性。 方法: 先記錄下已經刪除的資料(只記錄資料被刪除,不執行搬移資料的操作)。當陣列沒有更多空間儲存資料時,觸發真正的刪除操作,也就是將多次刪除操作集中在一起執行,從而提高刪除的效率。
==》擴充套件: JVM 標記清除垃圾回收演算法的核心思想
==》很多時候我們並不是要去死記硬背某個資料結構或者演算法,而是要學習它背後的思想和處理技巧,這些東西才是最有價值的。
四、陣列訪問越界問題
1、示例
int main(int argc, char* argv[]){
int i = 0;
int arr[3] = {0};
for(; i <= 3; i++){
arr[i] = 0;
printf("hello world\0");
}
return 0;
}
結果如下: 發生陣列訪問越界==》執行結果並非是列印三行“hello word”,而是四行“hello word”或無限列印“hello world”
2、分析
(1)在 C 語言中,只要不是訪問受限的記憶體,所有的記憶體空間都是可以自由訪問的。根據我們前面講的陣列定址公式,a[3] 也會被定位到某塊不屬於陣列的記憶體地址上,而這個地址正好是儲存變數i 的記憶體地址(下面解釋),那麼 a[3]=0 就相當於 i=0,所以就會導致程式碼無限迴圈。
(2)函式體內的區域性變數存在棧上,且是連續壓棧。在Linux程序的記憶體佈局中,棧區在高地址空間,從高向低增長。變數i和arr在相鄰地址,且i比arr的地址大,所以arr越界正好訪問到i。當然,前提是i和arr元素同類型,否則那段程式碼仍是未決行為。
五、容器 vs. 陣列
很多語言都提供了容器類,比如 Java 中的 ArrayList、C++ STL 中的 vector。 容器類的最大的優勢就是可以將很多陣列操作的細節封裝起來。 容器適用於業務開發,省時省力;非常底層的開發(網路框架等)或效能要求特別高,優先使用陣列
- Java ArrayList 無法儲存基本型別,比如int、long,需要封裝為Integer、Long類,而 Autoboxing、Unboxing 則有一定的效能消耗,所以如果特別關注效能,或者希望使用基本型別,就可以選用陣列。
- 如果資料大小事先已知,並且對資料的操作非常簡單,用不到 ArrayList 提供的大部分方法,也可以直接使用陣列。
- 表示多維陣列時,用陣列往往會更加直觀。比如 Object[][] array;而用容器的話則需要這樣定義:ArrayList<ArrayList> array。