std::map
是 C++ STL 中最常用的有序键值对容器,其核心功能是存储唯一键(key)与对应值(value)的映射关系,并自动按键的顺序排序。底层基于红黑树(自平衡二叉搜索树)实现,这使得它在键的查找、插入、删除等操作上保持稳定的高效性。
1、底层数据结构与核心特性
1.1 底层数据结构
- 底层结构:通常实现为红黑树(Red-Black Tree),一种自平衡的二叉搜索树(BST)。
- 核心特性:
- 关联性:元素是键值对,即
std::pair<const Key, T>
。每个元素将一个键(Key) 与一个值(Value) 关联起来。 - 有序性:容器中的元素会根据键(Key) 按照特定的比较准则(默认为
std::less<Key>
)自动进行排序。遍历时,元素总是按键的升序排列。 - 唯一性:集合中每个元素的键(Key) 都是唯一的。不允许存在两个键相等的元素。
- 关联性:元素是键值对,即
- 节点结构:红黑树的每个节点存储一个
std::pair<const Key, T>
对象。键是const
的,以保证树的结构不被破坏,而值是可以修改的。
1.2 原理
-
键值对与自动排序
map
的所有操作都是基于键(Key) 的。- 排序和查找只关心键,值只是与键关联的数据。
- 插入时,元素会根据其键的值被放到红黑树的正确位置,以维持二叉搜索树的性质。
-
操作效率 (O(log n))
- 红黑树保证在最坏情况下,通过键进行的插入、删除和查找操作的时间复杂度都是对数级的,即 O(log n)。
-
迭代器稳定性
- 插入和删除操作不会使任何现有迭代器失效(除了指向被删除元素的迭代器)。
- 这与
vector
的动态内存重新分配形成鲜明对比。
-
键不可修改,值可修改
- 元素的键是
const
的。一旦插入,就不能再修改键,否则会破坏树的有序性。 - 元素的值是非
const
的,可以随时修改。
- 元素的键是
2、操作指导与代码示例
2.1 初始化与构造函数
#include <map>
#include <string>// 1. 空map
std::map<int, std::string> map1;// 2. 使用初始化列表 (C++11)
std::map<int, std::string> map2 = {{1, "Alice"},{2, "Bob"},{3, "Charlie"}
};// 3. 通过迭代器范围(另一个容器的)
std::vector<std::pair<int, std::string>> vec = {{4, "David"}, {5, "Eve"}};
std::map<int, std::string> map3(vec.begin(), vec.end());// 4. 自定义比较准则(例如:按键降序)
std::map<int, std::string, std::greater<int>> map4 = {{1, "a"}, {3, "c"}, {2, "b"}};
// 遍历顺序:3->"c", 2->"b", 1->"a"// 5. 拷贝构造函数
std::map<int, std::string> map5(map2);
2.2 元素访问与查找
这是 map
最核心的操作之一
std::map<int, std::string> m = {{1, "Apple"}, {2, "Banana"}};// --- 最常用的访问操作:operator[] ---
// 如果键存在,返回对应的值的引用。
// 如果键不存在,则会插入一个具有该键的新元素,并将其值进行值初始化(对于string是空字符串""),然后返回这个新值的引用。
m[1] = "Apricot"; // 修改已存在的键值对:{1, "Apricot"}
std::cout << m[2]; // 输出: Banana
std::cout << m[3]; // 键3不存在!这会执行插入操作:m[3] = ""。现在map中有3个元素。// --- 安全的查找操作:find() ---
// 推荐在需要“只读”访问时使用find,因为它不会改变map。
auto it = m.find(2);
if (it != m.end()) { // 必须检查是否找到!std::cout << "Found: " << it->first << " -> " << it->second << "\n";it->second = "Blueberry"; // 可以修改值// it->first = 4; // 错误!键是const的,不能修改。
} else {std::cout << "Key not found.\n";
}// --- 检查存在性:count() ---
// 对于map,返回值只能是0或1。
if (m.count(4) == 0) {std::cout << "Key 4 is not present.\n";
}// --- 边界查找:lower_bound(), upper_bound(), equal_range() ---
// 用于范围查询,非常高效(O(log n))
std::map<int, std::string> numMap = {{10, "Ten"}, {20, "Twenty"}, {30, "Thirty"}};
// 找到第一个键 >= 15 的元素
auto lowIt = numMap.lower_bound(15); // 指向20->"Twenty"
// 找到第一个键 > 25 的元素
auto highIt = numMap.upper_bound(25); // 指向30->"Thirty"
2.3 增加元素
有多种方法可以向 map
中插入元素,各有优缺点。
std::map<int, std::string> m;// 1. insert: 插入一个pair
// 方法一:make_pair
auto ret1 = m.insert(std::make_pair(1, "One"));
// 方法二:使用{}构造pair (C++11)
auto ret2 = m.insert({2, "Two"});// ret 的类型是 std::pair<iterator, bool>
// ret.first: 指向被插入元素(或已存在元素)的迭代器
// ret.second: 插入是否成功(true表示成功,false表示键已存在)
if (ret2.second) {std::cout << "Insertion of key 2 succeeded.\n";
}// 2. emplace: 原地构造,避免临时对象(高效!)
// 直接传递构造键和值所需的参数给emplace
auto ret3 = m.emplace(3, "Three"); // 直接在map内部构造pair(3, "Three")
if (ret3.second) {std::cout << "Emplace succeeded.\n";
}// 3. try_emplace (C++17): 更安全高效的emplace
// 如果key不存在,则用参数构造value。
// 如果key已存在,则什么都不做,且不会构造value对象。这对于构造开销大的value类型非常关键。
auto ret4 = m.try_emplace(3, "Third"); // 键3已存在,不会构造"Third",ret4.second为false
auto ret5 = m.try_emplace(4, "Four"); // 键4不存在,构造"Four",ret5.second为true// 4. operator[]: 如上所述,如果键不存在会执行插入。
m[5] = "Five"; // 如果5不存在,则插入{5, ""},然后赋值"Five"
2.4 删除元素
std::map<int, std::string> m = {{1, "A"}, {2, "B"}, {3, "C"}, {4, "D"}, {5, "E"}};// 1. erase by key: 删除键为3的元素,返回删除的元素个数(0或1)
size_t numErased = m.erase(3); // numErased = 1// 2. erase by iterator: 删除指定位置的元素,更高效
auto it = m.find(2);
if (it != m.end()) {m.erase(it); // 删除迭代器指向的元素
}// 3. erase by range: 删除一个区间的元素 [first, last)
auto first = m.find(4);
auto last = m.end();
m.erase(first, last); // 删除4及其之后的所有元素(4和5被删除)
2.5 遍历元素
std::map<std::string, int> population = {{"Beijing", 2154},{"Shanghai", 2424},{"Guangzhou", 1404}
};// 1. 使用迭代器(总是按键有序的)
std::cout << "Using iterators:\n";
for (auto it = population.begin(); it != population.end(); ++it) {// it->first 是键(const),it->second 是值std::cout << it->first << ": " << it->second << "万人\n";
}
// 输出按城市名拼音排序:
// Beijing: 2154万人
// Guangzhou: 1404万人
// Shanghai: 2424万人// 2. 使用范围for循环 (C++11) - 最简洁
std::cout << "\nUsing range-for:\n";
for (const auto& pair : population) { // 使用const引用,避免拷贝std::cout << pair.first << ": " << pair.second << "万人\n";
}// 3. 使用结构化绑定 (C++17) - 最清晰,推荐!
std::cout << "\nUsing structured binding (C++17):\n";
for (const auto& [city, num] : population) {std::cout << city << ": " << num << "万人\n";
}// 4. 使用反向迭代器(逆序遍历)
std::cout << "\nReverse order:\n";
for (auto rit = population.rbegin(); rit != population.rend(); ++rit) {std::cout << rit->first << ": " << rit->second << "万人\n";
}
2.6 特殊成员函数与算法
map
提供了基于其有序特性的高效操作,如 lower_bound(key)
, upper_bound(key)
, equal_range(key)
,用于范围查询,效率为 O(log n)。
std::map<int, char> m = {{10, 'a'}, {20, 'b'}, {30, 'c'}, {40, 'd'}};// 删除所有键在 [15, 35) 范围内的元素
auto low = m.lower_bound(15); // 第一个 >=15 的元素,指向20->'b'
auto high = m.upper_bound(35); // 第一个 >35 的元素,指向40->'d'
m.erase(low, high); // 删除 [20, 30] -> 'b'和'c'被删除
2.7 自定义类型作为键
要让自定义类型(如MyClass)作为 map 的键,你需要为其定义排序准则。
2.7.1 在自定义类型内重载 operator<(推荐)
struct Point {int x;int y;// 必须定义为const成员函数// 必须定义严格的弱序(Strict Weak Ordering)bool operator<(const Point& other) const {if (x == other.x) {return y < other.y;}return x < other.x;}
};std::map<Point, std::string> pointMap;
pointMap[{1, 2}] = "First point";
pointMap[{3, 4}] = "Second point";
2.7.2 提供自定义的函数对象(仿函数)作为模板参数
struct Point {int x;int y;
};// 自定义比较器
struct PointCompare {bool operator()(const Point& a, const Point& b) const {return a.x < b.x; // 只按x坐标比较}
};// 使用时必须将比较器作为模板参数传入
std::map<Point, std::string, PointCompare> pointMap;
3、常见问题
-
std::map
的底层实现是什么?- A: 底层通常是红黑树(一种自平衡二叉搜索树)。每个节点存储一个
std::pair<const Key, T>
对象。
- A: 底层通常是红黑树(一种自平衡二叉搜索树)。每个节点存储一个
-
map::operator[]
和map::insert
的区别是什么?
这是最关键的区别。operator[]
如果键不存在,会插入一个具有该键的新元素,并将其值进行值初始化,然后返回该值的引用。它总是会改变map(如果键不存在)。insert
只会尝试插入键值对。如果键已存在,则插入失败,不会改变已存在的值。它返回一个pair<iterator, bool>
,其中bool
表示插入是否成功。
-
尝试修改
map
中元素的键会发生什么?
会导致编译错误。因为键的类型是const Key
,它是不可修改的,以防止破坏红黑树的有序结构。 -
map
的迭代器在插入和删除操作后会失效吗?
插入操作不会使任何迭代器失效。删除操作只会使指向被删除元素的迭代器失效,其他迭代器仍然有效。 -
如何检查一个键是否存在于
map
中?哪种方法最好?if (m.find(key) != m.end()) {...}
if (m.count(key) > 0) {...}
(对于map,count只能是0或1)
最好使用find()
,因为它不仅告诉你键是否存在,还直接返回指向该元素的迭代器,方便后续操作。使用count()
仅仅是为了检查存在性。
-
什么时候该用
map
,什么时候该用unordered_map
?
如果需要元素按键有序,或者需要进行范围查询(如找所有大于某值的键),或者非常关心最坏情况的性能,就选择map
。如果只需要最快的平均查找、插入和删除速度,并且不关心元素的顺序,那么unordered_map
是更好的选择。