"pear""kiwi""fig" 3060
PREREQ P6

mapset迭代器

先備知識 · 語法與互動
m[k]|count/find|set 去重|有序 vs 雜湊|begin() / end()|m[k]++
向下捲動開始互動
📌 本頁使用方式(先備知識 · 語法與互動)

先讀語法與例子,再操作互動圖解並完成四選一自測。忘記寫法時回看速查表,最後用中英詞彙卡複習。

CONTENTS · 內容目錄
PROLOGUE · 開場

用名字查東西

vector 用編號查:v[3] 是「第 4 格」。可是現實裡你想查的通常是「"pear" 賣多少錢」「"A" 這個頂點連到誰」。名字不是編號,沒辦法當索引用,於是你只剩一條路:從頭一筆一筆比對過去。

那條路有多慢,跑一次就知道:

同樣一組 10 萬筆資料,查 1000 次(真的跑出來的)
#include <iostream> #include <vector> #include <unordered_set> #include <chrono> using namespace std; int main() { const int N = 100000, Q = 1000; vector<int> v; unordered_set<int> s; for (int i = 0; i < N; i++) { v.push_back(i * 2); s.insert(i * 2); } int linearHits = 0, hashedHits = 0; // 一:用 vector 從頭找到尾,查 Q 個不存在的鍵 auto t0 = chrono::steady_clock::now(); for (int q = 0; q < Q; q++) for (int i = 0; i < N; i++) if (v[i] == q * 2 + 1) { ++linearHits; break; } auto t1 = chrono::steady_clock::now(); // 二:用 unordered_set 查表,查同樣那 Q 個鍵 for (int q = 0; q < Q; q++) hashedHits += s.count(q * 2 + 1); auto t2 = chrono::steady_clock::now(); long a = chrono::duration_cast<chrono::microseconds>(t1 - t0).count(); long b = chrono::duration_cast<chrono::microseconds>(t2 - t1).count(); cout << "N=" << N << ",查詢 Q=" << Q << ",預期命中 0" << endl; cout << "vector:命中 " << linearHits << ",耗時 " << a << " us" << endl; cout << "unordered_set:命中 " << hashedHits << ",耗時 " << b << " us" << endl; cout << "本次耗時比: " << a / (b ? b : 1) << "" << endl; return 0; }
輸出形式
N=100000,查詢 Q=1000,預期命中 0
vector:命中 0,耗時 [依環境而異] us
unordered_set:命中 0,耗時 [依環境而異] us
本次耗時比:[依環境而異] 倍

兩段都查詢 1000 個不存在的奇數鍵,所以命中數應同為 0;把計數印出來也讓查詢結果成為可觀察行為,避免最佳化器直接刪除無人使用的工作。vector 每次最壞掃過 N 筆,unordered_set 則提供平均常數時間查詢;實際耗時與比值受編譯選項、機器及雜湊實作影響。

O(n)
vector 線性搜尋
O(1)
unordered_set 平均
O(log n)
map 有序版本

map 與 set 提供依鍵查詢的介面。先看資料要保存鍵值還是只有鍵,再看是否需要順序,才能挑選合適的容器。

⚠️ 使用哪個標準元件,就引入哪個標頭

不要依賴其他標頭碰巧間接引入宣告。使用 mapsetunordered_mapunordered_set 或迭代器工具時,應分別明確引入對應標頭。

所以本頁每一段可執行的範例都寫成完整程式#includemain()),你可以整段複製貼上。另外注意:#include 不能被 kernel 的自動包裝塞進 main() 裡面,一格裡有 #include 就把整支程式(含 main)寫完整。詳細規矩看 00B · 環境設定 的 FAQ。

忘記 #include <map> 時的編譯訊息(前三行)
#include <iostream> using namespace std; int main() { map<string, int> price; // 忘了 #include <map> price["pear"] = 30; cout << price["pear"] << endl; return 0; }
編譯訊息
[編譯失敗]
cell.cpp: In function ‘int main()’:
cell.cpp:5:5: error: ‘map’ was not declared in this scope
    5 |     map<string, int> price;      // 忘了 #include <map>

<iostream> 不會順便把 map 帶進來。看到 ‘map’ was not declared in this scope,漏寫 #include <map> 是常見原因之一;補上標頭後若仍失敗,再依診斷位置檢查語法。

📌 本頁定位
PART 01 · map 基礎

map<K, V>:一張鍵對值的表

宣告的時候要講清楚兩件事:鍵長什麼樣、值長什麼樣。map<string, int> 就是「用字串查整數」。Python 的 dict 不必先講,因為它同一個容器裡可以塞各種型別;C++ 的容器是同質的,型別在編譯期就釘死。

pair<K, V> 把一個鍵與一個值包成一組,成員名稱是 firstsecondmake_pair(k, v) 會依引數建立這一組資料;獨立使用時應明確引入 <utility>。下面先用它把 "kiwi"60 一起交給 insert

map 的六個基本動作
#include <iostream> #include <map> #include <string> #include <utility> using namespace std; int main() { map<string, int> price; price["pear"] = 30; // 用 [] 放進去 price["apple"] = 45; price.insert(make_pair("kiwi", 60)); // 用 insert 放進去 cout << "size = " << price.size() << endl; cout << "apple 賣 " << price["apple"] << endl; cout << "有沒有 kiwi? " << price.count("kiwi") << endl; cout << "有沒有 mango? " << price.count("mango") << endl; price["apple"] = 50; // 同一個鍵再指派=覆蓋 cout << "改價後 apple 賣 " << price["apple"] << ",size 還是 " << price.size() << endl; price.erase("pear"); cout << "刪掉 pear 後 size = " << price.size() << endl; return 0; }
預期輸出
size = 3
apple 賣 45
有沒有 kiwi? 1
有沒有 mango? 0
改價後 apple 賣 50,size 還是 3
刪掉 pear 後 size = 2

四件事值得注意:① map<string, int> 的兩個型別是鍵的型別值的型別,順序不能反。② insert 要包成一組 pair,所以有 make_pair。③ count 只回傳 0 或 1(map 的鍵不會重複),拿來當「在不在」的判斷剛好。④ 同一個鍵再指派一次是覆蓋size() 不會變。

你想做的事寫法回傳什麼
放進去/改掉m[k] = v;將值指定給 m[k];整個指定運算式也可產生所指定的值,通常直接當一行敘述使用
放進去(已存在就不動)m.insert(make_pair(k, v));一組 pair:迭代器 + 有沒有成功放進去
問「在不在」m.count(k)0 或 1
找到並拿出來m.find(k)迭代器,找不到時等於 m.end()
刪掉一個鍵m.erase(k);刪掉幾筆(0 或 1)
問有幾筆m.size()目前的鍵數
清空m.clear();
count 與 find 的分工

find 回傳的不是值,而是一個表示容器位置的迭代器。找到時它指向一組 pair,其中 first 是鍵、second 是值;找不到時則等於尾後哨兵 end(),不可解參考。PART 05 會再系統整理迭代器的走訪語法。

#include <iostream> #include <map> #include <string> using namespace std; int main() { map<string, int> price; price["pear"] = 30; // count:只想知道在不在 if (price.count("pear")) cout << "count 說 pear 在" << endl; // find:想知道在不在,順便把值拿出來 map<string, int>::iterator it = price.find("pear"); if (it != price.end()) cout << "find 找到 " << it->first << " = " << it->second << endl; map<string, int>::iterator miss = price.find("mango"); if (miss == price.end()) cout << "find 找不到 mango,回傳的是 end()" << endl; return 0; }
預期輸出
count 說 pear 在
find 找到 pear = 30
find 找不到 mango,回傳的是 end()

count 回答「在不在」,find 回傳一個迭代器:找到就指著那一組資料,找不到就等於 end()。想拿值就用 find,只想判斷就用 count,免得同一個鍵查兩次。it->first 是鍵、it->second 是值,這兩個名字後面會一直出現。

💡 鍵的型別有一個條件

map 內部靠「比大小」把鍵排好,所以鍵的型別必須支援 <intdoublestring 都內建有,直接可以用。等你到 P9 想把自己寫的類別當鍵,就得自己多載 operator<,否則編譯器會產生冗長的樣板診斷訊息。

PART 02 · operator[] 的陷阱

m[k] 查不到,會自己建一個 與 Python 差最多的一點

Python 的 d["mango"] 查不到會立即丟出 KeyError。C++ 的 m["mango"] 則會靜默插入一筆,再回傳那筆的值;若原意只是查詢,這項改動可能到較後面的結果才被發現。

只是查一下,size 卻自己長大了
#include <iostream> #include <map> #include <string> using namespace std; int main() { map<string, int> price; price["pear"] = 30; cout << "一開始 size = " << price.size() << endl; // 只是「查」一個不存在的鍵而已 cout << "查 mango:" << price["mango"] << endl; cout << "查完 size = " << price.size() << endl; // 再查兩個不存在的 price["mango"]; price["guava"]; price["lychee"]; cout << "又查三個之後 size = " << price.size() << endl; // 正確的查法:count 或 find,不會動到容器 cout << "count 查 durian:" << price.count("durian") << endl; cout << "用 count 查完 size = " << price.size() << endl; return 0; }
預期輸出
一開始 size = 1
查 mango:0
查完 size = 2
又查三個之後 size = 4
count 查 durian:0
用 count 查完 size = 4

從 1 變 2、再變 4。m[k] 查不到就會當場建一個,值是預設初始化的結果:int 是 0、double 是 0.0、string 是空字串、vector 是空的。所以印出來的 0 不是「查不到」,是「剛剛替你建了一個 0」。最後兩行是對照組:count 查完 size 一動也不動。

查一個不存在的鍵Python dictC++ map
會發生什麼丟出 KeyError,程式中斷建一個新鍵,值是預設初始化的結果
容器大小不變加一
不想動到容器的查法d.get(k) 或 k in dm.count(k)|m.find(k)
唯讀情境照樣可以用中括號const map 不能用 [],編譯器直接擋下來
兩種查法 CODE
if (m[k] != 0) { ... } // 會建鍵 if (m.count(k)) { ... } // 不會建鍵
什麼時候該用哪個 KEY

確定要寫入(指派、++push_back)就用 m[k],它順便建鍵反而幫了你的忙。
只是要讀或判斷就用 countfind

隨堂 1 · 這段程式跑完 size 是多少

一個空的 map<string, int> m;,接著執行:m["a"] = 1;if (m["b"] > 0) cout << "yes";m.count("c");。最後 m.size() 是?

(A) 1
(B) 2
(C) 3
(D) 程式會丟出例外
✅ 現代 C++ 對照

若要求鍵必須存在,可用 m.at(k),找不到會丟 out_of_range。C++17 可用 count 或 find 做存在性判斷;contains 是較新版本的介面,不在本頁 C++17 範例使用。

PART 03 · set

set<T>:一個「這個我見過沒有」的容器

三個特性,一次到手:不重複自動排序查得快。你只要問它「這個在不在」,它不存值。

丟了五次 apple,裡面只留一個
#include <iostream> #include <set> #include <string> using namespace std; int main() { set<string> seen; seen.insert("pear"); seen.insert("apple"); seen.insert("kiwi"); seen.insert("apple"); // 重複的,直接被吃掉 seen.insert("apple"); cout << "丟了 5 次,size = " << seen.size() << endl; cout << "走訪:"; for (set<string>::iterator it = seen.begin(); it != seen.end(); ++it) cout << *it << " "; cout << endl; cout << "apple 在嗎? " << seen.count("apple") << endl; seen.erase("apple"); cout << "erase 之後還在嗎? " << seen.count("apple") << ",size = " << seen.size() << endl; return 0; }
預期輸出
丟了 5 次,size = 3
走訪:apple kiwi pear
apple 在嗎? 1
erase 之後還在嗎? 0,size = 2

set 就是只有鍵、沒有值map:同樣不重複、同樣自動排序、同樣用 count 查、用 erase 刪。差別只在 insert 直接丟元素進去,以及走訪時 *it 就是元素本身,不必再拆 firstsecond。注意走訪印出來是 apple kiwi pear —— 字典序,不是丟進去的順序。

操作set<T>map<K, V>
放進去s.insert(x);m[k] = v;
查在不在s.count(x)m.count(k)
刪掉s.erase(x);m.erase(k);
走訪時 *it元素本身一組 pair(firstsecond
重複丟同一個只留一份,size 不變值被覆蓋,size 不變
💡 去重的一行寫法

把資料加入 set,再讀 size 就能得到不同值的數目。也能用 set 記錄已處理的識別碼,避免同一項重複處理;加入與查詢是兩種不同操作。

✅ 有序容器用「排序等價」,不一定呼叫 operator==

setmap 依比較器判斷兩個鍵是否屬於同一位置:若 !(a < b) && !(b < a),兩者便視為等價鍵。這和直接計算 a == b 是不同機制;自訂比較器必須維持一致且符合嚴格弱序。

PART 04 · 有序 vs 無序

兩套實作,同一組介面

map 與 unordered_map 的常用介面相近,但契約不同:map 依比較器維持鍵的次序,unordered_map 依雜湊與相等判定查找鍵。這裡比較介面、順序與查詢成本;使用這些類別不需要先實作內部資料結構。

同一組鍵,兩種容器走訪出來的順序(真的跑出來的)
#include <iostream> #include <map> #include <unordered_map> #include <string> using namespace std; int main() { string keys[8] = {"kiwi", "apple", "durian", "pear", "mango", "guava", "lychee", "fig"}; map<string, int> ordered; unordered_map<string, int> hashed; for (int i = 0; i < 8; i++) { ordered[keys[i]] = i; hashed[keys[i]] = i; } cout << "插入順序 : "; for (int i = 0; i < 8; i++) cout << keys[i] << " "; cout << endl; cout << "map 走訪 : "; for (map<string, int>::iterator it = ordered.begin(); it != ordered.end(); ++it) cout << it->first << " "; cout << endl; cout << "unordered_map : "; for (unordered_map<string, int>::iterator it = hashed.begin(); it != hashed.end(); ++it) cout << it->first << " "; cout << endl; cout << "桶數 bucket_count = " << hashed.bucket_count() << endl; return 0; }
輸出形式
插入順序      : kiwi apple durian pear mango guava lychee fig
map 走訪      : apple durian fig guava kiwi lychee mango pear
unordered_map : [順序未指定]
桶數 bucket_count = [由實作與目前狀態決定]

map 依鍵的大小順序走訪;unordered_map 的走訪順序未指定。bucket_count() 回傳目前的桶數,由標準函式庫實作與容器的容量狀態決定。

INSERT 插入順序
kiwi
apple
durian
pear
mango
guava
lychee
fig
map 走訪(依鍵排序)
apple
durian
fig
guava
kiwi
lychee
mango
pear
unordered_map 走訪(走訪順序未指定)
fig
lychee
mango
durian
pear
apple
guava
kiwi
同一組鍵、同一個插入順序,map 走出來是字典序,unordered_map 走訪順序未指定,不保證保留插入順序。
底下差在哪 WHY

map 的一般查詢具有 O(log n) 複雜度;unordered_map 平均為 O(1),最壞可能為 O(n)。這是成長速度的比較,不表示每次實測後者都較快,仍受資料量、鍵與實作影響。

走訪順序不保證 WARN

右欄那個順序只是這台機器、這個版本跑出來的結果。你在自己的 notebook 跑,很可能得到另一個順序,那是正常的。

有序才做得到的三件事
#include <iostream> #include <map> #include <string> using namespace std; int main() { map<int, string> score; score[91] = "Ann"; score[58] = "Bob"; score[77] = "Cid"; score[64] = "Dee"; // 插入順序是 91 58 77 64,走訪出來卻是排好的 cout << "走訪: "; for (auto& [s, name] : score) cout << s << ":" << name << " "; cout << endl; // 有序才做得到的事:最小、最大、第一個 >= 70 的 cout << "最低分 " << score.begin()->first << endl; cout << "最高分 " << score.rbegin()->first << endl; cout << "第一個不低於 70 的: " << score.lower_bound(70)->first << endl; return 0; }
預期輸出
走訪: 58:Bob 64:Dee 77:Cid 91:Ann
最低分 58
最高分 91
第一個不低於 70 的: 77

插入順序是 91、58、77、64,走訪卻是 58、64、77、91,因為預設 map 依鍵排序。非空時 begin() 指向最小鍵、rbegin() 指向最大鍵;lower_bound(70) 找第一個不小於 70 的鍵,找不到則回傳 end(),不可直接解參考。unordered_map 沒有這些排序保證。

什麼時候用哪個

你的需求選誰為什麼
只是查得到就好,量很大unordered_map平均常數時間查詢;實際速度仍依資料與環境而異
要照鍵的順序輸出map走訪本身就是排好的,省一次排序
要「最小的鍵」「第一個不小於 x 的鍵」mapbegin() 取得最小鍵需非空;lower_bound 查下界。無序版的 begin() 不代表最小鍵,也沒有 lower_bound。
鍵是自己寫的類別map多載一個 operator< 就好;雜湊版要自己寫雜湊函式,麻煩得多
要求最壞情況也穩定map標準保證對數時間;雜湊表碰撞嚴重時最壞可退化成線性時間
作業沒特別要求map輸出順序固定,比對答案時不會因為走訪順序不同而看起來像錯的
PART 05 · 迭代器

begin()end():所有 STL 容器的共同語言

迭代器是一個「指著容器裡某個位置」的東西,用起來像指標:*it 取值、++it 往前一格、it-> 存取成員。學會這四個符號,vectorsetmaplist 就全部會走訪了。

寫法意思要注意
c.begin()指向第一個元素容器是空的時候,它等於 end()
c.end()指向最後一個的再下一格不能解參考,它只是個哨兵
++it往後移一格慣例寫前置 ++it,不寫 it++:後置版會多複製一份
*it取出它指著的東西map 拿到的是 pair,setvector 拿到的是元素
it->first等同 (*it).first跟指標的 -> 完全同一個符號
it != c.end()還沒走完迴圈條件用 != 不用 <:樹和雜湊表的迭代器沒有大小可比

[begin, end):左閉右開,end() 站在門外

end() 不是最後一個元素,是最後一個元素的再下一格。整個範圍是「含頭、不含尾」, 這種區間叫左閉右開 —— C++ 所有吃一對迭代器的東西(走訪、P5 的範圍建構式、sort)都是這個慣例:

begin() 10 20 30 沒有元素 0 1 2 end() 哨兵, 不可 *

v = {10, 20, 30}begin() 指第一格,end() 指「索引 3」那個不存在的位置。所以 vector<int> sub(v.begin()+1, v.begin()+3) 抄到的是索引 1、2 —— 20 30,尾端那格不含。

它像指標,但不是指標

vector 來說,迭代器的行為特別像指標:連 begin() + i 這種「往後跳 i 格」都支援 (P4 的指標算術),所以 *(v.begin() + 2) 拿到第三個元素。 但別的容器就不行了 —— 差別整理成一句:

✅ 一句話心法

指標是記憶體位址;迭代器是「用來走訪容器的位置物件」。元素連續排列的 vectorstring,迭代器可以 + i 隨機跳;listmapset 的元素散在各處,迭代器只能 ++it 一步一步走,寫 L.begin() + 1 直接編譯錯誤 (no match for 'operator+')。這也是上表說迴圈條件用 != 不用 < 的原因。

解參考、往後跳、範圍建構,一次驗證
#include <iostream> #include <list> #include <vector> using namespace std; int main() { vector<int> v = {10, 20, 30}; auto it = v.begin(); cout << "*it = " << *it << endl; // 解參考:跟指標同一個符號 cout << "*(begin()+2) = " << *(v.begin() + 2) << endl; // vector 可以隨機跳 vector<int> sub(v.begin() + 1, v.begin() + 3); // [1, 3):含頭不含尾 cout << "sub = " << sub[0] << " " << sub[1] << endl; list<int> L = {10, 20, 30}; auto lit = L.begin(); ++lit; // list 只能一步一步走;L.begin() + 1 編譯不過 cout << "list ++lit = " << *lit << endl; return 0; }
預期輸出
*it          = 10
*(begin()+2) = 30
sub          = 20 30
list ++lit   = 20

第 12 行對照上面的圖:[begin()+1, begin()+3) 抄索引 1、2,尾端不含。把第 17 行改成 auto x = L.begin() + 1; 會得到 no match for 'operator+' —— 迭代器像指標到什麼程度,是各容器自己決定的

隨堂 · 迭代器與指標的分界

list<int> L = {10, 20, 30}; 之後寫 cout << *(L.begin() + 1); 會發生什麼事?

(A) 印出 20
(B) 編譯錯誤:no match for 'operator+'
(C) 執行期當掉(segfault)
(D) 印出容器元素個數
三種容器,同一套走訪語法
#include <iostream> #include <set> #include <map> #include <vector> #include <string> using namespace std; int main() { vector<int> v; v.push_back(31); v.push_back(17); v.push_back(93); set<int> s; s.insert(31); s.insert(17); s.insert(93); map<string, int> m; m["pear"] = 30; m["apple"] = 45; // 同一套語法:begin() / end() / ++it / *it cout << "vector: "; for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) cout << *it << " "; cout << endl; cout << "set : "; for (set<int>::iterator it = s.begin(); it != s.end(); ++it) cout << *it << " "; cout << endl; // map 的 *it 是一組 pair,所以改用 it->first / it->second cout << "map : "; for (map<string, int>::iterator it = m.begin(); it != m.end(); ++it) cout << it->first << "=" << it->second << " "; cout << endl; return 0; }
預期輸出
vector: 31 17 93
set   : 17 31 93
map   : apple=45 pear=30 

vector 印出來是插入順序、set 是排好的、map 是照鍵排好的,但三個迴圈長得一模一樣。這就是迭代器的價值:容器內部是陣列、樹還是雜湊表,走訪的人不必知道。map 唯一的不同是 *it 拿到的是一組 pair,所以改用 it->firstit->second

⚠️ 走訪途中不要亂刪

erase 掉一個元素之後,指著它的迭代器就失效了,跟 P4 的懸空指標是同一種病。正確寫法是 it = m.erase(it);erase 會回傳下一格):刪的時候用 erase 的回傳值前進,沒刪才 ++it。標準句型:for (auto it = m.begin(); it != m.end(); ) { if (cond) it = m.erase(it); else ++it; }

<algorithm>:把一對迭代器交給現成的演算法

sortfind 這些不是容器的成員函式,你不會寫 v.sort()。它們是 <algorithm> 這個標頭裡的自由函式, 你要做的是把「從哪到哪」用一對迭代器告訴它:sort(v.begin(), v.end()) 的意思是「把 [begin, end) 這一段排好」。 這正是左閉右開慣例的回報 —— 演算法只認得迭代器,不認得容器,所以同一套 findcount 通吃 vectorstring、陣列, 一個都不用重寫。

寫法做什麼獨立使用方式
sort(b, e)把這一段由小到大排好拿來對照自己寫的排序:sort(aList.begin(), aList.end()); // the STL built-in sort
find(b, e, x)x回傳迭代器找不到就回傳 e(也就是 v.end())——不是 -1
reverse(b, e)把這一段前後反轉進位轉換:餘數是從低位算出來的,最後要反過來印
count(b, e, x)x 出現幾次統計字元/元素出現次數
min(a, b)/max(a, b)
swap(a, b)
兩個取小、取大、交換動態規劃 minCoins = min(numCoins, minCoins);、 heap swap(heap[i], heap[parentIdx]);
這三個吃的是值,不是迭代器swap 正式的家在 <utility>,但常跟這群一起用)
🔑 慣用法:判斷「在不在」要跟 end() 比

find 沒有辦法回傳「找不到」這種特別的數字,因為容器裡什麼值都可能是合法元素。它的答案是位置,所以找不到時它回傳門外那一格 —— 就是上面圖裡那個哨兵 end()。於是判斷成員的標準寫法長這樣:

if (find(v.begin(), v.end(), 15) != v.end()) { /* 有 15 */ }

查一般序列可使用 find(values.begin(), values.end(), target),並與 end 比較。std::find 會逐項掃描;map/set 的成員 find 則利用容器本身的查詢結構。相同名字不等於相同成本。

sort → find → reverse → count,一段 vector 全跑一遍
#include <iostream> #include <algorithm> #include <vector> using namespace std; int main() { vector<int> v = {31, 17, 93, 17, 55}; sort(v.begin(), v.end()); // 一對迭代器 = 排這一段 cout << "sorted : "; for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) cout << *it << " "; cout << endl; vector<int>::iterator hit = find(v.begin(), v.end(), 55); if (hit != v.end()) // 找得到:hit 指著那一格 cout << "find 55 : *it = " << *hit << ",索引 " << (hit - v.begin()) << endl; vector<int>::iterator miss = find(v.begin(), v.end(), 99); if (miss == v.end()) // 找不到:回傳的就是門外那一格 cout << "find 99 : 找不到,回傳的就是 end()" << endl; reverse(v.begin(), v.end()); cout << "reversed: "; for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) cout << *it << " "; cout << endl; cout << "count 17: " << count(v.begin(), v.end(), 17) << "" << endl; cout << "min/max : " << min(3, 8) << " / " << max(3, 8) << endl; return 0; }
預期輸出
sorted  : 17 17 31 55 93
find 55 : *it = 55,索引 3
find 99 : 找不到,回傳的就是 end()
reversed: 93 55 31 17 17
count 17: 2 次
min/max : 3 / 8

第 16 行的 hit - v.begin() 就是「這是第幾格」:兩個迭代器相減得到距離,這也是 vector 迭代器像指標才有的特權。 最後補一句限制:sort 需要迭代器能隨機跳,所以 sort(L.begin(), L.end())list 編譯不過(list 有自己的 L.sort()), 而 setmap 本來就排好了,根本不需要排。

隨堂 · find 找不到的時候

vector<int> v = {10, 20, 30}; 執行 find(v.begin(), v.end(), 99);,這個 99 不在裡面。它回傳什麼?

(A) v.end()
(B) -1
(C) 丟出例外
(D) v.begin()
PART 06 · 走訪 map

it->firstauto& [k, v]

map 裡每一筆資料是一組 pair<const K, V>first 是鍵、second 是值。走訪就是把這些 pair 一組一組拿出來。

auto 與範圍 for:各省下什麼?

語法糖(syntax sugar)讓常見操作有更簡潔的寫法。這裡分成兩件事:auto 讓編譯器從初值推導型別,省下型別名稱;範圍 for 則把 begin()end()、前進與取元素的步驟包起來,省下走訪的迴圈控制。兩者可以分開使用。

① 完整 iterator:先看每一步在做什麼
#include <iostream> #include <map> #include <string> #include <utility> using namespace std; int main() { map<string, string> capitals = { {"Iowa", "Des Moines"}, {"Wisconsin", "Madison"} }; cout << capitals["Iowa"] << endl; capitals["Utah"] = "Salt Lake City"; capitals["California"] = "Sacramento"; cout << capitals.size() << endl; for (map<string, string>::iterator it = capitals.begin(); it != capitals.end(); ++it) { cout << it->second << " is the capital of " << it->first << endl; } return 0; }
預期輸出
Des Moines
4
Sacramento is the capital of California
Des Moines is the capital of Iowa
Salt Lake City is the capital of Utah
Madison is the capital of Wisconsin

這份 map 以州名作為鍵,依鍵的順序走訪。it 指著目前的位置,*it 是該位置的鍵值對;it->second 就是 (*it).second

把上面程式的 for 迴圈換成以下寫法。capitals.begin() 回傳 map<string, string>::iterator,因此 auto it 的型別在編譯時就已確定。

② auto iterator:只省下型別名稱
for (auto it = capitals.begin(); it != capitals.end(); ++it) { cout << it->second << " is the capital of " << it->first << endl; }

再改成範圍 for,直接為每個元素取名 entry。現在存取的是元素本身,使用 entry.firstentry.second

③ 範圍 for:省下走訪步驟
for (auto& entry : capitals) { cout << entry.second << " is the capital of " << entry.first << endl; }

這三種走訪都會印出相同的州名與首府。auto 與範圍 for 從 C++11 就能使用;後面的 auto& [key, value] 則再加上 C++17 的結構化繫結。

auto 不會替你加上 &

map<string, string> 的元素型別是 pair<const string, string>。決定「拿副本還是改原元素」的是宣告中的 &

範圍 for 裡的宣告entry 的完整型別修改 second 的結果
auto entrypair<const string, string>複製一筆資料,只改到副本
pair<const string, string> entrypair<const string, string>同樣是副本;明寫型別不會省掉複製
auto& entrypair<const string, string>&參考原元素,修改會寫回 map
const auto& entryconst pair<const string, string>&唯讀參考,不複製,也不能透過 entry 修改

所以 for (pair<const string, string> entry : capitals) 能正常印出資料,但 entry.second = "Unknown"; 只會改到副本。要改原本的首府,寫 for (auto& entry : capitals);完整寫法則是 for (pair<const string, string>& entry : capitals)。只要印出資料時,可用 const auto& 避免複製。

把所有首府改成 Unknown
for (auto& entry : capitals) { entry.second = "Unknown"; }

對這個 map,上面的範圍 for 可對照成以下 iterator 寫法。關鍵是每一輪的 auto& entry = *it;;拿掉 &,就會複製目前的元素。

範圍 for 的走訪步驟
for (auto it = capitals.begin(), last = capitals.end(); it != last; ++it) { auto& entry = *it; entry.second = "Unknown"; }

元素中的 firstconst string,即使使用 auto&,也不能改鍵;second 才是可修改的值。程式裡把迴圈變數命名為 entry,可以清楚區分變數名稱與型別名稱 pair

再把鍵和值各取一個名字

走訪 map 的三種寫法,結果完全一樣
#include <iostream> #include <map> #include <string> using namespace std; int main() { map<string, int> price; price["pear"] = 30; price["apple"] = 45; price["kiwi"] = 60; // 寫法一:迭代器 for (map<string, int>::iterator it = price.begin(); it != price.end(); ++it) cout << it->first << " " << it->second << " / "; cout << endl; // 寫法二:range-for,p 是一組 pair for (auto& p : price) cout << p.first << " " << p.second << " / "; cout << endl; // 寫法三:C++17 結構化繫結,直接拆成兩個名字 for (auto& [name, dollars] : price) cout << name << " " << dollars << " / "; cout << endl; // 加上 & 才改得動;沒有 & 就是複製一份 for (auto& [name, dollars] : price) dollars += 5; cout << "全部漲五元後:"; for (auto& [name, dollars] : price) cout << name << "=" << dollars << " "; cout << endl; return 0; }
預期輸出
apple 45 / kiwi 60 / pear 30 /
apple 45 / kiwi 60 / pear 30 /
apple 45 / kiwi 60 / pear 30 /
全部漲五元後:apple=50 kiwi=65 pear=35 

三行輸出一模一樣,選最好讀的那個就好。結構化繫結(C++17)把 pair 當場拆成兩個有名字的變數,比 p.firstp.second 好懂太多。最後一段是關鍵:auto&& 不能省,少了它你改的是複製品,原本的 map 不會變。順帶一提,鍵是唯讀的 —— 改了鍵,樹的排序就壞了,所以編譯器直接禁止。

寫法長什麼樣什麼時候用
迭代器for (map<string,int>::iterator it = m.begin(); it != m.end(); ++it)要在迴圈裡刪東西,或需要拿到位置
range-for + pairfor (auto& p : m)C++11 起可用,最通用
結構化繫結for (auto& [k, v] : m)C++17 起可用,最好讀,優先選這個
🎯 一個 & 的差別

for (auto p : m):每一輪複製一組 pair,改了不算數,而且鍵是 string 的話每輪都在複製字串。
for (auto& p : m)參考本尊,改得動,也不複製。
for (const auto& p : m):參考但唯讀,只是要印出來時最安全。這三個差別在 P3 講過,這裡是它最常出現的場合。

PART 07 · 三個常用組合

計數、去重、分組

計數、去重與分組都可以用已學過的容器操作組合起來,不需要自己實作容器內部。先看一段完整程式,再比較每個核心敘述會不會新增鍵、會不會改變元素數量。

計數、去重、分組:一段程式跑完三個
#include <iostream> #include <map> #include <set> #include <vector> #include <string> using namespace std; int main() { string words[8] = {"pear", "apple", "pear", "kiwi", "apple", "pear", "fig", "kiwi"}; // 組合一:計數 map<string, int> freq; for (int i = 0; i < 8; i++) freq[words[i]]++; cout << "計數: "; for (auto& [w, n] : freq) cout << w << "x" << n << " "; cout << endl; // 組合二:去重 set<string> uniq; for (int i = 0; i < 8; i++) uniq.insert(words[i]); cout << "去重: " << uniq.size() << " 種 → "; for (auto& w : uniq) cout << w << " "; cout << endl; // 組合三:分組(一個鍵對應多個值) map<int, vector<string>> byLength; for (int i = 0; i < 8; i++) byLength[words[i].size()].push_back(words[i]); cout << "依長度分組:" << endl; for (auto& [len, group] : byLength) { cout << " " << len << " 個字母: "; for (auto& w : group) cout << w << " "; cout << endl; } return 0; }
預期輸出
計數: applex2 figx1 kiwix2 pearx3
去重: 4 種 → apple fig kiwi pear
依長度分組:
  3 個字母: fig
  4 個字母: pear pear kiwi pear kiwi
  5 個字母: apple apple 

計數freq[w]++ 這一行同時處理了兩種情況:沒看過的鍵先被建出來、值預設是 0,再 ++ 變 1;看過的鍵就直接加一。PART 02 說 m[k] 會自己建鍵是個陷阱,在這裡它反而是整個寫法成立的理由。
去重:全丟進 setsize() 就是種類數。
分組byLength[len].push_back(w)[] 先生出一個空的 vector,再往裡面塞。

要做的事容器核心那一行
算每個東西出現幾次map<T, int>freq[x]++;
問「有幾種不同的」set<T>uniq.insert(x);
問「這個處理過了沒」set<T>if (visited.count(x)) continue;
把同類的收在一起map<K, vector<V>>group[key].push_back(v);
一個分類對應多個項目:部門聯絡名單
#include <iostream> #include <map> #include <vector> #include <string> using namespace std; int main() { map<string, vector<string>> members; members["Design"].push_back("Ada"); members["Design"].push_back("Ben"); members["Support"].push_back("Cora"); members["Training"]; // 建立空名單 for (const auto& [department, names] : members) { cout << department << ": "; for (const auto& name : names) cout << name << " "; cout << "(" << names.size() << " people)" << endl; } cout << "departments = " << members.size() << endl; }
預期輸出
Design: Ada Ben (2 people)
Support: Cora (1 people)
Training: (0 people)
departments = 3

members["Design"] 在缺少鍵時建立空 vector,再由 push_back 加入名字。外層 members.size() 是部門數,每份 names.size() 是該部門人數,兩者不可混為一談。Training 雖然沒有成員,仍是已建立的鍵;若只想查部門是否存在,可用 find,避免意外建立空名單。

EX · 隨堂練習

兩題確認觀念

EXERCISE 1 · 選容器

你要統計一份成績單裡每個分數出現幾次,最後從低分到高分印出來。哪一個選擇最省事?

(A) unordered_map<int, int>,走訪直接印
(B) map<int, int>,走訪直接印
(C) set<int>,走訪直接印
(D) vector<int>,每次查都掃一遍
EXERCISE 2 · 這行為什麼能執行

一個空的 map<string, int> freq;,第一次執行 freq["pear"]++;freq["pear"] 明明還不存在,為什麼可以直接 ++

(A) 因為 map 對不存在的鍵會回傳 0
(B) [] 先插入一筆值為 0 的資料,++ 再把它加成 1
(C) 其實會丟出例外,要用 try 包起來
(D) 編譯不會過,必須先 insert
REFERENCE · 速查表

P6 速查表

四個容器怎麼選

mapunordered_mapsetunordered_set
存什麼鍵 + 值鍵 + 值只有鍵只有鍵
組織方式依比較器排序雜湊分桶依比較器排序雜湊分桶
查詢/插入/刪除$O(\log n)$平均 $O(1)$
最壞 $O(n)$
$O(\log n)$平均 $O(1)$
最壞 $O(n)$
走訪順序照鍵排好未指定的走訪順序照鍵排好未指定的走訪順序
鍵的型別要有operator<hash + operator==operator<hash + operator==
要 include<map><unordered_map><set><unordered_set>
lower_boundrbegin沒有沒有
典型用途要排序輸出、要範圍查詢純查表、量大去重、走過沒去重、量大

語法骨架(整段可以直接貼進 notebook)

#include <iostream> #include <map> #include <set> #include <unordered_map> #include <vector> #include <string> using namespace std; int main() { map<string, int> m; m["pear"] = 30; // 放進去(沒有就新增) if (m.count("pear")) { /* 有 */ } // 查在不在,不會建鍵 m.erase("pear"); // 刪掉 set<string> s; s.insert("pear"); // 重複丟不會變多 for (auto& [k, v] : m) // C++17 走訪 cout << k << " " << v << endl; return 0; }

Python ↔ C++ 對照

你在 Python 寫C++ 寫法差在哪
d = {}map<string, int> d;C++ 要先講清楚鍵與值的型別
d[k] = vd[k] = v;一樣
d[k](k 不存在)d[k]Python 丟 KeyError;C++ 建一個新鍵
k in dd.count(k)C++ 回傳 0 或 1,可以直接當條件
d.get(k, 0)d.count(k) ? d[k] : 0C++ 沒有現成的 get,自己判斷
del d[k]d.erase(k);C++ 刪不存在的鍵不會出錯,回傳 0
len(d)d.size()一樣
for k, v in d.items()for (auto& [k, v] : d)C++17 起可用
s = set()set<string> s;C++ 的 set 自動排序,Python 的不排
s.add(x)s.insert(x);一樣
走訪 dict 的順序C++ map 是排序的Python 3.7 起是插入順序;C++ 沒有插入順序的版本

四條記得住的規則

① 要寫入就用 m[k],只是要查就用 m.count(k)
<map><set><unordered_map> 一定要自己 include。
③ 需要排序輸出、需要 lower_bound 就選 map;純查表選 unordered_map
④ 走訪 map 優先寫 for (auto& [k, v] : m),那個 & 不要漏。

延伸閱讀

資源看什麼
cppreference · std::map完整的成員函式清單。查 lower_boundatemplace 的正確用法。
cppreference · std::unordered_mapbucket_countload_factorrehash,會用到這些名詞。
C++ Tutor把 PART 02 那段貼進去,一步一步看 m["mango"] 執行完 map 裡多了什麼。
本站 · 搜尋與排序雜湊表的內部:雜湊函式、碰撞處理、負載因子。
C++ Gossip · 容器與樣板用 map<string, vector<string>> 表示部門名單,比較外層鍵數與內層人數。
C++ Gossip · 容器與樣板比較器與相等判定:確認「排列順序」和「鍵是否相同」的關係。
QUIZ · 自我檢測

自我檢測:map、set 與迭代器 隨堂自測 · 6 題

每個選項都有解說:選錯也點開看看為什麼錯。全對之後再往下翻詞彙卡。

Q1.以下程式印出什麼?
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> m;
    m["pear"] = 30;
    cout << m["mango"] << " ";
    cout << m.count("kiwi") << " ";
    cout << m.size() << endl;
    return 0;
}
Q2.以下程式印出什麼?
#include <iostream>
#include <set>
using namespace std;

int main() {
    set<int> s;
    s.insert(93); s.insert(17); s.insert(93);
    s.insert(31); s.insert(17);
    cout << s.size() << ": ";
    for (set<int>::iterator it = s.begin(); it != s.end(); ++it)
        cout << *it << " ";
    cout << endl;
    return 0;
}
Q3.以下程式印出什麼?
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    string w[5] = {"b", "a", "b", "c", "b"};
    map<string, int> freq;
    for (int i = 0; i < 5; i++) freq[w[i]]++;
    for (auto& [k, n] : freq) cout << k << n << " ";
    cout << endl;
    return 0;
}
Q4.以下程式印出什麼?
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, int> m;
    m["pear"] = 30;
    map<string, int>::iterator it = m.find("kiwi");
    if (it == m.end()) cout << "miss ";
    else cout << it->second << " ";
    cout << m.size() << endl;
    return 0;
}
Q5.以下程式印出什麼?
#include <iostream>
#include <map>
#include <vector>
#include <string>
using namespace std;

int main() {
    map<string, vector<string>> adj;
    adj["A"].push_back("B");
    adj["A"].push_back("C");
    adj["B"].push_back("C");
    cout << adj.size() << " ";
    cout << adj["C"].size() << " ";
    cout << adj.size() << endl;
    return 0;
}
Q6.你要處理一份一百萬筆的資料,只需要不斷查詢「這個鍵在不在」,完全不需要照順序輸出。應該選哪個容器,為什麼?
CARDS · 關鍵詞彙卡

關鍵詞彙卡:點卡片翻面

先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。