10.2 vector:長度可變的陣列
vector 定義在標頭檔 <vector> 裡。宣告時跟基本型態多一件事:要用角括號指定「這個櫃子裝什麼型態」。角括號指的就是 < 和 > 這對符號——單獨出現時是比較大小的「小於、大於」,成對包住東西時就當括號用;你每天寫的 #include <iostream> 裡包住函式庫名稱的正是它(上冊 1.1 第一次見過它):
vector<int> v; // 一個裝 int 的 vector,一開始是空的
vector<double> w; // 元素型態隨你指定
注意型態名稱是 vector<int> 整個合起來——角括號填進去之後才算完整,vector 三個字單獨存在不是型態。而 vector<int> 的地位跟 int、double 完全相同:宣告句型一樣是「型態 變數名稱;」,之後也能當函式的參數與回傳值,甚至(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};意思完全相同。(長度, 初始值):指定初始長度,每一格都填成初始值;初始值省略時自動補 0(vector<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;
}
執行結果(輸入 4 與 3 1 4 1):
9
這支程式做的事拆成三步:先讀進 n、開一個剛好 n 格的 vector 把 n 個數字讀進去,再用一個迴圈從頭到尾把每個元素累加到 total,最後印出來。範例輸入的四個數字是 3 + 1 + 4 + 1 = 9,所以答案是 9。
累加的寫法跟上冊 6.4 用陣列做總和時一模一樣——換成 vector 之後,唯一的差別只有「開多大」這件事:陣列要事先寫死一個上限,vector 直接拿讀進來的 n 開。(這裡的數字小,total 用 int 就夠;真正的題目資料量大時,總和很容易超過 int 的範圍,記得照上冊 6.4 的習慣改用 long long。)
¶vector 的抽象理解
vector 到底是什麼?照下面這樣想就夠了:
- 它維護一列序列,所有元素的型態相同。
- 元素住在連續的記憶體裡(跟上冊 6.1 的陣列同款)。
- 給定索引值,一步(O(1)) 就能取得或修改該處的元素。
- 序列的長度可以改變。
- 長度的增減在尾端最快最順;中間其實也能插入移除(10.5 會教),但後方元素得整段搬動。
這叫「抽象理解」——電腦實際做的事不保證跟上面一模一樣(例如它會偷偷多預留一些空間,免得每次變長都要整批搬家),但照這樣想,你就會用 vector,也能正確估計每種操作的快慢。
(小例外先打個招呼:vector<bool> 是標準函式庫的特殊版本,行為跟本節的模型不完全一樣——初學需要「是/否」標記陣列時,用 vector<int> 就好。)
¶成員函式:掛在變數身上的操作
本節開頭用過的 v.push_back(6),寫法是「變數名稱+一個點+函式呼叫」。這種掛在某個變數身上的函式叫成員函式(member function):
變數名稱.成員函式名稱(參數)
它跟上冊第七單元的一般函式差在「歸屬」:一般函式(像 abs(x))獨立存在,誰都能直接呼叫;成員函式屬於容器型態本身,必須透過某個容器變數來呼叫,動的也就是那個變數本人——v.push_back(6) 的意思就是「對 v 做 push_back」。
vector 常用的成員函式一次列齊(快慢標籤是 10.1 的 O(\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、每格都是 x(x 不可省略) |
O(n) |
v.resize(n, x) |
長度改成 n:變短截尾、變長補 x(x 可省略,預設 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 到底變成什麼樣子。
resize 與 assign 的行為也用同一招看最清楚:
#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) 則是整個換掉。