09-STL 容器 - vector

9-1 vector

1. 為什麼需要 vector?

1.1 傳統陣列的限制

在 C++ 中,我們熟悉的傳統陣列有一個根本的問題:大小必須在編譯時決定,且無法改變

int scores[100];   // 固定 100 格,多了浪費,少了不夠用

傳統陣列的痛點:

// 傳統做法:又長又容易出錯
int* scores = new int[100];  // 動態跟作業系統要一塊記憶體
int count = 0;

scores[count++] = 90;  // 注意這裡的 count++,count 值先被評估,再遞增 1
scores[count++] = 85;

cout << "We have " << count << " items in scores[]" << endl;

for(int i=0; i<count; i++)
{
    cout << "scores[" << i << "] = " << scores[i] << endl;
}

// 用完還要記得把記憶體還作業系統
delete[] scores;

1.2 vector 是什麼?

vector 是 C++ 標準模板庫(STL) 中的一種動態陣列容器

它的核心優勢:

1.3 圖解:記憶體動態擴張

初始狀態(capacity = 1):
┌───┐
│ 3 │
└───┘
  size=1, capacity=1

push_back(7),空間不足 → 自動擴張為 capacity=2:
┌───┬───┐
│ 3 │ 7 │
└───┴───┘
  size=2, capacity=2

push_back(5),空間不足 → 自動擴張為 capacity=4:
┌───┬───┬───┬───┐
│ 3 │ 7 │ 5 │   │
└───┴───┴───┴───┘
  size=3, capacity=4(預留空間)

push_back(1),空間足夠 → 直接寫入:
┌───┬───┬───┬───┐
│ 3 │ 7 │ 5 │ 1 │
└───┴───┴───┴───┘
  size=4, capacity=4

重點:size 是目前元素數量,capacity 是已分配的記憶體空間。vector 通常以倍增方式擴張,以攤平擴張的成本。

9-2 vector 的基礎操作

vector 基礎操作

1. 引入標頭檔與宣告

使用 vector 前,需要引入標頭檔:

#include <iostream>
#include <vector>

using namespace std;

2. 宣告與初始化

// 方式一:宣告空的 vector
// capacity:0, size: 0
vector<int> v1;

// 方式二:指定初始大小(元素值為 0)
// capacity:5, size: 5
vector<int> v2(5);          // [0, 0, 0, 0, 0]

// 方式三:指定大小與預設值
// capacity:5, size: 5
vector<int> v3(5, 100);     // [100, 100, 100, 100, 100]

// 方式四:用初始化列表
// capacity:5, size: 5
vector<int> v4 = {3, 1, 4, 1, 5};  // [3, 1, 4, 1, 5]

// 方式五:複製另一個 vector
// capacity:5, size: 5
vector<int> v5(v4);         // [3, 1, 4, 1, 5]

// 也可以存其他型別
vector<double> scores;
vector<string> names;
vector<bool>   flags;

3. 查詢大小與清空

vector<int> v = {1, 2, 3, 4, 5};

cout << v.size();    // 5:目前元素個數
cout << v.empty();   // 0(false):是否為空

v.clear();           // 清空所有元素
cout << v.size();    // 0
cout << v.empty();   // 1(true)

4. 新增與移除元素

在末尾新增元素

vector 的成員函數 push_back(val),可以將一個值新增到目前 vector 的尾端。

vector<int> v;
v.push_back(10);   // [10]
v.push_back(20);   // [10, 20]
v.push_back(30);   // [10, 20, 30]

移除末尾元素

vector 的成員函數 pop_back(),可以將 vector 尾端的那個元素移除。

v.pop_back();      // [10, 20]

**注意**:對空的 vector 呼叫 pop_back() 是未定義行為(UB),請先確認 !v.empty()。

5. 存取元素

下標運算子 []

vector<int> v = {10, 20, 30};
cout << v[0];   // 輸出 10
v[1] = 99;      // 修改第二個元素

帶邊界檢查的存取 at(i)

cout << v.at(0);   // 輸出 10
cout << v.at(5);   // 超出範圍 → 丟出 std::out_of_range 例外
方式 速度 邊界檢查 建議使用時機
v[i] 較快 ❌ 沒有 確定索引合法時
v.at(i) 稍慢 ✅ 有 需要安全保障時

6. 範例:動態收集成績

#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> scores;
    int n, score;

    cout << "請輸入學生人數:";
    cin >> n;

    for (int i = 0; i < n; i++)
    {
        cout << "輸入第 " << i + 1 << " 位學生成績:";
        cin >> score;
        scores.push_back(score);   // 動態新增
    }

    // 計算總分
    int total = 0;
    for (int i = 0; i < scores.size(); i++) {
        total += scores[i];
    }

    cout << "平均分數:" << (double)total / scores.size() << endl;
    return 0;
}

9-3 走訪 vector

走訪 vector

1. 方法一:下標 for 迴圈

最直覺的方式,適合需要使用索引時:

vector<int> v = {10, 20, 30, 40, 50};

for (int i = 0; i < v.size(); i++) {
    cout << v[i] << " ";
}
// 輸出:10 20 30 40 50

v.size() 回傳型別為 size_t(無號整數),若用 int i = v.size() - 1 倒著走時要小心,空 vector 的 0 - 1 會變成一個很大的數。

2. 方法二:Range-based for(範圍式 for)

C++11 起支援,最簡潔:

// 唯讀走訪
for (int x : v) {
    cout << x << " ";
}

// 修改元素(加上 &)
for (int& x : v) {
    x *= 2;   // 每個元素乘以 2
}

3. 方法三:迭代器(Iterator)

迭代器的概念類似指標,這也是為什麼熟悉指標的你能很快上手。

圖解:迭代器與指標的對比

傳統陣列指標:
  int arr[] = {10, 20, 30};
  int* ptr = arr;       // ptr 指向第一個元素

  ┌────┬────┬────┐
  │ 10 │ 20 │ 30 │
  └────┴────┴────┘
    ↑
   ptr

  ptr++;                // 指向下一個
  *(ptr) = 99;          // 解參考後修改

vector 迭代器:
  vector<int> v = {10, 20, 30};
  auto it = v.begin();  // it 指向第一個元素

  ┌────┬────┬────┬──────┐
  │ 10 │ 20 │ 30 │(end) │
  └────┴────┴────┴──────┘
    ↑                ↑
  begin()           end()(指向最後一個之後)

  it++;             // 指向下一個(同 ptr++)
  *it = 99;         // 解參考後修改(同 *ptr)

迭代器的基本用法

vector<int> v = {10, 20, 30, 40, 50};

// 傳統的寫法,宣告 iterator 的型別有點複雜
// vector<int>::iterator it = v.begin();

// 自 C++ 11 起可以用 auto 自動推導型別
auto it = v.begin();    // 指向第一個元素

while (it != v.end()) {
    cout << *it << " ";  // 解參考取值
    it++;                // 移動到下一個
}
// 輸出:10 20 30 40 50

更常見的寫法(for 迴圈):

for (auto it = v.begin(); it != v.end(); it++) {
    cout << *it << " ";
}

三種走訪方式比較

方式 優點 缺點
下標 v[i] 直覺,可使用索引 需要注意型別
range-based for 最簡潔 無法直接取得索引
迭代器 與 STL 演算法相容 語法略繁瑣

實務建議:平常用 range-based for,需要插入/刪除時用迭代器,需要索引時用下標。

9-4 常用成員函數

其他常用成員函數

1. front() 與 back()

vector<int> v = {10, 20, 30, 40, 50};

cout << v.front();  // 10:第一個元素
cout << v.back();   // 50:最後一個元素

v.front() = 99;     // 可以修改
v.back()  = 1;
// v 現在是 {99, 20, 30, 40, 1}

2. insert():在指定位置插入

vector<int> v = {1, 2, 3, 4, 5};

// insert(位置迭代器, 值)
auto it = v.begin() + 2;   // 指向索引 2(值為 3)
v.insert(it, 99);

// v 現在是 {1, 2, 99, 3, 4, 5}

圖解:

插入前:
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │
└───┴───┴───┴───┴───┘
          ↑
         it(索引 2)

插入後(後面的元素全部後移):
┌───┬───┬────┬───┬───┬───┐
│ 1 │ 2 │ 99 │ 3 │ 4 │ 5 │
└───┴───┴────┴───┴───┴───┘

insert() 後,原本的迭代器可能**失效**,請重新取得。

3. erase():移除指定位置的元素

vector<int> v = {1, 2, 99, 3, 4, 5};

// 移除單一元素
v.erase(v.begin() + 2);   // 移除索引 2(值 99)
// v 現在是 {1, 2, 3, 4, 5}

// 移除一個範圍 [begin+1, begin+3)(左閉右開)
v.erase(v.begin() + 1, v.begin() + 3);
// 移除索引 1 和 2(值 2 和 3)
// v 現在是 {1, 4, 5}

4. resize() 與 reserve()

這兩個函數常被搞混,用下圖區分:

resize(n):改變 size(元素個數)

vector<int> v = {1, 2, 3};  size=3, capacity=3

v.resize(5);
→ {1, 2, 3, 0, 0}           size=5, capacity=5(新元素填 0)

v.resize(2);
→ {1, 2}                    size=2, capacity=5(縮小 size,不影響 capacity)

reserve(n):改變 capacity(預留記憶體)

vector<int> v = {1, 2, 3};  size=3, capacity=3

v.reserve(10);
→ {1, 2, 3}                 size=3, capacity=10(只預留空間,不新增元素)

何時用 reserve?

當你事先知道大概會新增多少元素時,使用 reserve 可以避免多次重新分配記憶體,提升效能:

vector<int> v;
v.reserve(1000);          // 預先分配 1000 個空間

for (int i = 0; i < 1000; i++) {
    v.push_back(i);       // 不會觸發記憶體重新分配
}

5. capacity():查詢已分配空間

vector<int> v;
cout << v.size()     << endl;   // 0
cout << v.capacity() << endl;   // 0(或平台相關的初始值)

v.push_back(1);
cout << v.capacity() << endl;   // 1(或更大,依平台)

v.push_back(2);
cout << v.capacity() << endl;   // 2(擴張)

v.push_back(3);
cout << v.capacity() << endl;   // 4(擴張為倍數)

6. 二維 vector

vector 可以嵌套,用來表示矩陣或表格:

// 宣告 3x4 的二維 vector,初始值全為 0
vector<vector<int>> matrix(3, vector<int>(4, 0));

圖解:

matrix:
        [0]  [1]  [2]  [3]
[0] ──► ┌────┬────┬────┬────┐
        │ 0  │ 0  │ 0  │ 0  │
        └────┴────┴────┴────┘
[1] ──► ┌────┬────┬────┬────┐
        │ 0  │ 0  │ 0  │ 0  │
        └────┴────┴────┴────┘
[2] ──► ┌────┬────┬────┬────┐
        │ 0  │ 0  │ 0  │ 0  │
        └────┴────┴────┴────┘
// 存取與修改
matrix[1][2] = 99;

// 走訪二維 vector
for (int i = 0; i < matrix.size(); i++) {
    for (int j = 0; j < matrix[i].size(); j++) {
        cout << matrix[i][j] << " ";
    }
    cout << endl;
}

// 使用 range-based for
for (auto& row : matrix) {
    for (int val : row) {
        cout << val << " ";
    }
    cout << endl;
}

注意:二維 vector 的每一列長度可以不同(鋸齒狀陣列),這與傳統二維陣列不同。

9-5 實作練習

練習一:成績管理系統(基礎操作)

題目敘述

請你撰寫一個程式,功能如下:

  1. 讀入 N 筆學生成績(整數,0~100)
  2. 輸出所有成績
  3. 輸出最高分、最低分、平均分數(取到小數點後兩位)
  4. 刪除最後一筆成績後,再輸出一次所有成績

範例輸入

5
90 75 88 62 95

範例輸出

成績:90 75 88 62 95
最高分:95
最低分:62
平均:82.00
刪除最後一筆後:90 75 88 62

提示

參考解答(請先自己嘗試!)
#include <vector>
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    int n;
    cin >> n;

    vector<int> scores;
    for (int i = 0; i < n; i++) {
        int s;
        cin >> s;
        scores.push_back(s);
    }

    // 輸出所有成績
    cout << "成績:";
    for (int s : scores) cout << s << " ";
    cout << endl;

    // 找最大最小
    int maxVal = scores[0], minVal = scores[0];
    int total = 0;
    for (int s : scores) {
        if (s > maxVal) maxVal = s;
        if (s < minVal) minVal = s;
        total += s;
    }

    cout << "最高分:" << maxVal << endl;
    cout << "最低分:" << minVal << endl;
    cout << fixed << setprecision(2);
    cout << "平均:" << (double)total / scores.size() << endl;

    scores.pop_back();

    cout << "刪除最後一筆後:";
    for (int s : scores) cout << s << " ";
    cout << endl;

    return 0;
}

練習二:移除特定元素(insert / erase)

題目敘述

給定一個整數序列,請你:

  1. 讀入 N 個整數
  2. 讀入一個目標值 K
  3. 移除序列中所有等於 K 的元素
  4. 在序列開頭插入一個數字 0
  5. 輸出最終序列

範例輸入

8
1 3 5 3 2 3 7 9
3

範例輸出

0 1 5 2 7 9

提示

常見陷阱

// ❌ 錯誤寫法:erase 後 it 已失效,不可 it++
for (auto it = v.begin(); it != v.end(); it++) {
    if (*it == K) v.erase(it);
}

// ✅ 正確寫法:erase 回傳下一個有效迭代器
for (auto it = v.begin(); it != v.end(); ) {
    if (*it == K)
        it = v.erase(it);  // 不 it++
    else
        it++;
}
參考解答(請先自己嘗試!)
#include <vector>
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    vector<int> v;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        v.push_back(x);
    }

    int K;
    cin >> K;

    // 移除所有等於 K 的元素
    for (auto it = v.begin(); it != v.end(); ) {
        if (*it == K)
            it = v.erase(it);
        else
            it++;
    }

    // 在開頭插入 0
    v.insert(v.begin(), 0);

    // 輸出
    for (int x : v) cout << x << " ";
    cout << endl;

    return 0;
}

練習三:矩陣加法(二維 vector)

題目敘述

給定兩個 N×M 的矩陣 A 和 B,請計算 C = A + B,並輸出結果矩陣 C。

範例輸入

2 3
1 2 3
4 5 6
7 8 9
1 0 1

(第一行為 N M,接下來 N 行為矩陣 A,再 N 行為矩陣 B)

範例輸出

8 10 12
5 5 7

提示

參考解答(請先自己嘗試!)
#include <vector>
#include <iostream>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;

    vector<vector<int>> A(n, vector<int>(m));
    vector<vector<int>> B(n, vector<int>(m));
    vector<vector<int>> C(n, vector<int>(m, 0));

    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            cin >> A[i][j];

    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            cin >> B[i][j];

    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            C[i][j] = A[i][j] + B[i][j];

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cout << C[i][j];
            if (j < m - 1) cout << " ";
        }
        cout << endl;
    }

    return 0;
}

📌 重點總覽

常用成員函數速查表

函數 功能 時間複雜度
push_back(val) 在末尾新增元素 O(1) 均攤
pop_back() 移除末尾元素 O(1)
v[i] 存取索引 i 的元素(無檢查) O(1)
v.at(i) 存取索引 i 的元素(有檢查) O(1)
size() 回傳元素個數 O(1)
empty() 是否為空 O(1)
clear() 清空所有元素 O(n)
front() 第一個元素 O(1)
back() 最後一個元素 O(1)
insert(it, val) 在迭代器位置前插入 O(n)
erase(it) 移除迭代器位置的元素 O(n)
resize(n) 調整元素個數 O(n)
reserve(n) 預留記憶體空間 O(n)
capacity() 已分配的空間大小 O(1)
begin() 指向第一個元素的迭代器 O(1)
end() 指向最後一個之後的迭代器 O(1)

三大常見陷阱

  1. erase 後迭代器失效:請使用 it = v.erase(it) 取得新的有效迭代器。
  2. pop_back 空 vector:呼叫前先確認 !v.empty()
  3. size() 型別為 size_t:比較或相減時注意無號整數的行為。