蔡銘軒 @ Sprout 2020 C/C++語法班
一言以敝之:資料結構就是你怎麼存資料
情境:請你紀錄資訊之芽C/C++班所有學生第一次階段考的成績,之後要做各種統計
想法:先把資料存到陣列裡再說
陣列就是一種資料結構!
陣列只是最基本的資料結構....
更多資料結構例如:
(balanced) binary search tree
heap/priority queue
hash table
族繁不及備載
課程緊湊沒有時間教這麽多QQ
Standard Template Library
由C++幫你寫好的許多實用功能,使用者不需要知道實作的細節,知道怎麼操作就好!
警告:以下內容只適用於C++,無法在C使用
接下來介紹一些STL內的container,可以用來裝資料並且有各種不同功能
Recall:
static的陣列: 宣告時就要決定長度,且無法修改...
linked-list: 空間彈性比較大,但速度比陣列慢,無法隨機存取還有指標一直RE...
動態陣列: 速度快、可以隨機存取、空間可以動態調整
std::vector 就是一種動態陣列!
常用指令:
操作 | 說明 | 複雜度 |
std::vector<T> v; std::vector<T> v(n); std::vector<T> v(n, e); |
宣告一個空的vector,用來裝type T的資料 宣告一個已經裝有n個type T的vector。內容為type T的預設值 宣告一個已經裝有n個type T的vector,並初始化它們為e |
O(1) O(n) O(n) |
v.push_back(element) v.emplace_back(element) |
將element這個元素放到v這個vector的最後面 | O(1) |
v[i] / v.at(i) | 回傳v裡的第i個元素 | O(1) |
v.size() | 回傳v裡裝了幾個元素 | O(1) |
v.capacity() | 回傳v目前最多可以裝多少個元素 | O(1) |
v.pop_back() | 移除v的最後一個元素 | O(1) |
v.empty() | 回傳v現在是否是空的 | O(1) |
v.back() | 回傳v的最後一個元素 | O(1) |
v.clear() | 將v清空 | O(n) |
v.resize(n) | 將v的size設為n,若size變小,只保留前n個元素 | O(n) |
範例 - 宣告
vector<int> v;
//空的vector,沒有內容。
vector<int> v(10);
//裝個10個int的vector,內容為該型態預設值。對int而言是0。
// 0 0 0 0 0 0 0 0 0 0
vector<int> v(10, -1);
// -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
vector<int> v{1, 2, 3};
// 1 2 3
vector<int> v = {1, 2, 3}
//跟array一樣
範例 - 新增元素/size
vector<int> v;
//空的
cout << v.size() << endl;
// 0
v.push_back(0);
// 0
cout << v.size() << endl;
// 1
for (int i = 1; i < 10; i++)
v.emplace_back(i);
// 0 1 2 3 4 5 6 7 8 9
cout << v.size() << endl;
// 10
push_back/emplace_back都可以達到相同的效果,但兩者其實略有差異,視課程進度在後續介紹。
範例 - 存取元素
vector<int> v;
for (int i = 0; i < 10; i++)
v.push_back(i);
for (int i = 0; i < 10; i++)
cout << v[i] << ' ';
cout << endl;
//0 1 2 3 4 5 6 7 8 9
v[0] = 100;
//100 1 2 3 4 5 6 7 8 9
cout << v[10] << endl;
//??
cout << v.at(10) << endl;
//error
v.at(i) 會檢查存取範圍是否非法。v[i]則與array相同
範例 - 動態陣列
vector<int> v;
cout << v.size() << ' ' << v.capacity() << endl;
// 0 0
for (int i = 0; i < 10; i++) {
v.push_back(i);
cout << v.capacity() << ' ';
}
//1 2 4 4 8 8 8 8 16 16
vector會依據使用情況自動分配新的空間 (通常是兩倍),使用者無需操心!
範例 - empty與各種back
vector<int> v;
for (int i = 0; i < 10; i++)
v.push_back(i);
while (!v.empty()) { //當v還有元素
cout << v.back() << ' ';
v.pop_back();
}
// 9 8 7 6 5 4 3 2 1 0
v.pop_back();
//???
使用vector時,要隨時確認操作的合法性!
範例 - clear & resize
vector<int> v(100); //今年的100的學生
for (int i = 0; i < 100; i++)
v[i] = rand(); //做一些事
v.clear(); //清空今年學生資料
v.resize(200); //準備裝下一屆的200個學生
for (int i = 0; i < 200; i++)
v[i] = rand();
情境:今年資芽的100的學員結業了,下一屆準備招生200個學生。
v.clear()會把v的size歸0,也就是刪除所有元素。
n-d vector - 以二維vector為例
vector<vector<int>> v;
宣告一個vector,裝的是vector<int>
vector<vector<int>> v;
vector<int> u{1,2,3};
v.push_back(u);
直接push_back整個vector到v
與n-d array比較
int arr[3][3];
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; ++j)
arr[i][j] = i + j;
vector<vector<int>> v(3, vector<int>(3));
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; ++j)
v[i][j] = i + j;
小結語
std::vector算是最常被使用的STL container之一。
array能做到的事情,vector也能做到
vector能做到的事情,array不一定能做到
小缺點: vector的速度比array略慢。但大部分的時候這個差距很小,可忽略
vector的介紹到此結束?
還有更多!
1. 概念很類似指標
2. 可以想像成STL專用的指標
3. 根據不同的container,對應的iterator實作方式不盡相同。大部分的時候iterator != 先前學過的指標 (是更複雜的內容)
4. 底下的實作很複雜,我們只需要知道怎麼操作即可。
以std::vector<int>為例
vector<int> v | int arr[] | |
iterator/pointer型態 | vector<int>::iterator iter | int* ptr |
第一個 | v.begin() | arr |
最後一個 | v.end() | arr + N |
所指的內容物 | *iter | *ptr |
往前走/往後走 | iter++/iter-- | ptr++/ptr-- |
常見用法&與一般陣列 int arr[] (假設N個元素) 的比較
以std::vector<int>為例
一個簡單的圖例 (credit: Arvin)
v.end()是沒有內容的!!
以std::vector<int>為例
vector<int> v {1, 2, 3, 4, 5};
for (vector<int>::iterator iter = v.begin(); iter != v.end(); iter++) {
cout << *iter << ' ';
}
//1 2 3 4 5
其實...
vector的iterator就是之前學過的pointer!
你熟悉的pointer操作在這裡都適用
但通常其他container的iterator是比pointer更複雜的東西,我們還是要乖乖的看使用說明書
以std::vector<int>為例
有了iterator之後...
操作 | 效果 |
---|---|
v.insert(iter, x) | 在iter的地方插入x |
v.erase(iter) | 刪除iter所指的的元素 |
以std::vector<int>為例
vector<int> v{1, 2, 3, 4, 5};
v.insert(v.begin() + 2, 7);
//1 2 7 3 4 5
v.erase(v.begin())
//2 7 3 4 5
insert跟erase都是O(N),如果需要大量使用這兩個操作,建議改用其他資料結構
以std::vector<int>為例
排序vector(由小到大)
sort(v.begin(), v.end())
由大到小?
reverse(v.begin(), v.end())
iterator注意事項
v.push_back()
翻譯:如果v.push_back()使得vector需要更多空間(size超過capacity),程式會自動尋找另一塊記憶體來存放更長的vector。這時原本的iterator都會失效。
iterator注意事項
vector<int> v {1,2,3};
vector<int>::iterator iter = v.begin();
cout << *iter << endl;
//1
for (int i = 0; i < 10000; i++)
v.push_back(i);
cout << *iter << endl;
//???
iterator注意事項
對vector進行各種操作(例如push_back, insert, erase....)後,都可能影響到原本iterator的validity。
在使用iterator時要特別注意現在的iterator是否還是可以使用的。
什麼時候哪些操作會影響iterator validity? 多看使用說明書。
iterator好實用,但是vector<int>::iterator好長好難打..
以vector<int> 為例
C++11之後,有auto可以使用囉!
vector<int> v {1, 2, 3, 4, 5};
for (auto iter = v.begin(); iter != v.end(); iter++) {
cout << *iter << ' ';
}
//1 2 3 4 5
Q: 如何使用C++11?
A: 編譯時加上參數
範例:
g++ -std=c++11 <filename.cpp>
另有
c++14
c++17
c++2a (實驗階段)
使用auto時一定要賦值
int a; // ok
auto c; // error
auto的一些例子
auto a = 0; // int
auto b = 0LL; // long long
auto c = 3.14; // double
auto d = 3.14F; // float
auto e = "abc"; // char*
auto f = 'a'; // char
注意事項
1. auto雖然方便,但不建議大量使用。會降低程式碼的可讀性。基本的資料型別,例如int, double等,就直接寫出來
2. auto只是讓宣告變得方便,宣告後變數的型態還是固定的
auto a = 3;
a = 1.111; // a is still int
3. 使用auto時要注意型態可能不是你預期的
auto a = 0;
a = 127127127127127; // a is int, overflow
同樣是C++11之後的功能。
跟之前學過的for loop略有不同,直接來看用法
vector<int> v {1,2,3,4,5};
for (int number: v)
cout << number << ' ';
// 1 2 3 4 5
把v裡面的元素按照順序丟進number裡印出來
這裡的number只是一個暫時的容器,並不是真正vector裡的元素,例如
vector<int> v {1,2,3,4,5};
for (int number: v)
number = 2 * number;
for (int number: v)
cout << number << ' ';
// 1 2 3 4 5
可以搭配reference使用,能夠對容器的內容修改
vector<int> v {1,2,3,4,5};
for (int& number: v)
number = 2 * number;
for (int number: v)
cout << number << ' ';
// 2 4 6 8 10
也可以搭配auto
vector<int> v {1,2,3,4,5};
for (auto& number: v)
number = 2 * number;
for (auto number: v)
cout << number << ' ';
// 2 4 6 8 10
FILO (First in Last out): 先進後出
操作 | 說明 | 複雜度 |
std::stack<T> s; | 宣告一個空的stack,用來裝type T的資料 | O(1) |
s.push(element) | 將element這個元素放到s這個stack的上面 | O(1) |
s.pop() | 移除s最上面的元素 | O(1) |
s.top() | 回傳s最上面的元素 | O(1) |
s.size() | 回傳s目前裝了幾個元素 | O(1) |
s.empty() | 檢查s是否為空 | O(1) |
常用指令
範例
#include <iostream>
#include <stack>
using namespace std;
int main() {
stack<int> sta;
for (int i = 0; i < 10; i++)
sta.push(i);
// 0 1 2 3 4 5 6 7 8 9
while (!sta.empty()) {
cout << sta.top() << ' ';
sta.pop();
}
// 9 8 7 6 5 4 3 2 1 0
}
push跟pop好像在哪裡看過?
其實stack支援的功能,vector也都包了,所以其實可以用vector來模擬stack。而且通常vector的速度會再更快一些。
不過這裡最重要的是stack的FILO概念,在資工/程式的世界非常常見&常用。
FIFO (First in First out): 先進先出
操作 | 說明 | 複雜度 |
std::queue<T> que; | 宣告一個空的queue,用來裝type T的資料 | O(1) |
que.push(element) | 將element這個元素放到q這個queue的後面 | O(1) |
q.pop() | 移除q最前面的元素 | O(1) |
q.front() | 回傳q最前面的元素 | O(1) |
q.back() | 回傳q最後面的元素 | O(1) |
q.size() | 回傳q目前裝了幾個元素 | O(1) |
q.empty() | 檢查q是否為空 | O(1) |
常用指令
範例
#include <iostream>
#include <queue>
using namespace std;
int main() {
queue<int> que;
for (int i = 0; i < 10; i++)
que.push(i);
// 0 1 2 3 4 5 6 7 8 9
while (!que.empty()) {
cout << que.front() << ' ';
que.pop();
}
// 0 1 2 3 4 5 6 7 8 9
}
stack: 只有一個開口,進出共用,先進後出
queue: 有兩個開口,一個只進不出,另一個只出不進,先進先出
deque: 兩個開口,都可以進出,結合stack與queue
常用指令
操作 | 說明 | 複雜度 |
std::deque<T> deq; std::deque<T> deq(n); std::deque<T> deq(n, e); |
宣告一個空的deque,用來裝type T的資料 宣告一個已經裝有n個type T的deque。內容為type T的預設值 宣告一個已經裝有n個type T的deque,並初始化它們為e |
O(1) O(n) O(n) |
deq.push_back(element) deq.push_front(element) |
將element這個元素放到deq的最後面 將element這個元素放到deq的最前面 |
O(1) O(1) |
deq[i] | 回傳deq裡的第i個元素 | O(1) |
deq.size() | 回傳deq裡裝了幾個元素 | O(1) |
deq.back() deq.front() |
回傳deq最後面的元素 回傳deq最前面的元素 |
O(1) O(1) |
deq.pop_back() deq.pop_front() |
移除deq最後面的元素 移除deq最前面的元素 |
O(1) O(1) |
deq.empty() | 回傳deq現在是否是空的 | O(1) |
deq.clear() | 將deq清空 | O(n) |
deq.resize(n) | 將deq的size設為n,若size變小,只保留前n個元素 | O(n) |
範例
#include <iostream>
#include <deque>
using namespace std;
int main() {
deque<int> deq;
for (int i = 0; i < 10; i++)
if (i % 2 == 0)
deq.push_back(i);
else
deq.push_front(i);
// 9 7 5 3 1 0 2 4 6 8
while (!deq.empty()) {
cout << deq.front() << ' ';
deq.pop_front();
}
// 9 7 5 3 1 0 2 4 6 8
}
小討論
deque是目前看起來功能最多的資料結構,包含了所有vector(deque也有insert跟erase,用法同vector), stack, queue的功能。
但我們不常需要這麼多功能,大部分的時候還是使用vector/stack/queue。
其實std::stack跟std::queue底下是用std::deque實作的!而std::deque雖然功能眾多,速度相對就慢了一些。我們可以用更快的std::vector取代std::stack。
std::list其實就是double linked list
操作 | 說明 | 複雜度 |
std::list<T> lst; std::lst<T> lst(n); std::list<T> lst(n, e); |
宣告一個空的list,用來裝type T的資料 宣告一個已經裝有n個type T的list。內容為type T的預設值 宣告一個已經裝有n個type T的list,並初始化它們為e |
O(1) O(n) O(n) |
lst.push_back(element) lst.push_front(element) |
將element這個元素放到lst的最後面 將element這個元素放到lst的最前面 |
O(1) O(1) |
lst.size() | 回傳lst裡裝了幾個元素 | O(1) |
lst.back() lst.front() |
回傳lst最後面的元素 回傳lst最前面的元素 |
O(1) O(1) |
lst.pop_back() lst.pop_front() |
移除lst最後面的元素 移除lst最前面的元素 |
O(1) O(1) |
lst.empty() | 回傳lst現在是否是空的 | O(1) |
lst.insert(iter, element) lst.erase(iter) |
將element插在iter的地方 刪除lst中iter所指的元素 |
O(1) O(1) |
常用指令
警告
list的iterator與pointer不同,有些操作不能使用
list<int> lst(3);
auto iter = lst.begin();
iter++; // ok
iter += 1; // error
list的iterator可以使用iter++/iter--,但不能使用iter += k這樣的指標運算
範例
#include <iostream>
#include <list>
using namespace std;
int main(){
list<int> lst;
lst.push_back(1); // 1
lst.push_front(0); // 0 1
lst.push_back(3); // 0 1 3
auto iter = lst.begin();
cout << *iter << endl; // output 0
lst.insert(iter,5); // 5 0 1 3
iter++;
lst.erase(iter); // 5 0 3
for(auto i: lst)
cout << i << ' ';
// 5 0 3
}
小評論
list也包含許多功能,底下就是double linked list。
但list的速度偏慢。
但vector與list本身就用來處理不同的問題,沒有互相取代的問題。
credit: Arvin
不同資料結構有不同的功能,依據不同的情況選擇適合的資料結構有助於讓程式運作得更好!
怎麼知道什麼時候用什麼資料結構?
多寫題目、多練習
將兩個變數綁在一起,可以為不同類型。
操作 | 說明 |
std::pair<T1, T2> p; | 宣告一個pair,第一個元素為type T1,第二個為type T2 |
p.first p.second |
存取p的第一個元素(對應T1) 存取p的第二個元素(對應T2) |
std::make_pair(e1, e2) | 回傳一個pair由e1以及e2組成 |
注意p.first與p.second沒有括號!
p.first()/p.second()
範例
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
int main() {
vector<pair<string, int>> students;
students.push_back(make_pair("john", 20));
pair<string, int> p = students[0];
cout << p.first << ' ' << p.second << endl;
// john 20
}
在使用vector時,若要將元素放到最後面,可以使用push_back()與emplace_back()兩種方式。
v.push_back(element): 先複製一個與element一樣的元素,接著放到v的最後面。
v.emplace_back(arg): 使用提供的arg,直接在v的最後面用constructor建立一個新的元素。
範例:
vector<pair<int, int>> vec;
vec.push_back(make_pair(0,0)); //--- (1)
vec.emplace_back(0,0); //--- (2)
vec.emplace_back(make_pair(0,0)) //--- (3)
(1): 首先用make_pair建立一個(0,0)的pair,接著會再複製一份(0,0)放到v的後面。
(2): 使用兩個0當作參數,直接在v的後面建立一個(0, 0)的pair
(3): 若參數本身就是vector裝的類型,會使用copy constructor。這時其實也是複製了一個(0,0),效果跟push_back()相同
大部分的時候,使用emplace_back()會比push_back()略快一些,因為少了一次複製的動作。
但emplace_back()主要的用途不在於速度(push_back()已經很快了),而是它能讓我們在container裡面建立noncopyable的元素
noncopyable? 有興趣的同學自行研究囉
pair的進階版,將任意數量的變數綁在一起,可以為不同類型。
操作 | 說明 |
std::tuple<T1, T2, ....>t; | 宣告一個tuple,第一個元素為type T1,第二個為type T2...等等 |
std::get<i>(t) | 存取t這個tuple的第i個元素 |
std::make_tuple(e1, e2, ...) | 回傳一個tuple由e1,e2,....組成 |
#include <iostream>
#include <tuple>
using namespace std;
int main() {
auto t = make_tuple(10, "john", 3.14); // 10 "john" 3.14
get<1>(t) = "sandy"; // 10 "sandy" 3.14
cout << get<0>(t) << ' ' << get<1>(t) << ' ' << get<2>(t) << endl;
// 10 "sandy" 3.14
}
範例
#include <iostream>
#include <tuple>
using namespace std;
int main() {
auto t = make_tuple(10, "john", 3.14); // 10 "john" 3.14
int num;
string name;
tie(num, name, ignore) = t;
cout << num << ' ' << name << endl;
// 10 "john"
}
用std::tie以及std::ignore把tuple內的東西取出來
Since C++17
Q: 如何使用C++17?
A: 編譯時加上參數
範例:
g++ -std=c++17 <filename.cpp>
主要可以用在std::pair與std::tuple上
#include <iostream>
#include <tuple>
using namespace std;
int main() {
auto t = make_tuple(10, "john", 3.14);
int num;
string name;
double pi;
tie(num, name, pi) = t;
cout << num << ' ' << name << ' ' << pi << endl;
// 10 "john" 3.14
auto [a, b, c] = t; // structured binding
cout << a << ' ' << b << ' ' << c << endl;
// 10 "john" 3.14
return 0;
}
也可以搭配reference使用
#include <iostream>
#include <tuple>
using namespace std;
int main() {
auto t = make_tuple(10, "john", 3.14);
auto &[a, b, c] = t;
a = 5;
int num;
string name;
double pi;
tie(num, name, pi) = t;
cout << num << ' ' << name << ' ' << pi << endl;
// 5 "john" 3.14
return 0;
}
跟for-each loop一起用
#include <iostream>
#include <tuple>
#include <vector>
using namespace std;
int main() {
vector<pair<int, int>> vec;
for (int i = 0; i < 3; i++)
vec.emplace_back(i, 2 * i);
for (auto [a, b]: vec)
cout << a << ' ' << b << endl;
return 0;
}
// 0 0
// 1 2
// 2 4