常用STL

蔡銘軒 @ Sprout 2020 C/C++語法班

資料結構

一言以敝之:資料結構就是你怎麼存資料

情境:請你紀錄資訊之芽C/C++班所有學生第一次階段考的成績,之後要做各種統計

想法:先把資料存到陣列裡再說

陣列就是一種資料結構!

資料結構

陣列只是最基本的資料結構....

更多資料結構例如:

(balanced) binary search tree

heap/priority queue

hash table

族繁不及備載

課程緊湊沒有時間教這麽多QQ

STL

Standard Template Library

由C++幫你寫好的許多實用功能,使用者不需要知道實作的細節,知道怎麼操作就好!

警告:以下內容只適用於C++,無法在C使用

接下來介紹一些STL內的container,可以用來裝資料並且有各種不同功能

std::vector

std::vector

Recall:

std::vector

static的陣列: 宣告時就要決定長度,且無法修改...

linked-list: 空間彈性比較大,但速度比陣列慢,無法隨機存取還有指標一直RE...

動態陣列: 速度快、可以隨機存取、空間可以動態調整

std::vector 就是一種動態陣列!

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)

std::vector

範例 - 宣告

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一樣

std::vector

範例 - 新增元素/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都可以達到相同的效果,但兩者其實略有差異,視課程進度在後續介紹。

std::vector

範例 - 存取元素

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相同

std::vector

範例 - 動態陣列

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會依據使用情況自動分配新的空間 (通常是兩倍),使用者無需操心!

std::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時,要隨時確認操作的合法性!

std::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,也就是刪除所有元素。

std::vector

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

std::vector

與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

小結語

std::vector算是最常被使用的STL container之一。

array能做到的事情,vector也能做到

vector能做到的事情,array不一定能做到

小缺點: vector的速度比array略慢。但大部分的時候這個差距很小,可忽略

vector的介紹到此結束?

還有更多!

std::iterator

std::iterator

1. 概念很類似指標

2. 可以想像成STL專用的指標

3. 根據不同的container,對應的iterator實作方式不盡相同。大部分的時候iterator != 先前學過的指標 (是更複雜的內容)

4. 底下的實作很複雜,我們只需要知道怎麼操作即可。

std::iterator

以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::iterator

以std::vector<int>為例

一個簡單的圖例 (credit: Arvin)

v.end()是沒有內容的!!

std::iterator

以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::iterator

以std::vector<int>為例

有了iterator之後...

操作 效果
v.insert(iter, x) 在iter的地方插入x
v.erase(iter) 刪除iter所指的的元素

std::iterator

以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::iterator

以std::vector<int>為例

排序vector(由小到大)

sort(v.begin(), v.end())

由大到小?

reverse(v.begin(), v.end())

std::iterator

iterator注意事項

v.push_back()

翻譯:如果v.push_back()使得vector需要更多空間(size超過capacity),程式會自動尋找另一塊記憶體來存放更長的vector。這時原本的iterator都會失效。

std::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;
//???

std::iterator

iterator注意事項

對vector進行各種操作(例如push_back, insert, erase....)後,都可能影響到原本iterator的validity。

 

在使用iterator時要特別注意現在的iterator是否還是可以使用的。

 

什麼時候哪些操作會影響iterator validity? 多看使用說明書

實用小技巧

auto

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

auto

Q: 如何使用C++11?

A: 編譯時加上參數

範例:

g++ -std=c++11 <filename.cpp>

另有

c++14

c++17

c++2a (實驗階段)

auto

使用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

auto

注意事項

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

for-each loop

同樣是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裡印出來

for-each loop

這裡的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

for-each loop

可以搭配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

std::stack

std::stack

FILO (First in Last out): 先進後出

std::stack

操作 說明 複雜度
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)

常用指令

std::stack

範例

#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
}

std::stack

push跟pop好像在哪裡看過?

其實stack支援的功能,vector也都包了,所以其實可以用vector來模擬stack。而且通常vector的速度會再更快一些。

 

不過這裡最重要的是stack的FILO概念,在資工/程式的世界非常常見&常用。

小練習

std::queue

std::queue

FIFO (First in First out): 先進先出

std::queue

操作 說明 複雜度
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)

常用指令

std::queue

範例

#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
}

小練習

std::deque

std::deque

stack: 只有一個開口,進出共用,先進後出

queue: 有兩個開口,一個只進不出,另一個只出不進,先進先出

deque: 兩個開口,都可以進出,結合stack與queue

std::deque

常用指令

操作 說明 複雜度
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)

std::deque

範例

#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
}

std::deque

小討論

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

std::list

std::list其實就是double linked list

std::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)

常用指令

std::list

警告

list的iterator與pointer不同,有些操作不能使用

list<int> lst(3);
auto iter = lst.begin();
iter++; // ok
iter += 1; // error

list的iterator可以使用iter++/iter--,但不能使用iter += k這樣的指標運算

std::list

範例

#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
}

std::list

小評論

list也包含許多功能,底下就是double linked list。

但list的速度偏慢。

但vector與list本身就用來處理不同的問題,沒有互相取代的問題。

總結

整理表

credit: Arvin

結語

不同資料結構有不同的功能,依據不同的情況選擇適合的資料結構有助於讓程式運作得更好!

怎麼知道什麼時候用什麼資料結構?

多寫題目、多練習

Bonus!!

std::pair

std::pair

將兩個變數綁在一起,可以為不同類型。

操作 說明
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()

std::pair

範例

#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
}

push or emplace?

在使用vector時,若要將元素放到最後面,可以使用push_back()與emplace_back()兩種方式。

v.push_back(element): 先複製一個與element一樣的元素,接著放到v的最後面。

v.emplace_back(arg): 使用提供的arg,直接在v的最後面用constructor建立一個新的元素。

push or emplace?

範例:

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()相同

push or emplace?

大部分的時候,使用emplace_back()會比push_back()略快一些,因為少了一次複製的動作。

但emplace_back()主要的用途不在於速度(push_back()已經很快了),而是它能讓我們在container裡面建立noncopyable的元素

noncopyable? 有興趣的同學自行研究囉

std::tuple

std::tuple

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,....組成

std::tuple

#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
}

範例

std::tuple

#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內的東西取出來

structured binding

structured binding

Since C++17

Q: 如何使用C++17?

A: 編譯時加上參數

範例:

g++ -std=c++17 <filename.cpp>

structured binding

主要可以用在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;
}

structured binding

也可以搭配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;
}

structured binding

跟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

Sprout 2020 C/C++ STL

By JT

Sprout 2020 C/C++ STL

  • 1,201