一、关联容器的底层数据结构特性
C++ 标准库中的map和set均属于关联容器,其底层实现基于红黑树(Red-Black Tree)—— 一种自平衡的二叉搜索树。这种数据结构具有以下核心特性:
红黑树的平衡机制确保了所有基本操作(插入、删除、查找)都能在O(log n) 的时间复杂度内完成,这使得关联容器在需要频繁查找和有序遍历的场景中表现优异。
二、map 与 set 的存储机制差异
虽然map和set共享相同的底层实现机制,但它们的存储方式存在本质区别:
特性 |
set |
map |
存储内容 |
仅存储键(key) |
存储键值对(key-value) |
元素唯一性 |
键值唯一,不允许重复 |
键值唯一,不允许重复 |
访问方式 |
只能通过键访问元素 |
可通过键访问对应的值 |
模板参数 |
std::set<Key, Compare, Allocator> |
std::map<Key, Value, Compare, Allocator> |
关键区别:set是 "键即值,值即键" 的特殊情况,而map则明确区分键(用于排序和查找)和值(用于存储数据)。
1 2 3 4 5 6 7 8 9
| #include <set> #include <map> #include <string>
// set存储单个元素(键) std::set<int> integer_set;
// map存储键值对 std::map<std::string, int> string_to_int;
|
三、基本操作详解与代码示例
3.1 增删改查
1. 插入操作
- map:支持
insert
(插入元素)和 emplace
(直接构造元素)
- set:仅支持
insert
和 emplace
,无修改功能
- 时间复杂度:O(log n)(红黑树的插入操作)
2. 查找操作
- map:
find()
返回对应值的迭代器,operator[]
支持直接访问
- set:
find()
返回键的迭代器
- 性能差异:
map
的查找需要额外寻址值,而 set
的查找更直接
3. 修改操作
- map:通过
operator[]
修改值,或使用 at()
进行安全访问
- set:不支持修改,仅允许删除和插入操作
- 注意:
operator[]
会自动插入新键,不适合需要判断键是否存在的情况
4. 删除操作
- map:
erase()
删除指定键,clear()
清空所有元素
- set:
erase()
删除指定键,erase(iterator)
删除指定位置元素
- 迭代器失效:删除操作可能使部分迭代器失效,建议遍历结束后进行删除
3.2 插入操作
set 的插入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <set> #include <iostream>
int main() { std::set<int> s; // 插入单个元素,返回pair<iterator, bool> // 其中bool表示插入是否成功(是否存在重复元素) auto result = s.insert(10); if (result.second) { std::cout << "元素10插入成功" << std::endl; } // 插入重复元素会失败 result = s.insert(10); if (!result.second) { std::cout << "元素10已存在,插入失败" << std::endl; } // 插入多个元素 int arr[] = {20, 30, 40}; s.insert(arr, arr + 3); return 0; }
|
map 的插入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <map> #include <string> #include <iostream>
int main() { std::map<std::string, int> m; // 方法1:使用pair插入 m.insert(std::make_pair("apple", 5)); // 方法2:使用emplace(C++11) m.emplace("banana", 3); // 方法3:使用initializer_list(C++11) m.insert({{"orange", 7}, {"grape", 2}}); return 0; }
|
插入操作的时间复杂度为O(log n),其中 n 是容器中已有的元素数量。
3.3 查找操作
set 的查找:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <set> #include <iostream>
int main() { std::set<int> s = {10, 20, 30, 40, 50}; // 查找元素,返回迭代器 auto it = s.find(30); if (it != s.end()) { std::cout << "找到元素: " << *it << std::endl; } else { std::cout << "未找到元素" << std::endl; } // 统计元素出现次数(对于set只能是0或1) size_t count = s.count(20); std::cout << "元素20出现的次数: " << count << std::endl; return 0; }
|
map 的查找:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <map> #include <string> #include <iostream>
int main() { std::map<std::string, int> m = { {"apple", 5}, {"banana", 3}, {"orange", 7} }; // 查找键,返回指向键值对的迭代器 auto it = m.find("banana"); if (it != m.end()) { std::cout << "找到键: " << it->first << ", 值: " << it->second << std::endl; } // 使用operator[]访问(不存在则插入默认值) int value = m["apple"]; // 安全访问已存在的键 int unknown = m["watermelon"]; // 插入键"watermelon",值为0 return 0; }
|
查找操作的时间复杂度为O(log n)。注意map::operator[]在键不存在时会插入新元素,这可能导致意外的性能开销。
3.4 删除操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include <set> #include <map> #include <iostream>
int main() { // set的删除 std::set<int> s = {10, 20, 30, 40}; // 按值删除 size_t removed = s.erase(20); std::cout << "set中删除的元素数量: " << removed << std::endl; // 按迭代器删除 auto it_s = s.find(30); if (it_s != s.end()) { s.erase(it_s); } // map的删除 std::map<std::string, int> m = {{"a",1}, {"b",2}, {"c",3}}; // 按键删除 removed = m.erase("b"); std::cout << "map中删除的元素数量: " << removed << std::endl; // 按迭代器删除 auto it_m = m.find("c"); if (it_m != m.end()) { m.erase(it_m); } // 清空容器 s.clear(); m.clear(); return 0; }
|
删除操作的时间复杂度为O(log n)。需要注意的是,删除操作会使指向被删除元素的迭代器失效,但其他迭代器不受影响。
3.5 修改操作
set 的修改限制:
set中的元素是常量,不能直接修改,因为这会破坏红黑树的有序性。若要 "修改" 元素,需先删除旧元素再插入新元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <set> #include <iostream>
int main() { std::set<int> s = {10, 20, 30}; // 错误:不能修改set中的元素 // auto it = s.find(20); // *it = 25; // 编译错误 // 正确方式:先删除再插入 auto it = s.find(20); if (it != s.end()) { s.erase(it); // O(log n) s.insert(25); // O(log n) } return 0; }
|
map 的修改:
map 中的键是常量,但值可以修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <map> #include <string> #include <iostream>
int main() { std::map<std::string, int> m = {{"apple", 5}, {"banana", 3}}; // 修改值(键不能修改) auto it = m.find("apple"); if (it != m.end()) { it->second = 10; // 合法操作 } // 使用operator[]修改 m["banana"] = 7; // 合法操作 return 0; }
|
四、性能分析与优化策略
4.1 时间复杂度对比
操作 |
set |
map |
时间复杂度 |
插入 |
插入元素 |
插入键值对 |
O(log n) |
查找 |
按值查找 |
按键查找 |
O(log n) |
删除 |
按值 / 迭代器删除 |
按键 / 迭代器删除 |
O(log n) |
遍历 |
全量遍历 |
全量遍历 |
O(n) |
范围查询 |
lower_bound/upper_bound |
lower_bound/upper_bound |
O (log n + k),k 为结果数量 |
4.2 性能优化策略
选择合适的初始化方式
1 2 3 4 5 6 7 8
| // 低效:多次插入 std::set<int> s; s.insert(1); s.insert(2); s.insert(3);
// 高效:一次初始化 std::set<int> s = {1, 2, 3};
|
避免不必要的查找
1 2 3 4 5 6 7 8 9 10
| // 低效:两次查找 if (m.find("key") != m.end()) { m["key"] = 42; }
// 高效:一次查找 auto it = m.find("key"); if (it != m.end()) { it->second = 42; }
|
批量操作优化
当需要插入大量元素时,先在容器外排序再插入可以减少红黑树的平衡操作:
1 2 3 4 5 6 7 8 9
| #include <vector> #include <algorithm>
// 高效批量插入 std::vector<int> temp = {5, 3, 8, 1, 2}; std::sort(temp.begin(), temp.end()); // 预先排序
std::set<int> s; s.insert(temp.begin(), temp.end()); // 批量插入
|