1. 序列式容器与关联式容器对比
在C++标准模板库(STL)中,容器主要分为两大类:
-
序列式容器:如
string
、vector
、list
、deque
等,这些容器维护元素的线性序列,元素之间的关系仅由它们在容器中的位置决定。特点是:- 元素按存储位置顺序保存和访问
- 交换元素位置不会破坏容器结构
- 适合需要保持插入顺序的场景
-
关联式容器:如
map/set
系列和unordered_map/unordered_set
系列,这些容器基于元素的关键字而非位置来组织数据。特点是:- 元素按关键字保存和访问
- 通常采用非线性结构(如树或哈希表)
- 交换元素可能破坏内部结构
- 提供高效的查找操作(O(logN)或O(1))
本章重点讲解基于红黑树实现的map
和set
容器,它们提供了有序的键值存储和高效的查找能力。
2. set容器详解
2.1 set基本特性
set
是存储唯一键的关联容器,底层用红黑树实现,提供O(logN)的查找、插入和删除操作。主要特点包括:
- 元素自动排序(默认升序)
- 键值唯一(不允许重复)
- 不可直接修改元素值(会破坏排序)
template <class T,
class Compare = less<T>,
class Alloc = allocator<T>>
class set;
注:
set默认情况下遍历出来是升序,但是如果定义对象时显示传参greater,就是降序!
2.2 set常用操作
构造方式:
set<int> s1; // 默认构造
set<int> s2 = {1, 2, 3}; // 初始化列表构造
set<int> s3(s2.begin(), s2.end()); // 迭代器范围构造
插入元素:
s1.insert(4); // 插入单个元素
s1.insert({5, 6, 7}); // 插入初始化列表
插入的返回值也是一个pair结构pair中存储了布尔类型和迭代器,分别代表此次插入是否成功,若成功则返回被插入元素迭代器的位置
查找与删除:
//std::set<int>::iterator it;
auto it = s1.find(4); // 查找,返回迭代器
if(it != s1.end()) {
s1.erase(it); // 通过迭代器删除
}
size_t n = s1.erase(5); // 直接通过值删除,返回删除数量
2.3 set应用示例
去重排序:
vector<int> nums = {3,1,4,1,5,9,2,6,5};
set<int> unique_nums(nums.begin(), nums.end());
// unique_nums: {1,2,3,4,5,6,9}
集合运算:
// 求两个数组的交集
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int> s1(nums1.begin(), nums1.end());
set<int> s2(nums2.begin(), nums2.end());
vector<int> res;
auto it1 = s1.begin(), it2 = s2.begin();
while(it1 != s1.end() && it2 != s2.end()) {
if(*it1 < *it2) ++it1;
else if(*it1 > *it2) ++it2;
else {
res.push_back(*it1);
++it1; ++it2;
}
}
return res;
}
3. map容器详解
3.1 map基本特性
map
是存储键值对的关联容器,同样基于红黑树实现,提供O(logN)的操作效率。主要特点包括:
- 按键自动排序(默认升序,和set一样)
- 键唯一,值可重复
- 可修改值但不可直接修改键
template <class Key,
class T,
class Compare = less<Key>,
class Alloc = allocator<pair<const Key,T>>>
class map;
3.2 pair类型
map
中存储的元素类型为pair<const Key, T>
,STL提供了方便的pair
操作:
pair<string, int> p1("apple", 3); // 直接构造
auto p2 = make_pair("banana", 2); // 使用make_pair函数
cout << p1.first << ": " << p1.second; // 访问成员
3.3 map常用操作
插入元素:
map<string, int> m;
m.insert({"apple", 3}); // 方式1
m.insert(make_pair("banana", 2)); // 方式2
m["orange"] = 5; // 使用[]操作符
map的方括号设计的十分巧妙,它的参数的pair中的first,返回值是pair中的second,并且它返回的是引用,可以根据first修改second它可以用来计数,请看如下代码:
string arr[]={"苹果","西瓜","香蕉","苹果","西瓜","西瓜","西瓜","苹果"};
map<string,int> countmap;
for(auto str : arr)
{
countmap[str]++;
}
访问元素:
auto it = m.find("apple"); // 查找
if(it != m.end()) {
cout << it->first << ": " << it->second;
}
int count = m["banana"]; // 使用[]访问,若不存在会插入
删除元素:
m.erase("apple"); // 按键删除
m.erase(m.begin()); // 通过迭代器删除
m.erase(m.begin(), m.end()); // 删除范围
3.4 map应用示例
词频统计:
vector<string> words = {"apple", "banana", "apple", "orange"};
map<string, int> word_count;
// 方法1:使用find和insert
for(const auto& word : words) {
auto it = word_count.find(word);
if(it != word_count.end()) {
it->second++;
} else {
word_count.insert({word, 1});
}
}
// 方法2:使用[]操作符(更简洁)
for(const auto& word : words) {
word_count[word]++;
}
复杂问题简化:
// 复制带随机指针的链表
Node* copyRandomList(Node* head) {
map<Node*, Node*> node_map;
Node* curr = head;
// 第一次遍历创建所有节点
while(curr) {
node_map[curr] = new Node(curr->val);
curr = curr->next;
}
// 第二次遍历设置指针
curr = head;
while(curr) {
node_map[curr]->next = node_map[curr->next];
node_map[curr]->random = node_map[curr->random];
curr = curr->next;
}
return node_map[head];
}
4. multiset和multimap
multiset
和multimap
是允许键重复的版本,与set
和map
的主要区别:
- 允许存储多个相同键
multimap
不支持[]
操作符find
返回第一个匹配元素的迭代器count
返回匹配元素的数量
multiset<int> ms = {1,2,2,3,3,3};
cout << ms.count(2); // 输出2
multimap<string, int> mm;
mm.insert({"a", 1});
mm.insert({"a", 2}); // 允许插入相同键
5. 使用建议
-
有序性:
- 需要元素有序:选择
map/set
(基于红黑树) - 不需要有序:选择
unordered_map/unordered_set
(基于哈希表,O(1)查找)
- 需要元素有序:选择
-
重复键:
- 键唯一:
map/set
- 允许重复键:
multimap/multiset
- 键唯一:
-
操作复杂度:
map/set
:插入、删除、查找都是O(logN)unordered_map/unordered_set
:平均O(1),最差O(N)
6. 实际应用案例
6.1 前K个高频单词
vector<string> topKFrequent(vector<string>& words, int k) {
map<string, int> count_map;
for(const auto& word : words) {
count_map[word]++;
}
// 自定义比较器:频率降序,同频按字典序升序
auto cmp = [](const pair<string,int>& a, const pair<string,int>& b) {
return a.second > b.second || (a.second == b.second && a.first < b.first);
};
vector<pair<string,int>> vec(count_map.begin(), count_map.end());
sort(vec.begin(), vec.end(), cmp);
vector<string> result;
for(int i = 0; i < k; ++i) {
result.push_back(vec[i].first);
}
return result;
}
6.2 环形链表检测
ListNode *detectCycle(ListNode *head) {
set<ListNode*> visited;
while(head) {
if(visited.count(head)) {
return head;
}
visited.insert(head);
head = head->next;
}
return nullptr;
}