Chapter II. 新手上路
基礎資料結構
動態的陣列
作者建中大講義團隊
協作者8e7

動態陣列(Dynamic arrays)


最基礎的資料結構大概就是陣列了,相信大家也都會使用。
STL 的方便之處是它提供了一個動態型的陣列:vector,它可以像一般的陣列一樣操作,並且可以隨時更動陣列大小以配合內部元素數量的增減。

Vector 基礎用法

我們可以用以下方式來宣告 vector

vector<int> vecA;
// 宣告一個元素型別為 int 的空 vector
vector<int> vecB(10);
// 宣告一個長度為 10 的vector,初始值為 0
vector<int> vecC(10,2);
// 宣告一個長度為 10 的 vector,初始值為 2
vector<int> vecD = {1, 2, 3, 4, 5}; // 賦予初始值
vector<int> vecE {1, 2, 3, 4, 5}; // 也可以這樣賦予初值

< > 中的資料型別可以替換,代表陣列中元素的型別。

vectorpush_back 函數可以將元素新增到 vector 的最尾端;pop_back 函數則刪除最尾端的元素。

vector<int> vec;
vec.push_back(1); // [1]
vec.push_back(4); // [1,4]
vec.pop_back(); // [1];

vector 與一般陣列一樣,可以用 operator[] 進行查找元素。另外也可以透過 STL 內建的 at 函數與 back 函數來進行元素的查找。

cout << v[0] << endl;
// 印出 vector 的第 0 個元素
cout << v.at(0) << endl;
// 也可以這樣
cout << v.back() << endl;
// 印出 vector 尾端的元素值

尋訪 vector 裡的每個元素可以用更簡短的寫法:

for (auto &x : vec) {
    cout << x << endl;
}

size 函數回傳有多少元素在這個 vector 裡面。

for (int i = 0; i < vec.size(); i++)
    cout << vec[i] << endl;

size() 函數的回傳值是 unsigned,所以如果寫 for (int i = 0; i < vec.size() - 1; i++) 的迴圈的話,要注意 vec 的大小是否可能是 0,否則 vec.size() - 1 會變成 $2^{32}-1$ 或是 $2^{64}-1$,導致迴圈跑不完。

建議在這種狀況時轉型成 int,寫成 (int)vec.size() - 1

迭代器(Iterator)

還記得指標(pointer)嗎?他會指向一個變數所在的位置,並且在陣列裡面可以用來指向陣列的某一項。vector 也有類似指標的東西,他的名字是迭代器(Iterator)。顧名思義,迭代器就是一個能枚舉資料結構的資料的物件。讓我們來看他實際的使用方法吧!

下面的程式碼會列印出 a 的每一項:

vector<int>::iterator p = a.begin();
while (p != a.end()) {
    cout << *p << endl;
    p = next(p);
}

Iterator 的型態依據他使用的資料結構而異,因此這裡使用的型態是 vector<int>::iteratora.begin() 是指向 vector 第一項的物件,a.end() 則是 vector 最後一項的下一項的物件。如果要指向一個 iterator 的下一項,那麼可以使用 next(),上一項則可以使用 prev()

下面的程式碼一樣會列印出 a 的每一項:

auto p = a.begin();
while (p != a.end()) {
    cout << *p << endl;
    p = p + 1;
}

注意到,我們把 vector<int>::iterator 換成了 auto,這個是 C++ 的關鍵字,可以讓編譯器自己判斷變數的型態。另外我們將 next(p) 的部份改成了 p + 1,這是因為 vector 和一般的陣列一樣,數值是連續排列的,所以 Iterator 也可以用加減法來指向旁邊的項目。

如果想要更加了解 Iterator 的內容,可以參考 Iterator

Vector 的各種酷功能

讓我們來看看 vector 有什麼方便的功能吧!

首先是 find() 函式,這個函式可以尋找某個元素在 vector 出現的第一個位置。注意到他回傳的也是 Iterator 喔!

vector<int> a = {4, 3, 10, 1, 9};
auto it = find(a.begin(), a.end(), 1);
int pos = it - a.begin();
cout << "1 occurs at position " << pos << "\n";
if (find(a.begin(), a.end(), 2) == a.end()) {
    cout << "2 is not found\n";
}

int pos 那一行,我們將 find() 找到的 iterator 與 a.begin() 相減,得到數字 1 出現在陣列的位置。而如果一個數字不在 vector 裡面的話,find() 會回傳 a.end()

find() 必須要依序看過 vector 的每一項,因此時間複雜度是 $O(n)$。

接下來是 inserterase 兩個功能:

vector<int> a = {1, 2, 3};
// 在 a[0] 前插入 4
a.insert(a.begin(), 4); // 4, 1, 2, 3
// 在 a[3] 前插入 5
a.insert(a.begin() + 3, 5); // 4, 1, 2, 5, 3

vector<int> b = {10, 20};
//在 a 的結尾加上 b
a.insert(a.end(), b.begin(), b.end()); // 4, 1, 2, 5, 3, 10, 20

// 刪除 a[2]
a.erase(a.begin() + 2); // 4, 1, 5, 3, 10, 20
// 刪除 a[1] 和 a[2] (不含 a[3])
a.erase(a.begin() + 1, a.begin() + 3); // 4, 3, 10, 20

insert 功能是在某個位置之後插入一個(或多個)東西,erase 則是刪除一個(或多個)位置的東西。注意到這裡傳入的區間都是左閉右開的(包含第一項,不包含最後一項)。

還有一些常見的功能:

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

// 將 a 反轉
reverse(a.begin(), a.end()); // 5, 4, 3, 2, 1

// 將 a 排序
sort(a.begin(), a.end()) // 1, 2, 3, 4, 5

// 將 a 清空
a.clear()

有一些函式(如 insert, erase, clear)等等的寫法是 vec.function,但是 find, reverse, sort 的寫法卻是 function(begin, end)。前者的函式是屬於 vector 這個類別底下的 member function,後者則是 <algorithm> 底下的函式,相信讀者在基礎演算法 / 標準函式庫 ── <algorithm> 與 <numeric>已經有所了解。