1. 程式人生 > >【資料結構與演算法】二、陣列

【資料結構與演算法】二、陣列

一、線性表

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。 容器類的最大的優勢就是可以將很多陣列操作的細節封裝起來。 容器適用於業務開發,省時省力;非常底層的開發(網路框架等)或效能要求特別高,優先使用陣列

  1. Java ArrayList 無法儲存基本型別,比如int、long,需要封裝為Integer、Long類,而 Autoboxing、Unboxing 則有一定的效能消耗,所以如果特別關注效能,或者希望使用基本型別,就可以選用陣列。
  2. 如果資料大小事先已知,並且對資料的操作非常簡單,用不到 ArrayList 提供的大部分方法,也可以直接使用陣列。
  3. 表示多維陣列時,用陣列往往會更加直觀。比如 Object[][] array;而用容器的話則需要這樣定義:ArrayList<ArrayList> array。