資料結構與演算法學習-陣列

by在水一方發表於2019-03-07

前言

這一篇筆記主要記錄總結了線性表資料結構中的陣列概念以及相關的演算法

名詞解釋

1. 線性表(Linear List)

線性表是資料排成像一條線一樣的結構,每個線性表上的資料最多有向前和向後兩個方向。除了陣列外,連結串列、佇列、棧也是線性表資料結構。

資料結構與演算法學習-陣列

2. 非線性表

和線性表相對立,資料之間不是簡單的前後關係,這樣的結構稱為非線性表,如圖、樹、堆等資料結構。

資料結構與演算法學習-陣列

陣列

陣列是一種線性表資料結構,是用一組連續的記憶體空間,來儲存一組具有相同資料型別的資料。幾乎在所有的程式語言都存在陣列這中最基本的資料結構型別。在 Objective-C 語言中是 NSArray,當然 Objective-C 是 C 的超集,所以也完全可以使用 C 語言的陣列型別。

資料結構與演算法學習-陣列

隨機訪問

正式因為陣列是用一組連續的記憶體空間來儲存資料的,所以陣列支援下標隨機訪問,複雜度是 O(1)


int[] a = new int[10]

// 陣列a 首地址
base_address = 1000

// 定址公式 data_type_size:陣列中元素的資料型別長度
a[i]_address = base_address + i * data_type_size
複製程式碼

插入和刪除

相比於複雜度是 O(1)的隨機訪問操作,對於陣列而言,插入和刪除操作的複雜度都為O(n),因為每次要在陣列的第 k 個位置插入或者刪除一個元素的話,都需要移動 k ~ n 個元素的位置。

優化技巧:

插入操作:如果不需要追求陣列中元素的有序,則可以考慮直接將第 k 個位置的元素移到陣列的末尾,然後把要插入的新元素放到第 k 個位置就行,這樣,複雜度也就是O(1)。 刪除操作:在一些場景下,並不追求陣列中資料的連續性,可以將多次刪除操作集中在一起執行。先記錄下已經刪除的元素,並不真的刪除,當陣列沒有更多空間時,再觸發真正的刪除操作,這樣可以省下大量重複的資料移動操作。

警惕陣列越界問題

看一段 C 語言程式碼:

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;
}
複製程式碼

這裡就會出現陣列越界問題,C語言的執行結果是未決,就是沒有規定陣列訪問越界時編譯器應該如何處理。如果該記憶體是一塊可以訪問的不受限記憶體,在x86架構機器下,那麼執行結果是會無線迴圈列印hello world。原因是記憶體分配從棧的高位到低位開始,i 變數實際上與陣列元素 arr[2] 相鄰。陣列下標從 0 開始,當執行到迴圈最後一次 i = 3 時,根據根據之前的定址公式 a[i]_address = base_address + i * data_type_size,實際上 arr[3] 訪問的會是變數 i 的地址,賦值給 i = 0,然後死迴圈。

這裡還和使用的編譯器記憶體分配以及位元組對齊有關係,有些編譯器會預設開啟堆疊保護,當如果變數訪問一塊不屬於自己的記憶體時,會出現編譯錯誤。 為了不讓程式出現這種不確定的錯誤,導致 debug 難度大,還有就是容易被黑客利用攻擊,所以寫程式碼時要特別警惕陣列越界。不過很多高階語言的都會預設做越界檢查,如 Objective-C 裡面的陣列,如果越界訪問就會下面這種經典錯誤。

資料結構與演算法學習-陣列

怎麼選擇容器還是陣列?

容器優點

  1. 將很多陣列插入刪除等操作細節封裝起來,提供很多易用的API。
  2. 動態擴容,如果插入資料的時候發現陣列的空間不夠,就需要重新申請一塊更大的記憶體空間,並把原來的資料都複製進新的陣列,在將新的資料插入。但是如果事先知道資料的大小,可以建立的時候就制定好資料的大小,這樣可以避免不必要的動態擴容操作。

陣列優點

  1. 儲存基本資料型別。
  2. 當陣列大小事先已知,並且資料操作比較簡單。

總結

日常業務開發,使用高階語言提供的陣列容器就行,如 Objective-C 的 NSArray,損失一點效能,但寫起來方便簡單。如果是做比較注重效能的底層開發,可以考慮使用陣列。

陣列的下標為什麼從 0 開始?

  1. 定址演算法,如果下標不從 0 開始,從 1 開始會怎樣,定址演算法就變成 a[i]_address = base_address + (i - 1 * data_type_size),轉成彙編指令,對於 CPU 而言,就多了一條要執行的減法指令,而這種陣列的操作是很頻繁很底層的操作,為了優化,所以陣列的下標都設計從 0 開始。
  2. 歷史原因,由於 C 語言是後面很多語言設計的參考,為了保持程式設計師的編碼習慣,所以後面的程式語言設計者也保持和 C 語言陣列一樣的風格。

課後思考題

  1. 前面我基於陣列的原理引出 JVM 的標記清除垃圾回收演算法的核心理念,我不知道你是否使用 Java 語言,理解 JVM,如果你熟悉回顧下你理解的標記清除垃圾回收演算法。

    • 解答: 不熟悉 Java 語言。
  2. 前面我們講到一維陣列的記憶體定址公式,那你可以思考一下,類比一二維陣列的定址公式是怎樣?

    • 解答: m * n 陣列,a[i][j]_address = base_address + (i * n + j data_type_size),其中 i < m,j < n。記憶體佈局如下:

    資料結構與演算法學習-陣列

陣列相關程式設計題目

1. 實現一個支援動態擴容的陣列

// 標頭檔案 ————————————————————————————————————————————————————————
#ifndef DynamicExpansionArray_h
#define DynamicExpansionArray_h

#include <stdio.h>

typedef struct {
    int *array;// 指標陣列
    int size; // 陣列大小
}Array;

// 建立陣列
Array array_creat(int init_size);
// 釋放陣列
void array_free(Array *a);
// 獲取陣列大小
int array_size(const Array *a);

// 根據下標獲取陣列
int* array_at(Array *a, int index);

// 根據下標獲取值
int array_get(const Array *a, int index);
// 根據下標設定值
void array_set(Array *a, int index, int value);

// 陣列擴容
void array_inflate(Array *a, int more_size);


#endif /* DynamicExpansionArray_h */


// 實現檔案 ————————————————————————————————————————————————————
#include "DynamicExpansionArray.h"
#include <stdlib.h>
#include <string.h>

//typedef struct {
//    int *array;
//    int size;
//}Array;

const int BLOCK_SIZE = 20;

// 建立陣列
Array array_creat(int init_size)
{
    Array a;
    a.size = init_size;
    a.array = (int *)malloc(sizeof(int) * a.size);
    
    return a;
}

// 釋放陣列
void array_free(Array *a)
{
    free(a->array);
    a->size = 0;
    // 防止外界重複free導致崩潰,free(NULL) 是沒問題的。
    a->array = NULL;
}

// 獲取陣列大小
int array_size(const Array *a)
{
    return a->size;
}

// 返回對應index的記憶體地址
int* array_at(Array *a, int index)
{
    if (index < 0) {
        printf("下標不合法!!!!");
    }
    
    // 如果下標大於等於當前最大的size,則陣列需要擴容
    if (index >= a->size) {
        array_inflate(a, (index / BLOCK_SIZE + 1) * BLOCK_SIZE - a->size);
    }
    
    // array[index] :如果分配的是連續的記憶體空間,指標array可以像陣列一樣使用
    return &(a->array[index]);
}

// 根據下標獲取值
int array_get(const Array *a, int index)
{
    return a->array[index];
}

// 根據下標設定值
void array_set(Array *a, int index, int value)
{
    a->array[index] = value;
}

// 陣列擴容
void array_inflate(Array *a, int more_size)
{
    int *p = (int *)malloc((a->size + more_size) * sizeof(int));
    
    // memcoy,將a->array記憶體拷貝到p
    memcpy(p, a->array, sizeof(int) * a->size);
//    for (int i = 0; i < a->size; i++)
//    {
//        p[i] = a->array[i];
//    }
    
//    free(a->array);
    a->array = p;
    a->size += more_size;
}

// 測試 ——————————————————————————————————————————————————————————
#include <stdio.h>
#include "DynamicExpansionArray.h"

int main(int argc, const char * argv[]) {

    // 建立一個大小 100 陣列結構
    Array a = array_creat(10);
    int b[] = {0,1,2,3,4,5,6,7,8,9};
    a.array = b;
    
    printf("size = %d\n", array_size(&a));
    
    // 根據索引下標設定值
    *array_at(&a, 0) = 10;
    *array_at(&a, 1) = 12;
    
    // 根據索引下標取值
    int index_0_Value = *array_at(&a, 0);
    int index_1_Value = *array_at(&a, 1);
    printf("index_0_Value = %d\nindex_1_Value = %d\n",index_0_Value, index_1_Value);
    
    // 設定值
    array_set(&a, 2, 20);
    array_set(&a, 3, 21);
    
    // 測試超出陣列下標出插入,動態擴容陣列,原來陣列空間為 10,現在是120
    *array_at(&a, 101) = 101;
    int index_101_Value = *array_at(&a, 101);
    printf("index_101_Value = %d\n", index_101_Value);
    
    // 列印原來的值
    for (int i = 0; i < 10; i ++) {
        printf("---index_%d_Value %d\n", i, array_get(&a, i));
    }
    
    
    // 釋放記憶體空間
    array_free(&a);
    
    return 0;
}

// 列印日誌 ——————————————————————————————————————————————————————————
size = 10
index_0_Value = 10
index_1_Value = 12
index_101_Value = 101
---index_0_Value 10
---index_1_Value 12
---index_2_Value 20
---index_3_Value 21
---index_4_Value 4
---index_5_Value 5
---index_6_Value 6
---index_7_Value 7
---index_8_Value 8
---index_9_Value 9
Program ended with exit code: 0
複製程式碼

2. 實現一個大小固定的有序陣列,支援動態增刪改操作


// 標頭檔案 -----------------------------------------------------
#ifndef SortArray_h
#define SortArray_h

#include <stdio.h>

typedef struct {
    int size;// 陣列大小
    int used; // 陣列已經使用了多少
    int *array; // 指標
}Array;

// 根據陣列大小初始化一個陣列
Array arrayCreat(int init_size);

// 釋放空間
void arrayFree(Array *a);

// 增,在陣列末尾插入新資料
void arrayAdd(Array *a, int value);

// 刪
void arrayDelete(Array *a, int index);

// 改,修改指定下標位置的值
void arrayUpdate(Array *a, int index, int value);

#endif /* SortArray_h */


// 實現檔案 --------------------------------------------------------
#include "SortArray.h"
#include <stdlib.h>

//typedef struct {
//    int size;// 陣列大小
//    int *array; // 指標
//}Array;

// 建立固定大小陣列
Array arrayCreat(int init_size)
{
    Array a;
    a.array = (int *)malloc(init_size*sizeof(int));
    a.size = init_size;
    a.used = 0;
    
    return a;
}

// 釋放空間
void arrayFree(Array *a)
{
    free(a->array);
    a->size = 0;
    a->used = 0;
    // 防止外界重複free導致崩潰,free(NULL) 是沒問題的。
    a->array = NULL;
}

// 增,在陣列末尾插入新資料
void arrayAdd(Array *a, int value)
{
    // 先判斷陣列空間是否滿了
    if (a->used == a->size) {
        printf("新增失敗,陣列空間已滿!!!");
    } else {
        // 如果陣列為空
        if (a->used == 0) {
            a->array[a->used] = value;
        } else if (value >= a->array[a->used - 1]) {
            // 比陣列中最大的還大
            a->array[a->used] = value;
        } else {
            // 迴圈遍歷陣列中的元素,比較新加入的值是否比原來每一個元素大,大的話就往前再比
            for (int i = a->used - 1; i >= 0; i--) {
                // 將 i ~ used -1 下標都要往後移動一位
                a->array[i+1] = a->array[i];
                if (value >= a->array[i]) {
                    a->array[i + 1] = value;
                    break;
                } else {
                    if (i == 0) {
                        a->array[i] = value;
                    }
                }
            }
        }
       
        // 加入元素成功,更新used
        a->used += 1;
    }
}

// 刪,根據下標刪除一個元素
void arrayDelete(Array *a, int index)
{
    // 判斷下標是否合法
    if (index >= a->size || index < 0) {
        printf("下標不合法!!!");
    } else {
        // 從 index + 1 ~ used 位置的元素都需要向前移動
        for (int i = index + 1; i < a->used; i ++) {
            a->array[i - 1] = a->array[i];
        }
        
        // 更新used
        a->used -= 1;
    }
}

// 改,修改指定下標位置的值
void arrayUpdate(Array *a, int index, int value)
{
    // 判斷下標是否合法
    if (index >= a->used || index < 0) {
        printf("下標 = %d 不合法!!!", index);
    } else {
        if (value != a->array[index]) {
            // 先刪掉index位置的元素
            arrayDelete(a, index);
            
            // 重新把value加入進來
            arrayAdd(a, value);
        }
    }
}

// 測試 ------------------------------------------------------------------
// 1. 建立一個 10 大小的固定陣列
Array a = arrayCreat(10);
    
// 2.新增元素
printf("-----插入\n");
arrayAdd(&a, -4);
arrayAdd(&a, 8);
arrayAdd(&a, 2);
arrayAdd(&a, 19);
arrayAdd(&a, 4);
arrayAdd(&a, 78);
arrayAdd(&a, 100);
arrayAdd(&a, 11);
arrayAdd(&a, 12);
arrayAdd(&a, 5);
// 插入失敗
arrayAdd(&a, 10);
    
printf("\n");
    
// 列印陣列元素
for (int i = 0; i < a.used; i++) {
    printf("a[%d] = %d\n", i, a.array[i]);
}
    
// 3. 刪除
printf("-----刪除\n");
arrayDelete(&a, 0);
arrayDelete(&a, 1);
arrayDelete(&a, 2);
arrayDelete(&a, 3);
arrayDelete(&a, 4);
printf("\n");
    
for (int i = 0; i < a.used; i++) {
    printf("a[%d] = %d\n", i, a.array[i]);
}
    
// 4. 修改
printf("-----修改\n");
arrayUpdate(&a, 0, 10);
arrayUpdate(&a, 5, 50);
printf("\n");
    
for (int i = 0; i < a.used; i++) {
    printf("a[%d] = %d\n", i, a.array[i]);
}
    
// 釋放空間
arrayFree(&a);

// 列印日誌 --------------------------------------------------------------
-----插入
新增失敗,陣列空間已滿!!!
a[0] = -4
a[1] = 2
a[2] = 4
a[3] = 5
a[4] = 8
a[5] = 11
a[6] = 12
a[7] = 19
a[8] = 78
a[9] = 100
-----刪除

a[0] = 2
a[1] = 5
a[2] = 11
a[3] = 19
a[4] = 100
-----修改
下標 = 5 不合法!!!
a[0] = 5
a[1] = 10
a[2] = 11
a[3] = 19
a[4] = 100
Program ended with exit code: 0
複製程式碼

3. 實現兩個有序陣列合併為一個有序陣列


// 合併兩個有序陣列
Array mergeSortArray(const Array *a, const Array *b)
{
    Array p;
    // 申請記憶體空間
    p.array = (int *)malloc(sizeof(int) * (a->used + b->used));
    p.size = a->used + b->used;
    p.used = a->used + b->used;
	
	// 長短陣列同時遍歷,如果短陣列遍歷完了,剩下的就是長陣列裡面的資料,直接加上去就行,前面的資料都已經排好序了
    int i = 0, j = 0, k = 0;
    while (i < a->used && j < b->used) {
        if (a->array[i] <= b->array[j]) {
            p.array[k++] = a->array[i++];
        } else {
            p.array[k++] = b->array[j++];
        }
    }
    
    while (i < a->used) {
        p.array[k++] = a->array[i++];
    }
    
    while (j < b->used) {
        p.array[k++] = b->array[j++];
    }
    
    return p;
}

//測試 --------------------------------------------------------
Array a = arrayCreat(10);
Array b = arrayCreat(10);
    
printf("-----a陣列插入資料\n");
arrayAdd(&a, 20);
arrayAdd(&a, 93);
arrayAdd(&a, 3);
arrayAdd(&a, 43);
arrayAdd(&a, 65);
    
for (int i = 0; i < a.used; i++) {
    printf("a[%d] = %d\n", i, a.array[i]);
}
    
printf("-----b陣列插入資料\n");
arrayAdd(&b, 100);
arrayAdd(&b, 125);
arrayAdd(&b, 34);
arrayAdd(&b, 2);
arrayAdd(&b, 11);
arrayAdd(&b, 19);
arrayAdd(&b, 78);
arrayAdd(&b, 89);
    
for (int i = 0; i < b.used; i++) {
    printf("b[%d] = %d\n", i, b.array[i]);
}
    
printf("陣列合並\n");
Array c = mergeSortArray(&a,  &b);
for (int i = 0; i < c.used; i++) {
    printf("c[%d] = %d\n", i, c.array[i]);
}
    
arrayFree(&a);
arrayFree(&b);

// 列印日誌 ----------------------------------------------------
-----a陣列插入資料
a[0] = 3
a[1] = 20
a[2] = 43
a[3] = 65
a[4] = 93
-----b陣列插入資料
b[0] = 2
b[1] = 11
b[2] = 19
b[3] = 34
b[4] = 78
b[5] = 89
b[6] = 100
b[7] = 125
陣列合並
c[0] = 2
c[1] = 3
c[2] = 11
c[3] = 19
c[4] = 20
c[5] = 34
c[6] = 43
c[7] = 65
c[8] = 78
c[9] = 89
c[10] = 93
c[11] = 100
c[12] = 125
Program ended with exit code: 0
複製程式碼

分享個人技術學習記錄和跑步馬拉松訓練比賽、讀書筆記等內容,感興趣的朋友可以關注我的公眾號「青爭哥哥」。

資料結構與演算法學習-陣列

相關文章