C++中的 map vs unordered_map:選錯(cuò)容器讓你的程序慢十倍!
大家好,我是小康。
今天咱們聊一個(gè)看似簡(jiǎn)單卻經(jīng)常被忽視的話題:C++中的map和unordered_map到底有啥區(qū)別?
選錯(cuò)了容器,你的程序可能就慢了 10 倍不止!這可不是危言聳聽,而是實(shí)打?qū)嵉男阅懿罹唷?/p>
一、一個(gè)真實(shí)的"血淚"故事
前幾天我同事小王一臉沮喪地走過(guò)來(lái):"我的程序怎么這么慢啊,數(shù)據(jù)量一大就卡得不行..."
我瞄了一眼他的代碼,發(fā)現(xiàn)他在處理幾十萬(wàn)條數(shù)據(jù)時(shí)用的是map,而不是unordered_map。簡(jiǎn)單改了一下容器類型后,程序速度立馬提升了 8 倍多!
小王震驚了:"啥?就改個(gè)容器名字,速度差這么多?"
是的,就是這么神奇!今天我就帶大家徹底搞清楚這兩個(gè)容器的區(qū)別,以后再也不踩這個(gè)坑。
二、它們到底是啥?
1. map:有序的紳士
map就像一個(gè)有序的字典,它會(huì)自動(dòng)把你放進(jìn)去的鍵值對(duì)按鍵排序。
#include <iostream>
#include <map>
int main() {
std::map<std::string, int> scoreMap;
scoreMap["Zhang"] = 85;
scoreMap["Li"] = 92;
scoreMap["Wang"] = 78;
// 遍歷時(shí)會(huì)按鍵的字母順序輸出
for (constauto& student : scoreMap) {
std::cout << student.first << ": " << student.second << std::endl;
}
return0;
}
// 輸出結(jié)果:
// Li: 92
// Wang: 78
// Zhang: 85
// (按照字母順序)
2. unordered_map:隨性的混世魔王
unordered_map則像個(gè)不講究順序的字典,它只關(guān)心能不能快速找到東西,至于排序?不存在的!
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> scoreMap;
scoreMap["Zhang"] = 85;
scoreMap["Li"] = 92;
scoreMap["Wang"] = 78;
// 遍歷時(shí)輸出順序不確定
for (constauto& student : scoreMap) {
std::cout << student.first << ": " << student.second << std::endl;
}
return0;
}
// 可能的輸出結(jié)果:
// Wang: 78
// Zhang: 85
// Li: 92
// (順序可能每次運(yùn)行都不同)
三、它們內(nèi)部是咋實(shí)現(xiàn)的?
1. map:紅黑樹(有規(guī)有矩的大家族)
map內(nèi)部是用 紅黑樹 實(shí)現(xiàn)的。紅黑樹是一種自平衡的二叉查找樹。
想象一下,如果把map比作一個(gè)圖書館:
- 每本書(鍵值對(duì))都有固定的位置
- 所有書按書名(鍵)字母順序排列
- 要找一本書,圖書管理員會(huì)從中間的書架開始,然后告訴你"往左邊找"或"往右邊找"
- 找書的過(guò)程就像二分查找
// map的簡(jiǎn)化結(jié)構(gòu)示意圖(紅黑樹)
D
/ \
B F
/ \ / \
A C E G
在上面的圖中,每個(gè)字母代表一個(gè)鍵。查找鍵"E"的過(guò)程:
- 從根節(jié)點(diǎn)"D"開始
- "E"比"D"大,向右走到"F"
- "E"比"F"小,向左走到"E"
- 找到了!
2. unordered_map:哈希表(雜亂但高效的倉(cāng)庫(kù))
unordered_map內(nèi)部是用哈希表實(shí)現(xiàn)的。
繼續(xù)用圖書館打比方:
- 這是一個(gè)特殊的圖書館,沒有明顯的排序
- 但圖書管理員有一個(gè)神奇的公式,輸入書名就能直接告訴你書在哪個(gè)架子上
- 你直接去那個(gè)架子就能找到書,不需要一步步查找
- 這個(gè)"神奇公式"就是哈希函數(shù)
// unordered_map的簡(jiǎn)化結(jié)構(gòu)示意圖(哈希表)
桶0: [C] -> [K]
桶1: [A]
桶2:
桶3: [D] -> [H]
桶4: [B]
桶5: [G]
桶6: [F] -> [J]
桶7: [E] -> [I]
在上圖中,每個(gè)字母代表一個(gè)鍵。查找鍵"H"的過(guò)程:
- 計(jì)算"H"的哈希值,假設(shè)結(jié)果為 3
- 直接檢查桶 3
- 桶 3 有一個(gè)鏈表,檢查鏈表中的每個(gè)元素
- 找到"H"!
四、性能對(duì)比:差距到底有多大?
讓我們做個(gè)全面的性能對(duì)比,分別測(cè)試插入、查找、刪除和修改這四種操作:
#include <iostream>
#include <map>
#include <unordered_map>
#include <chrono>
#include <string>
#include <vector>
#include <random>
// 計(jì)時(shí)輔助函數(shù)
template<typename Func>
long long timeOperation(Func func) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto end = std::chrono::high_resolution_clock::now();
returnstd::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}
int main() {
constint COUNT = 100000;
// 準(zhǔn)備隨機(jī)數(shù)據(jù)
std::vector<int> keys;
for (int i = 0; i < COUNT; i++) {
keys.push_back(i);
}
// 打亂順序用于隨機(jī)訪問
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(keys.begin(), keys.end(), g);
std::map<int, int> orderedMap;
std::unordered_map<int, int> unorderedMap;
// 1. 插入性能
auto mapInsertTime = timeOperation([&]() {
for (int i = 0; i < COUNT; i++) {
orderedMap[i] = i * 2;
}
});
auto unorderedMapInsertTime = timeOperation([&]() {
for (int i = 0; i < COUNT; i++) {
unorderedMap[i] = i * 2;
}
});
// 2. 查找性能(順序訪問)
auto mapLookupTime = timeOperation([&]() {
int result = 0;
for (int i = 0; i < COUNT; i++) {
result += orderedMap[i];
}
// 防止編譯器優(yōu)化掉
volatileint dummy = result;
});
auto unorderedMapLookupTime = timeOperation([&]() {
int result = 0;
for (int i = 0; i < COUNT; i++) {
result += unorderedMap[i];
}
// 防止編譯器優(yōu)化掉
volatileint dummy = result;
});
// 3. 查找性能(隨機(jī)訪問)
auto mapRandomLookupTime = timeOperation([&]() {
int result = 0;
for (int key : keys) {
result += orderedMap[key];
}
// 防止編譯器優(yōu)化掉
volatileint dummy = result;
});
auto unorderedMapRandomLookupTime = timeOperation([&]() {
int result = 0;
for (int key : keys) {
result += unorderedMap[key];
}
// 防止編譯器優(yōu)化掉
volatileint dummy = result;
});
// 4. 修改性能
auto mapUpdateTime = timeOperation([&]() {
for (int i = 0; i < COUNT; i++) {
orderedMap[i] = i * 3;
}
});
auto unorderedMapUpdateTime = timeOperation([&]() {
for (int i = 0; i < COUNT; i++) {
unorderedMap[i] = i * 3;
}
});
// 5. 刪除性能
auto mapEraseTime = timeOperation([&]() {
for (int key : keys) {
if (key % 2 == 0) { // 刪除一半的元素
orderedMap.erase(key);
}
}
});
auto unorderedMapEraseTime = timeOperation([&]() {
for (int key : keys) {
if (key % 2 == 0) { // 刪除一半的元素
unorderedMap.erase(key);
}
}
});
// 打印結(jié)果
std::cout << "操作\t\tmap(微秒)\tunordered_map(微秒)\t性能比" << std::endl;
std::cout << "插入\t\t" << mapInsertTime << "\t\t" << unorderedMapInsertTime
<< "\t\t\t" << (float)mapInsertTime / unorderedMapInsertTime << std::endl;
std::cout << "順序查找\t" << mapLookupTime << "\t\t" << unorderedMapLookupTime
<< "\t\t\t" << (float)mapLookupTime / unorderedMapLookupTime << std::endl;
std::cout << "隨機(jī)查找\t" << mapRandomLookupTime << "\t\t" << unorderedMapRandomLookupTime
<< "\t\t\t" << (float)mapRandomLookupTime / unorderedMapRandomLookupTime << std::endl;
std::cout << "修改\t\t" << mapUpdateTime << "\t\t" << unorderedMapUpdateTime
<< "\t\t\t" << (float)mapUpdateTime / unorderedMapUpdateTime << std::endl;
std::cout << "刪除\t\t" << mapEraseTime << "\t\t" << unorderedMapEraseTime
<< "\t\t\t" << (float)mapEraseTime / unorderedMapEraseTime << std::endl;
return0;
}
// 輸出結(jié)果:
// 操作 map(微秒) unordered_map(微秒) 性能比
// 插入 225419 116690 1.93178
// 順序查找 103715 20122 5.15431
// 隨機(jī)查找 127432 25890 4.92205
// 修改 104918 20597 5.09385
// 刪除 130040 29996 4.33524
1. 性能分析
從測(cè)試結(jié)果可以清晰地看出:
(1) 插入操作:unordered_map比map快約1.9倍。這是因?yàn)閙ap每次插入都需要維護(hù)紅黑樹的平衡,而unordered_map只需計(jì)算哈希值并放入對(duì)應(yīng)的桶。
(2) 查找操作:
- 順序查找:unordered_map比map快約5.2倍
- 隨機(jī)查找:unordered_map比map快約4.9倍
查找是unordered_map最顯著的優(yōu)勢(shì),無(wú)論是順序還是隨機(jī)訪問模式下都有明顯提升。
- 修改操作:unordered_map比map快約5.1倍。修改操作本質(zhì)上是先查找再賦值,所以性能差距與查找操作接近。
- 刪除操作:unordered_map比map快約4.3倍。map刪除元素后可能需要重新平衡樹,而unordered_map只需從哈希表中刪除節(jié)點(diǎn)。
2. 小結(jié)
綜合來(lái)看,unordered_map在所有操作上都顯著優(yōu)于map,特別是在查找和修改操作上,性能提升達(dá)到了5倍左右。這意味著在大多數(shù)不需要有序遍歷的場(chǎng)景下,unordered_map是更優(yōu)的選擇。
記住這些差異,在實(shí)際開發(fā)中選擇合適的容器,可以為你的程序帶來(lái)顯著的性能提升。
五、什么時(shí)候用哪個(gè)?
1. 用 map 的情況
(1) 需要有序遍歷:如果你需要按鍵的順序遍歷元素
// 想按學(xué)生姓名字母順序打印成績(jī)單
std::map<std::string, int> scoreCard;
// ... 添加數(shù)據(jù) ...
for (const auto& item : scoreCard) {
std::cout << item.first << ": " << item.second << std::endl;
}
(2) 需要范圍查詢:找出所有鍵在某個(gè)范圍內(nèi)的元素
// 查找所有名字在A到C之間的學(xué)生
auto start = scoreCard.lower_bound("A");
auto end = scoreCard.upper_bound("C");
for (auto it = start; it != end; ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
(3) 需要穩(wěn)定的性能:最壞情況下查找復(fù)雜度是確定的O(log n)
2. 用 unordered_map 的情況
(1) 只關(guān)心查找速度:大多數(shù)情況下只是用來(lái)查找,不關(guān)心順序
// 快速查找某個(gè)學(xué)生的成績(jī)
std::unordered_map<std::string, int> scoreDB;
// ... 添加數(shù)據(jù) ...
std::cout << "Zhang's score: " << scoreDB["Zhang"] << std::endl;
(2) 數(shù)據(jù)量大:對(duì)于大數(shù)據(jù)量,unordered_map 的常數(shù)時(shí)間復(fù)雜度 O(1) 明顯優(yōu)于 map 的 O(log n)
(3) 不需要排序:如果不需要按鍵排序,就沒必要付出排序的成本
六、常見坑點(diǎn)
坑1:自定義類型做鍵
對(duì)于unordered_map,如果用自定義類型作為鍵,必須提供哈希函數(shù)和相等比較函數(shù):
struct Student {
std::string name;
int id;
booloperator==(const Student& other) const {
return id == other.id;
}
};
// 為Student提供哈希函數(shù)
namespacestd {
template<>
struct hash<Student> {
size_t operator()(const Student& s) const {
return hash<int>()(s.id);
}
};
}
// 現(xiàn)在可以使用了
std::unordered_map<Student, int> studentScores;
坑2:性能波動(dòng)
unordered_map在某些情況下可能會(huì)遇到哈希沖突,導(dǎo)致性能下降。如果你的應(yīng)用對(duì)性能穩(wěn)定性要求高,可能需要考慮使用map。
坑3:內(nèi)存占用
unordered_map通常比map消耗更多內(nèi)存,因?yàn)楣1頌榱私档蜎_突概率,會(huì)預(yù)留一定的空間。
七、實(shí)際使用建議
- 默認(rèn)選擇unordered_map:除非有特殊需求,一般情況下優(yōu)先使用unordered_map
- 測(cè)試決定:對(duì)于性能關(guān)鍵的代碼,最好實(shí)際測(cè)試兩種容器的性能差異
- 根據(jù)需求選擇:如果需要有序遍歷或范圍查詢,選擇map;如果只需要快速查找,選擇unordered_map
- 考慮數(shù)據(jù)規(guī)模:數(shù)據(jù)量越大,兩者的性能差距可能越明顯
八、總結(jié)
選對(duì)容器,事半功倍;選錯(cuò)容器,徒增煩惱。
- map:有序、穩(wěn)定、支持范圍查詢,但速度較慢(O(log n))
- unordered_map:無(wú)序、速度快(O(1)),但內(nèi)存占用較大,且不支持范圍查詢
記住這些區(qū)別,下次寫代碼時(shí),就能輕松選擇正確的容器,讓你的程序飛起來(lái)!