日期:2023年8月6日
傳統的陣列 array
C++ 傳統的陣列 array,可以用來儲存多筆相同格式的資料,儲存這些資料的記憶體位址是連續的,所以讀取資料時,是依據陣列開頭的記憶體位址再加上索引值,索引值由 0 開始。以下是關於 array 的基礎知識。
産生 array
可以使用所有內建的資料格式,例如 int、float、char、string,也可以使用自訂的資料格式,但是一個 array 當中所有的資料只能是同一種格式。語法通常有以下3種
資料格式 名稱[長度];
資料格式 名稱[長度] = {資料1, 資料2, 資料3, ...};
資料格式 名稱[長度] = {0};
- 第一種:産生 array 時沒有指定資料內容,切記在讀取 array 前一定要填入資料,否則無法預測會讀到什麼數值。
- 第二種:可以省略長度,編譯器會自動根據資料內容決定 array 的長度。
- 第三種:産生指定長度、所有資料皆為 0 的 array。
int a[5]; // 長度為 5 的整數 array,沒有指定資料內容
int b[] = {0, 1, 2, 3, 4}; // 長度為 5 的整數 array,內容為 {0, 1, 2, 3, 4}
int c[5] = {0}; // 長度為 5 的整數 array,內容為 {0, 0, 0, 0, 0}
int d[5] = {1}; // 不好的寫法,長度為 5 的整數 array,內容為 {1, 0, 0, 0, 0}
int e[5] = {-1}; // 不好的寫法,長度為 5 的整數 array,內容為 {-1, 0, 0, 0, 0}
bool f[5] = {true}; // 不好的寫法,長度為 5 的整數 array,內容為 {1, 0, 0, 0, 0}
bool g[5] = {false};// 長度為 5 的整數 array,內容為 {0, 0, 0, 0, 0}
char alphabet[] = {'A', 'B', 'C', 'D', 'E'};// 長度為 5 的字元 array
string names[] = {"Albert", "Bob", "Conan", "David", "Ethan"};// 長度為 5 的字串 array
搭配 memset 設定 array 內容
如果想要使用 memset,需要先引入函式庫 cstring,語法為
memset(名稱, 0, sizeof(名稱)); // 將指定名稱的 array 資料全部設定為 0
memset(名稱, -1, sizeof(名稱)); // 將指定名稱的 array 資料全部設定為 -1
例如以下的程式碼
int a[5];
memset(a, 0, sizeof(f)); // 長度為 5 的整數 array,內容為 {0, 0, 0, 0, 0}
int b[5];
memset(b, -1, sizeof(f)); // 長度為 5 的整數 array,內容為 {-1, -1, -1, -1, -1}
取得 array 的長度
array 沒有回傳長度的功能,通常是用 array 使用的記憶體除以資料格式使用的記憶體,語法為
sizeof(名稱)/sizeof(資料格式);
例如以下的程式碼
int a[5];
int length = sizeof(a)/sizeof(int); // 回傳值為 5
取得指定索引值的資料
語法有兩種
名稱[索引值]; // 比較常用的語法
*名稱 + 索引值; // 用指標 * 取得 array 開頭的記憶體位址
例如以下的程式碼,輸出的值皆為 1。
int a[] = {0, 1, 2, 3, 4};
cout << a[1] << "\n";
cout << *a + 1 << "\n";
使用 for 迴圈填入資料
如果想要産生一個很長的 array,但是資料之間有規律性,通常會使用 for 迴圈填入資料,手動填入資料太麻煩了,例如以下的程式碼,産生的 array 內容為 {0, 2, 4, 6, 8, 10, 12, 14, 16, 18}。
int a[10];
for(int i=0; i<10; i++) a[i] = 2*i;
使用 for 迴圈印出資料
如果想要印出 array 的資料,不能使用以下的語法,這樣只會印出 array 的記憶體位址。
cout << 名稱 << endl;
通常會使用 for 迴圈配合索引值印出資料,例如以下的程式碼
int a[] = {0, 1, 2, 3, 4};
int length = sizeof(a)/sizeof(int);
for(int i=0; i<length; i++) cout << a[i] << " \n"[i == length-1];
輸出為
0 1 2 3 4
這裡使用了一個小技巧,最後面的 [i == length-1] 是指當方括號中的條件成立時印出換行符號 \n,如果條件不成立時印出空格。因為程式競賽或 APCS 經常會要求這樣輸出答案,用空格分隔資料,但是最後一筆資料直接換行、不加空格。
二維 array
二維 array 很像把資料分布在 xy 平面上,每筆有兩個索引值,就像是 x、y 座標值。由於二維 array 看起來很像數學中的矩陣,通常會沿用矩陣的講法,將 {{0, 1, 2}, {3, 4, 5}} 這樣的二維 array 稱為 2 列 (row)、3 欄 (column)。産生二維 array 的語法為
資料格式 名稱[列數][欄數];
資料格式 名稱[列數][欄數] = {{資料1, 資料2, 資料3, ...}, {資料4, 資料5, 資料6, ...}, ...};
如果産生二維 array 時有指定資料可以省略列數,編譯器會自動從指定資料判斷列數,但是欄數不能省略。例如以下的程式碼,a 的內容為 {{0, 1, 2}, {3, 4, 5}},b 的內容為 {{1, 2, 0}, {3, 4, 0}},因為 b 應該有 3 欄,沒有指定的資料自動補 0。
int a[2][3] = {{0, 1, 2}, {3, 4, 5}};
int b[][3] = {{1, 2}, {3, 4}};
如果要印出二維 array 的內容,也是利用 for 迴圈
int a[2][3] = {{0, 1, 2}, {3, 4, 5}};
for(int i=0; i<2; i++) {
for(int j=0; j<3; j++) {
cout << a[i][j] << " \n"[j == 2];
}
}
輸出為
0 1 2
3 4 5
STL vector 函式庫
STL vector 與 array 不同,可以動態地改變容器長度,使用上比較方便。要先引入函式庫,函式庫的官方說明書在此 cplusplus.com std::vector。
#include <vector>
建立 vector
vector 的資料可以是所有內建的資料格式,例如 int、float、char、string,也可以使用自訂的資料格式。語法為
// 産生空的 vector
vector<資料格式> 名稱;
// 産生填入相同資料的 vector,資料可省略,預設值為 0
vector<資料格式> 名稱 (資料數量, 資料);
// 由 vector2 複製資料,也可以改變記憶體位置只複製部分內容
vector<資料格式> 名稱 (vector2名稱.begin(), vector2名稱.end());
// 由 vector2 複製資料
vector<資料格式> 名稱 (vector2名稱);
// 由已定義的 array 複製資料
vector<資料格式> 名稱 (array名稱, array名稱 + array長度);
// 産生陣列並指定資料
vector<資料格式> 名稱 {用逗號分隔的資料};
vector<資料格式> 名稱 = {用逗號分隔的資料};
例如以下的程式碼
vector<int> v1; // v1 是空的
vector<int> v2 (5, 1); // v2 的內容為 {1, 1, 1, 1, 1}
vector<int> v3 (v2.begin(), v2.end()); // v3 的內容為 {1, 1, 1, 1, 1}
vector<int> v4 (v3); // v4 的內容為 {1, 1, 1, 1, 1}
int data[] = {0, 1, 2, 3, 4};
vector<int> v5 (data, data + sizeof(data)/sizeof(int)); // v5 的內容為 {0, 1, 2, 3, 4}
vector<int> v6 {0, 1, 2, 3, 4}; // v6 的內容為 {0, 1, 2, 3, 4}
vector<int> v7 = {0, 1, 2, 3, 4}; // q7 的內容為 {0, 1, 2, 3, 4}
迭代器 iterator
迭代器可以用來取得物件對應的記憶體位址,有以下幾種:
名稱.begin(); // 回傳起點位址
名稱.end(); // 回傳終點位址
名稱.rbegin(); // 回傳終點位址,由後向前向讀取資料
名稱.rend(); // 回傳起點位址,由後向前向讀取資料
名稱.cbegin(); // 回傳起點位址,回傳值為常數
名稱.cend(); // 回傳終點位址,回傳值為常數
名稱.crbegin();// 回傳終點位址,由後向前向讀取資料,回傳值為常數
名稱.crend(); // 回傳起點位址,由後向前向讀取資料,回傳值為常數
迭代器的變數名稱通常是 it,寫法有兩種,例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4};
vector<int>::iterator it = v.begin();
auto it = v.begin();
但是 vector<int>::iterator 只能用在 begin()、end(),還是用 auto 會比較方便。如果要印出陣列中所有的資料,可以配合迭代器處理,例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4};
for(auto it = v.begin(); it != v.end(); it++)
cout << *it << " \n"[it == v.end()-1]; // 印出的值為 0 1 2 3 4
for(auto it = v.rbegin(); it != v.rend(); it++)
cout << *it << " \n"[it == v.rend()-1]; // 印出的值為 4 3 2 1 0
for(auto it = v.cbegin(); it != v.cend(); it++)
cout << *it << " \n"[it == v.cend()-1]; // 印出的值為 0 1 2 3 4
for(auto it = v.crbegin(); it != v.crend(); it++)
cout << *it << " \n"[it == v.crend()-1]; // 印出的值為 4 3 2 1 0
填入資料
第一種語法為
名稱.assign(長度, 資料);
例如以下的程式碼
vector<int> v;
v.assign(5, 10); // v 的內容為 {10, 10, 10, 10, 10}
第二種語法是由另一個 vector 中指定的記憶體位址複製資料,不包含終點位址。
名稱.assign(起點, 終點);
例如以下的程式碼
vector<int> v1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
vector<int> v2;
v2.assign(v1.begin()+5, v1.end()); // v2 的內容為 {5, 6, 7, 8, 9, 10}
第三種語法是由另一個 array 中指定的記憶體位址複製資料,不包含終點位址。
名稱.assign(array起點, array終點);
例如以下的程式碼
int data[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
vector<int> v;
v.assign(data+5, data+10); // v 的內容為 {5, 6, 7, 8, 9}
從最後面填入資料
語法為
名稱.push_back(資料);
例如以下的程式碼
vector<int> v;
v.push_back(0); // v 的內容為 {0}
v.push_back(1); // v 的內容為 {0, 1}
v.push_back(2); // v 的內容為 {0, 1, 2}
另一個工具是 emplace_back,語法為
名稱.emplace_back(資料);
看起來效果和 push_back 差不多,但是據說少了複製構造的步驟,所以效率更高。例如以下的程式碼
vector<int> v;
v.emplace_back(0); // v 的內容為 {0}
v.emplace_back(1); // v 的內容為 {0, 1}
v.emplace_back(2); // v 的內容為 {0, 1, 2}
於指定的位置插入資料
語法為
名稱.insert(記憶體位址, 資料);
要配合迭代器取得記憶體位址,例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4};
v.insert(v.begin(), -1); // 於最前面插入 -1,v 的內容變為 {-1, 0, 1, 2, 3, 4}
v.insert(v.end(), -2); // 於最後面插入 -2,v 的內容變為 {-1, 0, 1, 2, 3, 4, -2}
v.insert(v.begin()+2, -3); // 於開頭處向後 2 格插入 -3,v 的內容變為 {-1, 0, -3, 1, 2, 3, 4, -2}
v.insert(v.end()-2, -4); // 於結尾處向前 2 格插入 -4,v 的內容變為 {-1, 0, -3, 1, 2, 3, -4, 4, -2}
另一個工具是 emplace,語法為
陣列名稱.emplace(記憶體位址, 資料);
看起來效果和 instert 差不多,但是據說少了複製構造的步驟,所以效率更高。例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4};
v.emplace(v.begin()+1, -1); // 於開頭處向後 1 格插入 -1,v 的內容變為 {0, -1, 1, 2, 3, 4}
刪除最後面的資料
語法為
名稱.pop_back();
不需要任何參數,沒有回傳值。例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4, 5};
v.pop_back(); // 刪除開頭處的資料,v 的內容變為 {0, 1, 2, 3, 4}
如果陣列是空的,會回傳程式記憶體區段錯誤,例如以下的程式碼
vector<int> v;
v.pop_back();
刪除指定範圍的資料
語法為
名稱.erase(起點位址, 終點位址);
不包含終點位址。例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4, 5};
v.erase(v.begin()); // 刪除開頭處的資料,v 的內容變為 {1, 2, 3, 4, 5}
v.erase(v.begin(), v.begin()+2); // 刪除開頭處及下 1 格的資料,v 的內容變為 {3, 4, 5}
v.erase(v.begin(), v.end()); // 刪除所有資料
清除所有資料
語法為
名稱.clear();
不需要加上任何參數,清空資料後陣列長度為 0。
交換兩個 vector 的資料
語法為
名稱1.swap(名稱2);
取得最前面的資料
語法為
名稱.front();
不需要任何參數。例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4, 5};
int a = v.front(); // a 的內容為 0
如果陣列是空的,會回傳 0,例如以下的程式碼
vector<int> v;
int a = v.front(); // a 的內容為 0
取得最後面的資料
語法為
陣列名稱.back();
不需要任何參數。例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4, 5};
int a = v.back(); // a 的內容為 5
如果陣列是空的,會回傳程式記憶體區段錯誤,例如以下的程式碼
vector<int> v;
int a = v.back();
取得指定索引值的資料
語法有兩種,分別為
名稱[索引值];
名稱.at(索引值);
兩者有點差異,例如以下的程式碼
vector<int> v = {0, 1, 2, 3, 4, 5};
int a = v[1]; // a 的內容為 1
int b = v[10]; // 索引值超出範圍,回傳 0
int c = v.at(1); // c 的內容為 1
int d = v.at(10); // 索引值超出範圍,回傳錯誤訊息 std::out_of_range
取得長度
語法為
名稱.size();
不需要任何參數,回傳值格式為 size_t,沒有正負號的整數。
回傳最大長度
語法為
名稱.max_size();
不需要任何參數,回傳值格式為 size_t,沒有正負號的整數。在我使用的系統中回傳值為 2305843009213693951,為 $2^{61} - 1$。
調整長度
語法為
名稱.resize(長度, 資料);
將陣列調整成指定長度,資料參數可省略,預設值為 0。例如以下的程式碼
vector<int> v {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
v.resize(5); // v 的內容變為 {0, 1, 2, 3, 4}
v.resize(10, -1); // v 的內容變為 {0, 1, 2, 3, 4, -1, -1, -1, -1, -1}
v.resize(15); // v 的內容變為 {0, 1, 2, 3, 4, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0}
調整使用的記憶體
語法為
名稱.shrink_to_fit();
不需要任何參數,沒有回傳值。例如以下的程式碼
vector<int> v (100); // v 的內容為 100 個 0
v.resize(10); // v 的內容變為 10 個 0
v.shrink_to_fit(); // 調整 v 使用的記憶體
回傳使用的記憶體大小
語法為
名稱.capacity();
不需要任何參數,回傳值格式為 sizt_t。例如以下的程式碼
vector<int> v;
for(int i=0; i<100; i++) v.push_back(i); // v 的內容為 {0, 1, ..., 99}
cout << v.capacity() << endl; // 使用記憶體 128
cout << v.size() << endl; // v 的長度為 100
檢查 vector 是否為空
語法為
名稱.empty();
不需要任何參數,如果陣列為空回傳 1,如果有資料回傳 0。
二維 vector
二維 vector 很像把資料分布在 xy 平面上,每筆有兩個索引值,就像是 x、y 座標值。由於二維 vector 看起來很像數學中的矩陣,通常會沿用矩陣的講法,將 {{0, 1, 2}, {3, 4, 5}} 這樣的二維 vector 稱為 2 列 (row)、3 欄 (column)。産生二維 vector 的語法為
vector<vector <資料格式>> 名稱;
vector<vector <資料格式>> 名稱 = {{資料1, 資料2, 資料3, ...}, {資料4, 資料5, 資料6, ...}, ...};
如果使用第一種語法,通常搭配 for 迴圈及 push_back 填入資料,例如以下的程式碼
vector<vector <int>> v;
for(int i=0; i<2; i++) {
vector<int> tmp;
for(int j=0; j<3; j++) {
tmp.push_back(i*3 + j);
}
v.push_back(tmp);
}
v 的內容為
{{0, 1, 2},
{3, 4, 5}}
如果使用第二種語法,可以將資料寫在同一行,也可以將不同列的資料寫在不同行讓欄位對齊,例如以下的程式碼
vector<vector <int>> v = {{0, 1, 2},
{3, 4, 5}};
v 的內容為
{{0, 1, 2},
{3, 4, 5}}
vector 與 array 不同之處,vector 並沒有要求每列的欄數相同,例如以下的程式碼
vector<vector <int>> v2 = {{0},
{1, 2},
{3, 4, 5},
{6, 7, 8, 9}};
v2 的內容為
{{0},
{1, 2},
{3, 4, 5},
{6, 7, 8, 9}}
如果要印出二維 vector 的內容,也是利用 for 迴圈搭配索引值或是迭代器,例如以下的程式碼
vector<vector <int>> v = {{0, 1, 2},
{3, 4, 5}};
// 搭配索引值
for(size_t i=0; i<v.size(); i++) {
for(size_t j=0; j<v[i].size(); j++) {
cout << v[i][j] << " \n"[j == v[i].size()-1];
}
}
// 搭配迭代器
for(auto it = v.begin(); it != v.end(); it++) {
for(auto it2 = (*it).begin(); it2 != (*it).end(); it2++) {
cout << *it2 << " \n"[it2 == (*it).end()-1];
}
}
// 搭配迭代器,但是每筆資料後面都有空格
for(auto row : v) {
for(auto c : row) {
cout << c << " ";
}
cout << "\n";
}
輸出為
0 1 2
3 4 5
結語
以上是我目前常用到的 array 及 vector 功能,如果之後有遇到其它需求會再補充內容。
參考資料
HackMD 版本連結:https://hackmd.io/@yizhewang/Sy4Coccsh
沒有留言:
張貼留言