1. STL容器概述:为什么需要set和map?
在C++标准模板库(STL)中,set和map属于关联式容器,它们与序列式容器(vector/list等)最大的区别在于数据组织方式。关联容器通过键(key)来高效存储和检索元素,底层通常采用红黑树实现,这使得它们的查找、插入和删除操作都能保持O(log n)的时间复杂度。
我刚开始接触STL时,常常困惑什么时候该用vector,什么时候该用set/map。后来在实际项目中踩过几次坑才明白:当我们需要频繁检查元素是否存在、需要自动排序、或者需要建立键值映射时,set和map就是最佳选择。比如最近做的一个用户管理系统,如果用vector存储百万级用户ID,查找效率简直灾难;换成set后,性能立即提升了几十倍。
2. set容器深度解析
2.1 set的核心特性
set是一个不允许重复元素的排序集合,其核心特点包括:
- 自动去重:插入重复元素时只会保留一个副本
- 自动排序:默认按升序排列,可通过比较函数自定义
- 高效查找:基于红黑树的实现使得查找时间复杂度为O(log n)
cpp复制#include <set>
#include <iostream>
int main() {
std::set<int> nums {3, 1, 4, 1, 5, 9}; // 初始化
// 输出:1 3 4 5 9
for(int n : nums) std::cout << n << " ";
}
2.2 set的常用操作
2.2.1 插入与删除
cpp复制std::set<std::string> names;
names.insert("Alice"); // 插入单个元素
names.insert({"Bob", "Charlie"}); // 插入初始化列表
// 删除操作
names.erase("Bob"); // 删除指定元素
auto it = names.find("Charlie");
if(it != names.end()) names.erase(it); // 通过迭代器删除
注意:set的insert操作返回一个pair<iterator, bool>,其中second表示是否插入成功(对于已存在元素会返回false)
2.2.2 查找与计数
cpp复制std::set<int> scores {85, 90, 78, 92};
if(scores.find(90) != scores.end()) {
std::cout << "90分存在";
}
// count总是返回0或1(因为元素唯一)
if(scores.count(78)) {
std::cout << "78分存在";
}
2.3 自定义排序规则
set默认使用less
- 函数对象
cpp复制struct CaseInsensitiveCompare {
bool operator()(const std::string& a, const std::string& b) const {
return strcasecmp(a.c_str(), b.c_str()) < 0;
}
};
std::set<std::string, CaseInsensitiveCompare> words;
- 函数指针
cpp复制bool compareLength(const std::string& a, const std::string& b) {
return a.length() < b.length();
}
std::set<std::string, decltype(&compareLength)> lenSet(compareLength);
- Lambda表达式(C++11)
cpp复制auto cmp = [](int a, int b) { return a > b; };
std::set<int, decltype(cmp)> descendingSet(cmp);
3. map容器全面掌握
3.1 map的核心概念
map是存储键值对的关联容器,具有以下特点:
- 每个键唯一映射到一个值
- 按键自动排序
- 支持高效的键查找(O(log n))
cpp复制#include <map>
#include <string>
std::map<std::string, int> wordCount;
wordCount["apple"] = 5; // 插入/更新
wordCount.insert({"banana", 3});
// 范围遍历
for(const auto& pair : wordCount) {
std::cout << pair.first << ": " << pair.second << "\n";
}
3.2 map的进阶操作
3.2.1 安全的元素访问
直接使用operator[]访问不存在的键会创建该键(值初始化),有时这不是我们想要的。更安全的方式:
cpp复制std::map<std::string, int> inventory;
// 方法1:find检查
auto it = inventory.find("pear");
if(it != inventory.end()) {
std::cout << it->second;
}
// 方法2:C++20的contains
if(inventory.contains("pear")) {
std::cout << inventory.at("pear"); // at会检查边界
}
3.2.2 高效的插入更新
cpp复制// 统计单词频率的经典模式
std::map<std::string, int> freq;
for(const auto& word : words) {
auto [it, inserted] = freq.insert({word, 1});
if(!inserted) {
it->second++; // 已存在则递增
}
}
// C++17更简洁的写法
for(const auto& word : words) {
freq[word]++; // 不存在时会值初始化为0
}
3.3 multimap的应用场景
当需要允许重复键时,可以使用multimap。典型场景如:
- 电话簿(一个人可能有多个号码)
- 学生成绩记录(同一分数可能有多个学生)
cpp复制std::multimap<std::string, std::string> phonebook;
phonebook.insert({"Alice", "123-4567"});
phonebook.insert({"Alice", "765-4321"});
// 查找所有Alice的电话
auto range = phonebook.equal_range("Alice");
for(auto it = range.first; it != range.second; ++it) {
std::cout << it->second << "\n";
}
4. 性能优化与最佳实践
4.1 选择正确的容器
- 需要快速查找且元素唯一 → set
- 需要键值映射且键唯一 → map
- 允许重复元素 → multiset
- 允许重复键 → multimap
- 需要哈希表特性 → unordered_set/unordered_map(C++11)
4.2 迭代器失效问题
set和map的迭代器在删除元素时会失效,但有一个例外:
cpp复制std::set<int> s = {1, 2, 3, 4, 5};
for(auto it = s.begin(); it != s.end(); ) {
if(*it % 2 == 0) {
it = s.erase(it); // C++11起erase返回下一个有效迭代器
} else {
++it;
}
}
4.3 自定义类型的键
当使用自定义类型作为键时,必须提供比较函数或重载operator<:
cpp复制struct Employee {
int id;
std::string name;
bool operator<(const Employee& other) const {
return id < other.id; // 按ID排序
}
};
std::set<Employee> staff;
std::map<Employee, int> salaryMap;
重要提示:自定义比较函数必须满足严格弱序关系,即:
- 非自反性:comp(a,a)必须为false
- 非对称性:若comp(a,b)为true,则comp(b,a)必须为false
- 可传递性:若comp(a,b)和comp(b,c)为true,则comp(a,c)必须为true
5. 实际应用案例
5.1 使用map实现缓存系统
cpp复制template<typename Key, typename Value>
class LRUCache {
private:
size_t capacity;
std::list<std::pair<Key, Value>> items;
std::map<Key, typename std::list<std::pair<Key, Value>>::iterator> cacheMap;
public:
LRUCache(size_t cap) : capacity(cap) {}
Value* get(const Key& key) {
auto it = cacheMap.find(key);
if(it == cacheMap.end()) return nullptr;
items.splice(items.begin(), items, it->second);
return &(it->second->second);
}
void put(const Key& key, const Value& value) {
auto it = cacheMap.find(key);
if(it != cacheMap.end()) {
items.erase(it->second);
cacheMap.erase(it);
}
items.emplace_front(key, value);
cacheMap[key] = items.begin();
if(cacheMap.size() > capacity) {
auto last = items.end();
last--;
cacheMap.erase(last->first);
items.pop_back();
}
}
};
5.2 使用set实现敏感词过滤器
cpp复制class WordFilter {
private:
std::set<std::string> sensitiveWords;
public:
void addSensitiveWord(const std::string& word) {
sensitiveWords.insert(word);
}
bool containsSensitiveWord(const std::string& text) const {
// 简单实现:检查文本中是否包含任何敏感词
for(const auto& word : sensitiveWords) {
if(text.find(word) != std::string::npos) {
return true;
}
}
return false;
}
// 更高效的实现可以使用AC自动机
};
在多年C++开发中,我发现set和map最容易被低估的特性是它们的排序性质。很多开发者只把它们当作快速查找的容器,却忽略了自动排序带来的便利。比如最近处理一个日志分析需求时,利用map的自动排序特性,我轻松实现了按时间戳排序的日志条目,而不需要额外编写排序代码。这种"免费"的排序特性,在合适的场景下能大幅简化代码逻辑。