Chapter III. 漸入佳境
基礎資料結構
Set 與 Map
作者建中大講義團隊
協作者8e7

集合(Set)


不同的資料結構「擅長」做不同的操作。陣列(或是 vector)可以在 $O(1)$ 的時間內找到第 $i$ 項的數字,卻要花很久的時間插入或刪除元素。Linked List 可以在 $O(1)$ 時間插入跟刪除數字,但是要花很久才能找到某一個數字是否存在。有沒有一種方法可以折衷,讓「修改」跟「查詢」兩類的操作時間都不差?這就是 set 出場的時刻。

set 是一個 STL 提供的二元搜尋樹,內部實作的是紅黑樹(Red-Black Tree),是一個實作複雜,但是可以自動平衡避免退化的資料結構。因為 STL 已經幫你實作出來了,就不需要特別擔心實作細節,直接使用!

set 儲存的資料有以下特點:

set 在插入、查詢、刪除的複雜度都是 $O(\log n)$。看以下程式片段:

set<int> se; // 宣告一個元素型別為 int 的集合
se.insert(1); // 插入元素 1
se.insert(2);
se.insert(5);
cout << se.count(1) << endl; // 查找元素,true
cout << se.count(3) << endl; // false
se.erase(1); // 刪除元素,若集合內不含此元素則回傳 false
cout << *se.find(3) << endl;
// find() 回傳 iterator,若找不到則回傳 se.end()
cout << *se.lower_bound(3) << endl;
cout << *se.upper_bound(3) << endl;
// 因為 set 有序,所以也支援二分搜操作

multiset

STL 也支援可以重複元素的集合,用法與 set 差不多:

multiset<int> se; // 宣告一個元素型別為 int 的 multiset
se.insert(1); // 插入元素 1
se.insert(1); // 可以重複插入
se.insert(5);
cout << se.count(1) << endl; // 計算元素個數,2
cout << se.count(3) << endl; // 0
se.erase(1); 
// 一次刪除所有元素 1,若集合內不含此元素則回傳 false
se.erase(find(5)) // 一次刪除一個元素 5

當然,setmultiset 也支援 size()empty(),在此不做贅述了。

set 的遍歷

set 是一棵二元搜尋樹,當然可以進行遍歷的操作。STL 的二元搜尋樹沒辦法存取一個子樹的左右節點,不過 STL 提供了迭代器(iterator),可以用不同的方式進行遍歷。如果對這個語法感到困惑的話,可以參考基礎資料結構 / Iterator

for (set<int>::iterator it = se.begin(); it != se.end(); it++)
    cout << *it << ' ';

或者用 C++11 後提供的 auto 關鍵字進行尋訪:

for (auto &val: se)
    cout << val << ' ';

習題

不要被梗!

習題
今晚打老虎

奕辰找到了一個神奇的機器,可以做三件事:

  • INSERT $x$,也就是把一個數字輸入進去這個機器

  • QUERY MIN,也就是查詢目前的最小值

  • QUERY MAX,同上,查詢目前的最大值

而且, 只要一個數字被查詢過了,機器就會把他吐出來,代表不在機器中了!

條件限制
  • 同一時間內不會超過 $10^6$ 個數字,
  • 數字可以被 int 存下。

映射(Map)


map 的概念基本上跟 set 的一樣,可是不是存數字而是存兩個值,keyvalue。使用的方法大致上是用 key 這個值當成類似陣列的索引值,裡面存的東西是 value

map 的元素依照 key 排列,所以同理是依照 key 搜尋。跟 set 一樣,mapkey 值不能有重複。

map<string,int> mapp;
// 宣告一個由 string 映射到 int 的 map
mapp.insert({"apple", 1});
// 插入一個 key 值為 "apple" 的節點
mapp["book"] = 2; // 也可以這樣插入
mapp["apple"] = 3; // 改值
cout << mapp.count("apple") << endl; // true
cout << mapp.count("my girlfriend") << endl; // false

for (map<string,int>::iterator it = mapp.begin(); it != mapp.end(); it++)
    cout << it->first << ' ' << it->second << endl;

for (auto &val: mapp)
    cout << val.first << ' ' << val.second << endl;

// apple 3
// book 2

如果要看一個 map 裡面的東西,但是不確定它存不存在(像是以下程式)
if(mapp["orange"] == 2) //...
mapp 就會先開一個 key 為 orange 的點,佔空間!所以要先判斷存不存在:
if(mapp.find("orange") != mapp.end() && mapp["orange"] == 2) //...
才不會浪費許多不必要的記憶體。這個方法也可以用在 value 可能是預設值(像是 $0$)的情況。

multimap

同樣的STL也有 multimap 這種東西,實際上不太常使用,想知道詳細的可以看 CPP Reference

習題

map 是個好用的東西喔!

習題
Hardwood Species
Source:UVa 10226

恭喜成為森林管理者!現在你的第一個工作就是計算森林中樹的比例。有 $N$ 棵樹,給你他們的英文名稱(由大小寫英文字母表示),請輸出他們所佔的比例為何,輸出至小數點後四位。(比例定義為:$\frac{\text{樹出現的次數}}{N}$)

條件限制
  • 至多 $10^4$ 種樹、$10^6$ 棵樹
習題
Playlist
Source:CSES 1141

廣播電台上播了 $n$ 首歌的歌單,第 $i$ 首歌編號為 $k_i$。請求出最長一段,所有歌都相異的連續區間。

條件限制
  • $n \leq 2 \times 10^5$
  • $1 \leq k_i \leq 10^9$