語法書 / AA 競程語法書 下冊 / 第十單元 / vector:長度可變的陣列

10.2 vector:長度可變的陣列

vector 定義在標頭檔 <vector> 裡。宣告時跟基本型態多一件事:要用角括號指定「這個櫃子裝什麼型態」。角括號指的就是 <> 這對符號——單獨出現時是比較大小的「小於、大於」,成對包住東西時就當括號用;你每天寫的 #include <iostream> 裡包住函式庫名稱的正是它(上冊 1.1 第一次見過它):

vector<int> v;        // 一個裝 int 的 vector,一開始是空的
vector<double> w;     // 元素型態隨你指定

注意型態名稱是 vector<int> 整個合起來——角括號填進去之後才算完整,vector 三個字單獨存在不是型態。而 vector<int> 的地位跟 intdouble 完全相同:宣告句型一樣是「型態 變數名稱;」,之後也能當函式的參數與回傳值,甚至(10.3 會看到)當別的容器的元素。像這樣由函式庫提供的現成型態,到第十三單元你還會學到怎麼自己造

可以把 vector 當成「更厲害的陣列」:常用的索引、遍歷寫法都一樣,而它多了一項超能力——長度可以隨時改變

範例程式碼

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {4, 1, 2};     // 一開始有 3 個元素:v[0] 是 4、v[1] 是 1、v[2] 是 2
    cout << v[0] << ' ' << v[1] << ' ' << v[2] << '\n';

    v[1] = 3;                      // 跟陣列一樣用索引修改
    v.push_back(6);                // 在尾端多加一個元素 6:長度從 3 變 4!
    cout << v[0] << ' ' << v[1] << ' ' << v[2] << ' ' << v[3] << '\n';
    return 0;
}

執行結果:

4 1 2
4 3 2 6

取值、修改都跟陣列一模一樣。新面孔是 v.push_back(6):意思是「在 v 的尾端放進一個新元素 6」——原本只有索引 0 \sim 2,這行之後索引 3 就存在了。

兩行輸出把每個元素都印了出來,變化一目了然:v[1]1 變成 3、尾端多了一個 6,其他格完全沒被動到。

三種初始化

vector<int> a;               // 空的 vector:長度 0,之後再慢慢加
vector<int> b = {4, 1, 2};   // 大括號給初始內容:長度 3
vector<int> c(10);           // 長度 10,每格自動補 0
vector<int> d(5, -1);        // 長度 5,每格都是 -1
  • 什麼都不給:得到一個空的 vector,長度 0,之後靠 push_back 慢慢加。
  • = {...}:直接列出初始內容,跟上冊 6.5 的陣列初始化長得一樣。等號其實可以省略:vector<int> b {4, 1, 2}; 意思完全相同。
  • (長度, 初始值):指定初始長度,每一格都填成初始值;初始值省略時自動補 0vector<int> c(10); 就是 10 格的 0)。「補 0」是數字型態的情況——省略初始值時,每格其實是用元素型態自己的預設值vector<string> c(10); 得到的就是 10 格空字串。

先猜再跑vector<int> a(5);vector<int> b{5};——a.size()b.size()a[0]b[0] 各是多少?先寫下答案,再貼進編輯器印出來對。

重點來了:那個「長度」可以是變數。上冊 6.1 說陣列大小必須是常數,vector 沒有這個限制——從此可以「要多少開多少」。

拿一個最常見的題目來看:第一行給一個整數 n,第二行給 n 個整數,請輸出這 n 個數的總和

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;                      // 長度是執行時才知道的
    vector<int> v(n);              // 陣列辦不到的事:拿變數當大小
    for (int i = 0; i < n; i++) {
        cin >> v[i];               // 讀入方式跟陣列一模一樣
    }

    int total = 0;                 // total 用來累加總和,一開始是 0
    for (int i = 0; i < n; i++) {
        total += v[i];             // 把每個元素一個一個加進 total
    }
    cout << total << '\n';         // 全部加完,印出總和
    return 0;
}

執行結果(輸入 43 1 4 1):

9

這支程式做的事拆成三步:先讀進 n、開一個剛好 n 格的 vector 把 n 個數字讀進去,再用一個迴圈從頭到尾把每個元素累加到 total,最後印出來。範例輸入的四個數字是 3 + 1 + 4 + 1 = 9,所以答案是 9

累加的寫法跟上冊 6.4 用陣列做總和時一模一樣——換成 vector 之後,唯一的差別只有「開多大」這件事:陣列要事先寫死一個上限,vector 直接拿讀進來的 n 開。(這裡的數字小,totalint 就夠;真正的題目資料量大時,總和很容易超過 int 的範圍,記得照上冊 6.4 的習慣改用 long long。)

vector 的抽象理解

vector 到底是什麼?照下面這樣想就夠了:

  • 它維護一列序列,所有元素的型態相同。
  • 元素住在連續的記憶體裡(跟上冊 6.1 的陣列同款)。
  • 給定索引值,一步(O(1) 就能取得或修改該處的元素。
  • 序列的長度可以改變
  • 長度的增減在尾端最快最順;中間其實也能插入移除(10.5 會教),但後方元素得整段搬動。

這叫「抽象理解」——電腦實際做的事不保證跟上面一模一樣(例如它會偷偷多預留一些空間,免得每次變長都要整批搬家),但照這樣想,你就會用 vector,也能正確估計每種操作的快慢。

圖 10-1:vector 的抽象理解——長度可變的連續格子

(小例外先打個招呼:vector<bool> 是標準函式庫的特殊版本,行為跟本節的模型不完全一樣——初學需要「是/否」標記陣列時,用 vector<int> 就好。)

成員函式:掛在變數身上的操作

本節開頭用過的 v.push_back(6),寫法是「變數名稱+一個點+函式呼叫」。這種掛在某個變數身上的函式叫成員函式(member function)

變數名稱.成員函式名稱(參數)

它跟上冊第七單元的一般函式差在「歸屬」:一般函式(像 abs(x))獨立存在,誰都能直接呼叫;成員函式屬於容器型態本身,必須透過某個容器變數來呼叫,動的也就是那個變數本人——v.push_back(6) 的意思就是「對 v 做 push_back」。

vector 常用的成員函式一次列齊(快慢標籤是 10.1O(\cdot) 記號):

寫法 功能 快慢
v[i]v.at(i) 取得或修改索引 i 的元素 O(1)
v.push_back(x) 在尾端加入元素 x 均攤 O(1)(見表後說明)
v.emplace_back(x) push_back(見表後說明) 均攤 O(1)
v.pop_back() 移除尾端元素 O(1)
v.front()v.back() 取得或修改第一個/最後一個元素 O(1)
v.size() 回傳目前的元素個數 O(1)
v.empty() 回傳是否為空(空回傳 true O(1)
v.clear() 清空所有元素 O(n)
v.assign(n, x) 整個重設:長度 n、每格都是 xx 不可省略) O(n)
v.resize(n, x) 長度改成 n:變短截尾、變長補 xx 可省略,預設 0 與長度改變量成正比

完整清單可查 cppreference 的 vector 頁面

別人的程式碼裡常出現 v.emplace_back(x)——把它當 push_back 讀就對了。差別只在元素本身有好幾個欄位的時候可以少打一對大括號:裝 pair(10.11)的 vector 寫 v.push_back({3, 4}),也可以寫成 v.emplace_back(3, 4)——直接把「造一個元素要用的材料」交給它。本書一律用 push_back

成員函式範例

要看清楚每個操作到底把 vector 變成什麼樣子,最好的辦法是每一步都把裡面的每個數印出來。先寫一個負責印的小函式(就是上冊 7.3 的 void 函式),裡面的遍歷寫法跟陣列(上冊 6.4)一模一樣:

#include <iostream>
#include <vector>
using namespace std;

void show(vector<int> &v) {        // 把整個 vector 印出來,方便隨時看清楚內容
    int len = v.size();            // size() 先存進 int(10.4 會解釋原因)
    cout << '[';
    for (int i = 0; i < len; i++) {
        if (i > 0) cout << ", ";
        cout << v[i];
    }
    cout << "]\n";
}

int main() {
    vector<int> v = {10, 20, 30};
    show(v);
    cout << v.size() << '\n';
    v.push_back(40);               // 尾端加入 40
    show(v);
    v.pop_back();                  // 移除尾端元素
    v.pop_back();                  // 再移除一個
    show(v);
    cout << v.size() << '\n';
    cout << v.empty() << '\n';     // 0:還有元素,不是空的
    v.clear();                     // 清空所有元素
    show(v);
    cout << v.empty() << '\n';     // 1
    return 0;
}

執行結果:

[10, 20, 30]
3
[10, 20, 30, 40]
[10, 20]
2
0
[]
1

方括號與逗號是我自己加的裝飾,為的是把「有哪些元素」框得一清二楚——清空之後印出 [],一眼就看得出裡面真的空了。參數寫成 vector<int> &v(上冊 7.5 的傳參考)而不是 vector<int> v,因為後者會把整個 vector 複製一份給函式用,元素多的時候很浪費(10.6 會細講)。

這個 show 是很好用的除錯工具:程式跑出怪答案時,在可疑的地方插一行 show(v);,馬上就知道 vector 到底變成什麼樣子。

resizeassign 的行為也用同一招看最清楚:

#include <iostream>
#include <vector>
using namespace std;

void show(vector<int> &v) {        // 跟上一段的 show 完全相同
    int len = v.size();
    cout << '[';
    for (int i = 0; i < len; i++) {
        if (i > 0) cout << ", ";
        cout << v[i];
    }
    cout << "]\n";
}

int main() {
    vector<int> v = {1, 2, 3, 4, 5};
    v.resize(3);                   // 變短:只留前 3 個
    show(v);
    v.resize(5, 7);                // 變長:多出來的格子補 7
    show(v);
    v.assign(4, 0);                // 整個重來:長度 4、每格都是 0
    show(v);
    return 0;
}

執行結果:

[1, 2, 3]
[1, 2, 3, 7, 7]
[0, 0, 0, 0]

resize(3) 砍掉後面兩格、resize(5, 7) 在尾端補上兩個 7前面原有的元素不受影響)、assign(4, 0) 則是整個換掉。