【C++】競程筆記(資料結構:set)
===
[TOC]
程式碼範例參考:NTUCPC Guide,此筆記僅為個人學習用途。
簡介
---
set 是基於紅黑樹(Red-black tree)所實現的,紅黑樹是一種自平衡避免退化的二元搜尋樹,而因為 STL 幫我們實作出來了,所以不用再重造輪子。
所以 set 在查詢、插入、刪除這方面都是對數時間 $O(logn)$ 。
另外 set 也是一種關聯式容器(Associative Container),就是使用 key 來做資料存取(set 以輸入的 value 作為 key,可查找 value 是否存在 set 中)。
關聯式容器也可再細分為有序(ordered)與無序(unordered)。
### set 特性
1. 唯一性:
- set 中的元素不能重複出現,若 insert 重複值,會自動被忽略。
2. 有序性:
- set 中的元素會依照升序(預設)自動排序,可改成降序。
3. 無法直接修改:
- 因為直接修改會破壞 set 的有序性,所以要改的話要先刪除再插入。
### 語法(Syntax)
```
set<T, comp> s;
```
T:在 set 中元素的資料型態。
comp:比較函式。使用 `greater <T>` 可改成降序排序。
s:set 容器名稱。
### 標頭檔
使用前引入標頭檔:`<set>`。
### 基本操作
- 插入操作:
- `insert()`、`emplace()`。
- 存取元素:
- `begin()`、`end()` 用迭代器存取,之後用 `*` Dereference 解參考運算子得到值,也可藉助 `next()`、`advance()` 完成。
- 搜尋元素:
- `find()`,找到元素就回傳他的迭代器,找不到就是回傳 `end()`。
- 遍歷元素:
- range-based for loop 或是用一般的 for(只是要用迭代器)。
- 刪除元素:
- `erase(element)` or `erase(iterator)` or `erase(begin(), end())`
- 計算元素數量:
- `count()`
- set 大小:
- `size()`
- 清空 set:
- `clear()`
### insert() 和 emplace() 的差異
其實就跟 vector 中的 push_back 和 emplace_back() 的差異一樣(基本上就是這個概念)。
insert() 是在函式外面建構好物件在把它插入,emplace() 是在容器裡面就建立好物件,能避免像 insert() 不必要的複製與移動,有更好的效能。
emplace() 對於自訂的資料型態(如 struct、class)會更有優勢,普通的型態如 int、string 等用 insert() 其實就夠了。
操作範例
---
### 1. 插入操作
```cpp=
#include <iostream>
#include <set>
using namespace std;
int main(){
set <int> s;
s.insert(10);
s.insert(5);
s.insert(10);
s.emplace(20);
s.emplace(5);
for (int i : s){
cout << i << " ";
}
return 0;
}
```
輸出結果:
```
5 10 20
```
### 2. 存取操作
```cpp=
#include <iostream>
#include <set>
using namespace std;
int main(){
set <int> s = {3, 1, 4};
auto it = s.begin();
cout << "min : " << *it << '\n';
it = next(it, 2);
cout << "third min : " << *it;
return 0;
}
```
輸出結果:
```
min : 1
third min : 4
```
### 3. 查詢操作
```cpp=
#include <iostream>
#include <set>
using namespace std;
int main(){
set <int> s = {8, 3, 7};
auto it = s.find(3);
if (it != s.end()){
cout << "find!";
}
else{
cout << "this element is not exist.";
}
return 0;
}
```
輸出結果:
```
find!
```
### 4. 刪除操作
```cpp=
#include <iostream>
#include <set>
using namespace std;
int main(){
set <int> s = {10, 20, 30, 40};
s.erase(30);
auto it = s.find(40);
s.erase(it);
s.erase(s.begin(), s.end());
cout << "size of set s : " << s.size();
return 0;
}
```
輸出結果:
```
size of set s : 0
```
### 5. 自訂排序
```cpp=
#include <iostream>
#include <set>
using namespace std;
int main(){
set <int, greater<int>> s = {2, 5, 1, 3, 4};
for (int i : s){
cout << i << " ";
}
return 0;
}
```
輸出結果:
```
5 4 3 2 1
```
multiset
---
multiset 是 STL 提供的序列容器之一,跟 set 一樣屬於關聯式容器(Associative Container),其餘功能基本上與 set 類似,但有一點不一樣就是:
:::info
multiset 是一個可以存放重複元素的容器,而 set 只能存放唯一的元素。
:::
除了這個之外,其他所有特性基本上跟 set 一樣,就連語法、用的函式也是。
習題練習
---
### 今晚打老虎
Source:https://zerojudge.tw/ShowProblem?problemid=a091
為何用 multiset 而非 set?因為題目沒有提到數字不能重複,所以保險起見用 multiset,另外也說不定有可能會出現以下這種測資:
```
1 100
1 100
1 100
2
```
細節還要再更細節!既然都用到 multiset 了,所以不要忘了它是一個升序的容器,最小值就是 `begin()`,最大值為 `prev(end())`!
第 18 行,至於為何要加上 `prev()`,讓迭代器往前一位?
這是因為 multiset 的 `end()` 迭代器回傳結果是最後一個元素再往後一位,直接寫 `end()` 的話會造成未定義行為,所以要加上 `prev()`。
```cpp=
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr);
int op;
multiset <int> ms;
while (cin >> op){
if (op == 1){
int x;
cin >> x;
ms.emplace(x);
}
else if (op == 2){
if (!ms.empty()){
auto it = prev(ms.end());
cout << *it << '\n';
ms.erase(it);
}
}
else{
if (!ms.empty()){
auto it = ms.begin();
cout << *it << '\n';
ms.erase(it);
}
}
}
return 0;
}
```
參考資料
---
[STL container特性介紹.容器的選用 | Medium](https://wither23265.medium.com/stl-container%E7%89%B9%E6%80%A7-cc28fd8bc246)
[Set in C++ STL - GeeksforGeeks](https://www.geeksforgeeks.org/cpp/set-in-cpp-stl/)
[multiset begin() and end() function in C++ STL - GeeksforGeeks](https://www.geeksforgeeks.org/cpp/multiset-begin-and-end-function-in-c-stl/)