數組
提到數組,相信大家的都不陌生,畢竟每個編程語言都會有它的影子。
數組是最基礎的數據結構,儘管數組看起來非常的基礎簡單,但這個基礎的數據結構要掌握其精髓,也不是那麼簡單事。
開門見山
數組(Array)是一種線性表數據結構,它用一組連續的內存空間,來存儲一組具有相同類型的數據。
這個定義有幾個關鍵詞,也是數組的精髓所在。下面就從這幾個關鍵詞進一步理解數組。
第一個是線性表。顧名思義,線性表的特徵就是數據排成像一條線一樣的結構。每個線性表的數據最多隻有前和後兩個方向。除了數組,鏈表、隊列、棧等數據結構也是線性表結構。
舉個栗子,糖葫蘆串就與線性表的特徵非常相似。糖葫蘆(數據)串成在一條直線的竹籤,並且每個糖葫蘆(數據)最多隻有前和後兩個方向。
第二個是連續的內存空間和相同的類型的數據。因爲這兩個條件的限制,數組有了非常重要的特性:隨機訪問元素,隨機訪問元素的時間複雜度爲O(1)。但有利必有弊,這兩個條件的限制導致數據在進行插入和刪除一個數據的時候,爲了保證數據的連續性,就需要做數據的搬移操作。
隨機訪問
數組是如何實現根據下表隨機訪問數組元素的呢?
我們拿一個長度爲5的int
類型的數組int a[5]
,來舉例子。在我們定義這個數組時,計算機會給數組int a[5]
,分配了一塊連續的內存空間。
假設,數組int a[5]
內存塊的首地址爲base_address=100
,那麼
a[0]
的地址就是100(首地址)a[2]
的地址就是104a[3]
的地址就是108a[3]
的地址就是112a[4]
的地址就是116
計算機是通過訪內存地址,來訪問內存中存儲的數據。那麼,當計算機要隨機訪問數組中的某個元素時,會通過下面這條尋址公式,計算出對應元素的內存地址,從而通過內存地址訪問數據。
a[i]_address = base_address + i * data_type_size
a[i]_address
表示對應數組下標的內存地址,data_type_size
表示數組存儲的數據類型的大小,數組int a[5]
。存儲的是5個int
類型的數據,它的data_type_size
就爲4個字節。
二維數組的尋址公式,假設二位數組的維度是m*n,則公式爲:
a[i][j]_address = base_address + ( i * n + j ) * data_type_size
爲什麼數組下標從0開始?
要先解答這個問題時,我們試想假設數組下標從1開始,a[1]表示數組的首地址,那麼計算機的尋址公式就會變成爲:
a[i]_address = base_address + (i - 1) * data_type_size
對比數組下標從0開始和設數組下標從1開始的尋址公式,我們不難看出,從1開始編號,每次隨機訪問數組元素都多了一次減法運算,對於CPU來說,就是多了一次減法指令。
更何況數組是非常基礎的數據結構,使用頻率非常的高,所以效率優化必須要做到極致。所以爲了減少CPU的一次減法指令,數組選擇了從0開始編號,而不是從1開始。
以上是從計算機尋址公式角度分析的,當然其實還有歷史等原因。
數組的插入和刪除過程
前面提到對於數組的定義,數組爲了保持內存數據的連續性,就會導致插入和刪除這兩個操作比比較低效。接下來通過代碼來闡述爲什麼導致低效呢?又有哪些方法改進?
插入操作過程
插入操作對於數據的不同的場景和不同的插入位置,時間複雜度都略有不同。接下來以數組的數據是有序和沒有規律的兩種場景分析插入操作。
不管什麼場景,如果在數組的末尾插入元素,那麼就非常簡單,不需要搬移數據,直接將元素放入到數組的末尾,這時空間複雜度就爲O(1)。
如果在數組的開頭或中間插入數據呢?這時可以根據場景的不同,採用不同的方式。
如果數組的數據是有序(從小到大或從大到小),在第k位置插入一個新的元素時,就必須把k之後的數據往後移動一位,此時最壞時間複雜度是O(n)。
如果數組的數據沒有任何規律,那麼在第k位置插入一個新的元素時,先將舊的第k位置的數據搬移到數據末尾,在把新的元素數據直接放入到第k位置。那麼在這種特定場景下,在第k個位置插入一個元素的時間複雜度就爲O(1)。
一圖勝千言,我們以圖的方式展現數組的數據是有序和沒有規律場景的插入元素的過程。
刪除操作過程
跟插入數據類似,如果我們要刪除第k位置的數據,爲了內存的連續性,也是需要數據搬移,不然中間就會出現空洞,內存就不連續了。
如果刪除數組末尾的數據,則時間複雜度爲O(1);如果刪除開頭的數據,因需把k位置之後的數據往前搬移一位,那麼時間複雜度就爲O(n)。
一圖勝千言,我們以圖的方式展現數組刪除操作。
代碼實戰數組插入、刪除和查詢
本例子,以數組的數據是有序(數據是從小到大的順序)的場景,實現數組的插入、刪除和查詢操作。
先用結構體定義數組的屬性,分別有數組的長度、被佔用的個數和數組指針。
struct Array_t
{
int length; // 數組長度
int used; // 被佔用的個數
int *arr; // 數組地址
};
創建數組:
根據結構體設定的數組長度,創建對應連續空間並且相同類型的數組
void alloc(struct Array_t *array)
{
array->arr = (int *)malloc(array->length * sizeof(int));
}
插入過程:
- 判斷數組佔用個數是否超過數組長度
- 遍歷數組,找到待插入新元素的下標idx
- 如果找到插入元素的下標不是末尾位置,則需要將idx數據依次往後搬移一位
- 在idx下標插入新元素,並將數組佔用個數+1
/*
* 插入新元素
* 參數1:Array_t數組結構體指針
* 參數2:新元素的值
* 返回:成功返回插入的數組下標,失敗返回-1
*/
int insertElem(struct Array_t *array, int elem)
{
// 當數組被佔用數大於等於數組長度時,說明數組所有下標都已存放數據了,無法在進行插入
if (array->used >= array->length)
{
std::cout << "ERROR: array size is full, can't insert " << elem << " elem." << std::endl;
return -1;
}
int idx = 0;
// 遍歷數組,找到大於新元素elem的下標idx
for (idx = 0; idx < array->used; idx++)
{
// 如果找到數組元素的值大於新元素elem的值,則退出
if (array->arr[idx] > elem)
{
break;
}
}
// 如果插入的下標的位置不是在末尾,則需要把idx之後的
// 數據依次往後搬移一位,空出下標爲idx的元素待後續插入
if (idx < array->used)
{
// 將idx之後的數據依次往後搬移一位
memmove(&array->arr[idx + 1], &array->arr[idx], (array->used - idx) * sizeof(int));
}
// 插入元素
array->arr[idx] = elem;
// 被佔用數自增
array->used++;
// 成功返回插入的數組下標
return idx;
}
刪除過程:
- 判斷待刪除的下標是否合法
- 將待刪除idx下標之後的數據往前搬移一位
/*
* 刪除新元素
* 參數1:Array_t數組結構體指針
* 參數2:刪除元素的數組下標位置
* 返回:成功返回0,失敗返回-1
*/
int deleteElem(struct Array_t *array, int idx)
{
// 判斷下標位置是否合法
if (idx < 0 || idx >= array->used)
{
std::cout << "ERROR:idx[" << idx << "] not in the range of arrays." << std::endl;
return -1;
}
// 將idx下標之後的數據往前搬移一位
memmove(&array->arr[idx], &array->arr[idx + 1], (array->used - idx - 1) * sizeof(int));
// 數組佔用個數減1
array->used--;
return 0;
}
查詢下標:
遍歷數組,查詢元素值的下標,若找到則返回數組元素;沒找到則報錯提示
/*
* 查詢元素下標
* 參數1:Array_t數組結構體指針
* 參數2:元素值
* 返回:成功返回元素下標,失敗返回-1
*/
int search(struct Array_t *array, int elem)
{
int idx = 0;
// 遍歷數組
for (idx = 0; idx < array->used; idx++)
{
// 找到與查詢的元素值相同的數組元素,則返回元素下標
if (array->arr[idx] == elem)
{
return idx;
}
// 如果數組元素大於新元素,說明未找到此數組下標, 則提前報錯退出
// 因爲本例子的數組是有序從小到大的
if (array->arr[idx] > elem)
{
break;
}
}
// 遍歷完,說明未找到此數組下標,則報錯退出
std::cout << "ERROR: No search to this" << elem << " elem." << std::endl;
return -1;
}
打印數組:
輸出數組的每個元素
void dump(struct Array_t *array)
{
int idx = 0;
for (idx = 0; idx < array->used; idx++)
{
std::cout << "INFO: array[" << idx << "] : " << array->arr[idx] << std::endl;
}
}
main函數:
創建長度爲3,類型爲int的數組,並對數組插入元素、刪除元素、查詢元素和打印元素。
int main()
{
struct Array_t array = {3, 0, NULL};
int idx = 0;
std::cout << "alloc array length: " << array.length << " size: " << array.length * sizeof(int) << std::endl;
alloc(&array);
if (!array.arr)
return -1;
std::cout << "insert 1 elem" << std::endl;
insertElem(&array, 1);
std::cout << "insert 0 elem" << std::endl;
insertElem(&array, 0);
std::cout << "insert 2 elem" << std::endl;
insertElem(&array, 2);
dump(&array);
idx = search(&array, 1);
std::cout << "1 elem is at position " << idx << std::endl;
idx = search(&array, 2);
std::cout << "2 elem is at position " << idx << std::endl;
std::cout << "delect position [2] elem " << std::endl;
deleteElem(&array, 2);
dump(&array);
return 0;
}
運行結果:
[root@lincoding array]# ./array
alloc array length: 3 size: 12
insert 1 elem
insert 0 elem
insert 2 elem
INFO: array[0] : 0
INFO: array[1] : 1
INFO: array[2] : 2
1 elem is at position 1
2 elem is at position 2
delect position [2] elem
INFO: array[0] : 0
INFO: array[1] : 1
小結
數組是最基礎、最簡單的數據結構。數組用一塊連續的內存空間,來存儲相同類型的一組數據,最大的特點就是隨機訪問元素,並且時間複雜度爲O(1)。但是插入、刪除操作也因此比較低效,時間複雜度爲O(n)。
聲明:本文參考極客時間—數據結構與算法部分內容。
微信公衆號:小林coding
用簡潔的方式,分享編程小知識。