C++ std::map

std::map 是 C++ STL 中最常用的有序键值对容器,其核心功能是存储唯一键(key)与对应值(value)的映射关系,并自动按键的顺序排序。底层基于红黑树(自平衡二叉搜索树)实现,这使得它在键的查找、插入、删除等操作上保持稳定的高效性。

1、底层数据结构与核心特性

1.1 底层数据结构

  • 底层结构:通常实现为红黑树(Red-Black Tree),一种自平衡的二叉搜索树(BST)。
  • 核心特性
    1. 关联性:元素是键值对,即 std::pair<const Key, T>。每个元素将一个键(Key) 与一个值(Value) 关联起来。
    2. 有序性:容器中的元素会根据键(Key) 按照特定的比较准则(默认为 std::less<Key>)自动进行排序。遍历时,元素总是按键的升序排列。
    3. 唯一性:集合中每个元素的键(Key) 都是唯一的。不允许存在两个键相等的元素。
  • 节点结构:红黑树的每个节点存储一个 std::pair<const Key, T> 对象。键是 const 的,以保证树的结构不被破坏,而值是可以修改的。

1.2 原理

  1. 键值对与自动排序

    • map 的所有操作都是基于键(Key) 的。
    • 排序和查找只关心键,值只是与键关联的数据。
    • 插入时,元素会根据其键的值被放到红黑树的正确位置,以维持二叉搜索树的性质。
  2. 操作效率 (O(log n))

    • 红黑树保证在最坏情况下,通过键进行的插入、删除和查找操作的时间复杂度都是对数级的,即 O(log n)
  3. 迭代器稳定性

    • 插入和删除操作不会使任何现有迭代器失效(除了指向被删除元素的迭代器)。
    • 这与 vector 的动态内存重新分配形成鲜明对比。
  4. 键不可修改,值可修改

    • 元素的键是 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、常见问题

  1. std::map 的底层实现是什么?

    • A: 底层通常是红黑树(一种自平衡二叉搜索树)。每个节点存储一个 std::pair<const Key, T> 对象。
  2. map::operator[]map::insert 的区别是什么?
    这是最关键的区别。

    • operator[] 如果键不存在,会插入一个具有该键的新元素,并将其值进行值初始化,然后返回该值的引用。它总是会改变map(如果键不存在)。
    • insert 只会尝试插入键值对。如果键已存在,则插入失败,不会改变已存在的值。它返回一个 pair<iterator, bool>,其中 bool 表示插入是否成功。
  3. 尝试修改 map 中元素的键会发生什么?
    会导致编译错误。因为键的类型是 const Key,它是不可修改的,以防止破坏红黑树的有序结构。

  4. map 的迭代器在插入和删除操作后会失效吗?
    插入操作不会使任何迭代器失效删除操作只会使指向被删除元素的迭代器失效,其他迭代器仍然有效。

  5. 如何检查一个键是否存在于 map 中?哪种方法最好?

    1. if (m.find(key) != m.end()) {...}
    2. if (m.count(key) > 0) {...} (对于map,count只能是0或1)
      最好使用 find(),因为它不仅告诉你键是否存在,还直接返回指向该元素的迭代器,方便后续操作。使用 count() 仅仅是为了检查存在性。
  6. 什么时候该用 map,什么时候该用 unordered_map
    如果需要元素按键有序,或者需要进行范围查询(如找所有大于某值的键),或者非常关心最坏情况的性能,就选择 map。如果只需要最快的平均查找、插入和删除速度,并且不关心元素的顺序,那么 unordered_map 是更好的选择。

posted @ 2025-09-16 11:30  xclic  阅读(185)  评论(0)    收藏  举报