傳統陣列

極為常見

為什麼需要陣列

在程式設計中,我們常常需要儲存大量的資料,而一般變數通常只能儲存單一個數值。如果我們需要儲存多個數值,就需要使用陣列了。你可以把陣列想像成是一個「列表」或「清單」,並且可以根據「第幾項」來存取或修改數值。

舉個例子來說,假設你考了很多次 APCS,希望把每次考試的成績都存在程式裡面,該怎麼儲存呢?按照過去的變數使用方法,也許你會這樣宣告:

int apcs_01 = 3; // 第一次 APCS 考試成績為 3 級
int apcs_02 = 2; // 第二次 APCS 考試成績為 2 級
int apcs_03 = 3; // 第三次 APCS 考試成績為 3 級
int apcs_04 = 5; // 第四次 APCS 考試成績為 5 級

這樣的寫法雖然可以達到目的,但是當考試次數變多時,我們就需要宣告更多的變數,這樣的寫法會讓程式碼變得冗長且難以維護。並且,當我們需要查詢特定的考試成績時,查詢的方式也會變得很麻煩。

int n;
cin >> n;
if (n == 1) {
    cout << apcs_01;
} else if (n == 2) {
    cout << apcs_02;
} else if (n == 3) {
    cout << apcs_03;
} else if (n == 4) {
    cout << apcs_04;
}

為了解決這個問題,我們需要學習陣列這個資料結構。

陣列的概念

以生活中的例子來說,陣列就像是置物櫃,每個格子都有一個編號,你可以透過編號來存取或修改裡面的物品。

以下圖為例,圖中的「索引值」就是陣列中的「編號」,編號 0 對應的是第一次考試的成績 3 級,編號 1 對應的是第二次考試的成績 2 級,以此類推。

img

比較值得注意的部分是,在程式中,陣列的索引值是從 0 開始的,這點和一般常見的編號方式稍微不太一樣。

陣列宣告

要宣告一個陣列非常簡單,寫法跟變數很類似。

int apcs[4];

我們只需要在變數名稱後面加上一組中括號 [],並在中括號中填入陣列的大小即可。舉例來說你只要儲存 4 次 APCS 考試的成績,就需要四格儲存空間,像上面那樣。

但是我們要怎麼決定陣列的大小呢?一般來說,我們會直接開到我們需要的最大值,比如題目說明中有提到考試次數不會超過 100 次,那我們就可以開到 105 的空間,為什麼是 105 呢?那是因為有時候題目操作會容易超過陣列大小一點,所以我們通常會多開一點空間來盡量避免這個問題。

並且,main 函式中的最大陣列限制比較小,所以如果你要開一個很大的陣列(通常是 10410^4 以上),建議你在 main 函式外宣告。

int apcs[105];

int main() {
    // 你的程式碼
}

並且,main 函式之外宣告的陣列會有一個特性,就是裡面的數值會被自動初始化為 0,反之,main 函式中宣告的陣列如果沒有初始化,裡面的數值可能會是隨機的,這點要特別注意。

補充

如果需要的話,你也可以使用變數來宣告陣列的大小。

int n;
cin >> n;
int apcs[n];

但是這樣的寫法在一些編譯器上可能會有問題,所以建議在寫程式時直接填入數值,如果真的需要使用動態大小的陣列,可以使用後面章節提到的 vector

以上的舉例皆使用 int 類型,然而其他類型也可以開陣列,像是我們可以宣告一個浮點數陣列:

float apcs[105];

apcs 這個變數本身其實就是一個指向陣列開頭的指標,所以你可以把它當作一個指標來使用,後面會有更多的說明。

陣列初始化

宣告完陣列,我們還不知道現在陣列中的數值是什麼。就如同路邊的一排房子,你並不知道他們是否曾經住人,以及住過誰;同樣的道理你並不知道陣列宣告的範圍中是否為空,他可能包含之前執行的遺留數值,是一個你不可控的數值。所以我們要透過初始化去覆寫他。

float apcs[4] = {};

透過一個大括號可以初始化一個空值給陣列,每個位置都會是 0。

補充

或許你會看在其他地方看到這樣的寫法:

int apcs[4] = {0};

即便這兩種寫法是等價的,都是將陣列中的所有元素填入 0,但我們認為這種寫法容易誤導初學者。

事實上,上面這個作法是將陣列中的第一個元素填入 0,而其他的元素則會被自動初始化為 0。如果沒搞清楚這個概念的話,有些初學者可能會寫出這樣的東西,並以為可以把整個陣列初始化為 5:

int apcs[4] = {5};

但事實上這樣只會把第一個元素填入 5,其他的元素則會被自動初始化為 0。

因此,我們建議使用 {} 這種寫法,以避免這種誤解。

float apcs[4] = {3, 2, 3, 5};

也可以直接在大括號中填上數值,直接初始數值給陣列。

陣列存取

陣列的存取是透過一個叫「隨機存取」的概念來執行,是一種快速又準確的定位方法。簡單來說,你只需要在陣列名稱後面加上中括號,並在中括號中填入索引值即可取得或修改陣列中的數值。

float apcs[4] = {3, 2, 3, 5};
cout << apcs[3];
隨機存取的概念

在底層的實作中,陣列的資料是連續的,所以我們可以透過索引值來計算出數值的位置。比如說在 int 陣列中,一個 int 型態會占 4 bytes ,如果我們要存取陣列的第 3 個元素,它的位置就會是從陣列開頭往後 4×3=124 \times 3 = 12 的位置。

在這個程式中我們於大括號中填入 3,代表程式存取陣列中索引值 3 中的數值。而陣列的索引值是從 0 開始,也就是在這個例子中索引值是從 0 ~ 3,最後一個數值索引是 3,所以會輸出 5。

注意

再次提醒,陣列索引值是從 0 開始,而不是 1

而其他的操作也是一樣的概念:

// 輸入數值給陣列
cin >> apcs[3];
// 更新陣列數值
apcs[2] = 5;
// 將數值賦值給其他變數
score = apcs[3];
// 判斷陣列的數值
if (apcs[3] == 5) {
    cout << "great!" << endl;
}   

陣列與迴圈

當我們需要對陣列中的所有數值進行操作時,我們通常會使用 迴圈 來達成。透過迴圈,我們可以快速且簡潔地對陣列中的所有數值進行操作。

以下是一個簡單的例子,透過迴圈將陣列中的所有數值輸出:

int apcs[4] = {3, 2, 3, 5};

for (int i = 0; i < 4; i++) {
    cout << apcs[i] << " ";
}

在輸入時,我們可以透過迴圈將數值一一存入陣列。假設我們要輸入四次 APCS 考試的成績,就可以這樣寫:

for (int i = 0; i < 4; i++) {
    cin >> apcs[i];
}

好用的函式

在 C++ 中,有許多好用的函式可以讓我們更方便地操作陣列。以下是一些常用的函式:

  • reverse(begin, end):將陣列中的元素反轉。
  • max_element(begin, end):找出陣列中的最大值。
  • min_element(begin, end):找出陣列中的最小值。
  • fill(begin, end, value):將陣列中的所有元素填入相同的值。
  • count(begin, end, value):計算陣列中某個值出現的次數。
  • find(begin, end, value):找出陣列中第一個出現的某個值。
  • copy(begin, end, dest):將陣列中的元素複製到另一個陣列。

這些函式都可以在 algorithm 標頭檔中找到,使用時需要加上 #include <algorithm>,當然如果你已經使用了 bits/stdc++.h,就不需要再加入這個標頭檔了。

接下來我們會一一介紹這些函式的使用方法。

補充

或許你不知道「函式」是什麼,不過你可以暫時先把它當作是一個「功能」或「語法」,在後面的章節中我們會更詳細地介紹函式這個概念。

找出陣列中的最大值

這個 max_element 函式可以幫助我們找出陣列中的最大值,回傳的是一個 Iterator ,所以我們需要透過 * 來取得數值,並且要傳入兩個參數,分別是陣列的開頭與結尾(型態為 Iterator)。

int apcs[4] = {3, 2, 3, 5};

// 找出陣列中的最大值
int max_score = *max_element(apcs, apcs + 4);
cout << max_score << endl;

找出陣列中的最小值

這個 min_element 函式可以幫助我們找出陣列中的最小值,回傳的是一個 Iterator ,所以我們需要透過 * 來取得數值,並且需要傳入兩個參數,分別是陣列的開頭與結尾(型態為 Iterator)。

int apcs[4] = {3, 2, 3, 5};

// 找出陣列中的最小值
int min_score = *min_element(apcs, apcs + 4);
cout << min_score << endl;

將陣列中的元素填入相同的值

這個 fill 函式可以幫助我們將陣列中的所有元素填入相同的值,要傳入三個參數,分別是陣列的開頭、結尾以及要填入的值,開頭與結尾的型態都是 Iterator

int apcs[4] = {3, 2, 3, 5};

// 將陣列中的所有元素填入 0
fill(apcs, apcs + 4, 0);

for (int i = 0; i < 4; i++) {
    cout << apcs[i] << " ";
}

將陣列中的元素反轉

reverse 函式可以幫助我們將陣列中的元素反轉,要傳入兩個參數,分別是陣列的開頭與結尾,開頭與結尾的型態都是 Iterator。這個函式不會回傳任何值,會直接修改陣列中的元素。

int apcs[4] = {3, 2, 3, 5};

// 將陣列中的元素反轉
reverse(apcs, apcs + 4);

// 輸出反轉後的陣列
for (int i = 0; i < 4; i++) {
    cout << apcs[i] << " ";
}

計算陣列中某個值出現的次數

count 函式可以幫助我們計算陣列中某個值出現的次數,要傳入三個參數,分別是陣列的開頭、結尾以及要計算的值,開頭與結尾的型態都是 Iterator

int apcs[4] = {3, 2, 3, 5};

// 計算陣列中 3 出現的次數
int count_3 = count(apcs, apcs + 4, 3);
cout << count_3 << endl; // 2

找出陣列中第一個出現的某個值

find 函式可以幫助我們找出陣列中第一個出現的某個值,要傳入三個參數,分別是陣列的開頭、結尾以及要找的值,開頭與結尾的型態都是 Iterator。這個函式會回傳一個 Iterator ,所以我們需要透過 * 來取得數值。如果找不到指定的值,則會回傳結尾的 Iterator ,在這個例子中結尾的 Iterator 就是 apcs + 4

int apcs[4] = {3, 2, 3, 5};

// 找出陣列中第一個出現的 2
int* it = find(apcs, apcs + 4, 2);

if (it != apcs + 4) {
    cout << *it << endl; // 2 (數值)
    cout << it - apcs << endl; // 1 (索引值)
} else {
    cout << "Not found" << endl;
}

將陣列中的元素複製到另一個陣列

copy 函式可以幫助我們將陣列中的元素複製到另一個陣列,要傳入三個參數,分別是原陣列的開頭、結尾以及目標陣列的開頭,開頭與結尾的型態都是 Iterator

int apcs[4] = {3, 2, 3, 5};
int copy_apcs[4];

// 將 apcs 中的元素複製到 copy_apcs
copy(apcs, apcs + 4, copy_apcs);

for (int i = 0; i < 4; i++) {
    cout << copy_apcs[i] << " ";
}

小測驗

陣列索引值是從多少開始?