1. 理解map和set在OJ题中的核心价值
第一次接触编程竞赛时,我就被各种需要高效查询和去重的题目难住了。直到系统学习了STL中的map和set容器,解题效率才有了质的飞跃。在Online Judge(OJ)平台上,大约30%的中等难度题目都会涉及到这两种数据结构的应用。
map和set之所以成为算法竞赛的利器,关键在于它们基于红黑树实现的O(logn)时间复杂度。与普通数组相比,它们在处理动态数据时展现出巨大优势。举个例子,当我们需要统计一篇文章中每个单词出现的次数时,使用map<string, int>就能轻松搞定,而用数组实现则会异常麻烦。
2. map容器的典型应用场景
2.1 键值对映射问题
最常见的应用就是建立key-value映射关系。比如LeetCode第1题"两数之和",最优解就是使用unordered_map:
cpp复制vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hash;
for(int i = 0; i < nums.size(); ++i) {
auto it = hash.find(target - nums[i]);
if(it != hash.end())
return {it->second, i};
hash[nums[i]] = i;
}
return {};
}
这里利用了map的快速查找特性,将时间复杂度从暴力解的O(n²)降到了O(n)。
2.2 频率统计类题目
统计元素出现频率是map的拿手好戏。比如要找出数组中出现次数超过n/2的元素(LeetCode 169):
cpp复制int majorityElement(vector<int>& nums) {
unordered_map<int, int> counts;
for(int num : nums) {
if(++counts[num] > nums.size()/2)
return num;
}
return -1;
}
注意:在C++中,unordered_map的查找效率(平均O(1))通常高于map(O(logn)),但前者不保证元素顺序。竞赛中若无序要求,优先使用unordered_map。
3. set容器的妙用技巧
3.1 快速去重与存在性检测
set最直观的作用就是去重。比如需要判断链表是否有环(LeetCode 141),用set存储访问过的节点即可:
cpp复制bool hasCycle(ListNode *head) {
unordered_set<ListNode*> visited;
while(head) {
if(visited.count(head)) return true;
visited.insert(head);
head = head->next;
}
return false;
}
3.2 维护有序集合
当题目需要维护一个动态的有序集合时,set就派上用场了。比如实现一个支持快速获取中位数的数据结构(LeetCode 295):
cpp复制class MedianFinder {
multiset<int> nums;
multiset<int>::iterator mid;
public:
void addNum(int num) {
nums.insert(num);
if(nums.size() == 1) {
mid = nums.begin();
return;
}
if(num < *mid && nums.size()%2 == 0) --mid;
else if(num >= *mid && nums.size()%2 == 1) ++mid;
}
double findMedian() {
return nums.size()%2 ? *mid : (*mid + *next(mid)) / 2.0;
}
};
4. 常见OJ题型与解题模板
4.1 滑动窗口最大值(LeetCode 239)
这道题需要维护一个窗口内的最大值,使用map可以这样实现:
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
map<int, int> freq;
vector<int> res;
for(int i = 0; i < nums.size(); ++i) {
freq[nums[i]]++;
if(i >= k) {
if(--freq[nums[i-k]] == 0)
freq.erase(nums[i-k]);
}
if(i >= k-1)
res.push_back(freq.rbegin()->first);
}
return res;
}
4.2 最近最久未使用缓存(LeetCode 146)
LRU缓存是map和list结合的经典案例:
cpp复制class LRUCache {
list<pair<int, int>> cache;
unordered_map<int, list<pair<int, int>>::iterator> map;
int capacity;
public:
LRUCache(int capacity) : capacity(capacity) {}
int get(int key) {
if(!map.count(key)) return -1;
cache.splice(cache.begin(), cache, map[key]);
return map[key]->second;
}
void put(int key, int value) {
if(map.count(key)) {
cache.splice(cache.begin(), cache, map[key]);
map[key]->second = value;
return;
}
if(cache.size() == capacity) {
map.erase(cache.back().first);
cache.pop_back();
}
cache.emplace_front(key, value);
map[key] = cache.begin();
}
};
5. 性能优化与避坑指南
5.1 避免不必要的拷贝
当map的value是大型对象时,使用指针或移动语义可以显著提升性能:
cpp复制map<int, vector<string>> data;
// 低效写法
vector<string> temp = {"a", "b", "c"};
data[1] = temp; // 发生拷贝
// 高效写法
data[1] = {"a", "b", "c"}; // 直接构造
data[2] = move(temp); // 移动语义
5.2 正确使用迭代器
map和set的迭代器稳定性需要特别注意:
cpp复制map<int, int> m = {{1,10}, {2,20}, {3,30}};
auto it = m.find(2);
m.erase(1); // 安全,不影响it
m.erase(2); // it现在失效了!
if(it != m.end()) // 未定义行为
cout << it->second;
5.3 自定义比较函数
当key是自定义类型时,需要提供比较函数:
cpp复制struct Point {
int x, y;
bool operator<(const Point& p) const {
return x < p.x || (x == p.x && y < p.y);
}
};
map<Point, int> points;
对于unordered_map,还需要实现hash函数:
cpp复制struct PointHash {
size_t operator()(const Point& p) const {
return hash<int>()(p.x) ^ hash<int>()(p.y);
}
};
unordered_map<Point, int, PointHash> points;
6. 实战训练建议
要真正掌握map和set的应用,我建议按照以下顺序练习OJ题目:
- 基础应用:LeetCode 1, 242, 349
- 进阶技巧:LeetCode 347, 451, 692
- 综合应用:LeetCode 76, 159, 340
- 系统设计:LeetCode 146, 460
每次练习时,先尝试用普通数组实现,再改用map/set优化,体会性能差异。记录下每种解法的时间复杂度和空间复杂度,培养复杂度分析的直觉。
