C++ STL set 与 map 全套详解:关联式容器、红黑树底层、代码坑点、刷题实战

前面我们学习了普通二叉搜索树,但是普通 BST 存在退化风险。set / multiset / map / multimap属于 STL 有序关联式容器,底层是红黑树(平衡二叉搜索树),保证增删查稳定(O(logN))。本文把接口、底层原理、坑点、力扣例题全部讲透,同时补充拓展知识。

一、序列式容器 vs 关联式容器

1. 序列式容器

vector、list、deque、string、array属于序列式容器:元素按照存储位置来保存访问,交换两个元素,容器逻辑不会被破坏。

2. 关联式容器

set/map/multiset/multimapunordered_set/unordered_map属于关联式容器

  • 元素依靠key 关键字保存、查找;
  • 底层非线性树形结构,交换元素会直接破坏容器逻辑;
  • set/map底层红黑树(平衡 BST),有序;
  • unordered_xxx底层哈希表,无序。

区分:

  • set/multiset:纯 key 模型,对应手写 BST 的 key 版本;
  • map/multimap:key‑value 模型,对应手写 BST 的 key‑value 版本。

二、set /multiset

2.1 set 模板原型

cpp 复制代码
template<class T, class Compare = less<T>, class Alloc = allocator<T>>
class set;
  • T:元素类型,key 和 value 是同一个
  • Compare=less<T>:比较仿函数,默认升序;传greater<T>可以改为降序;
  • Alloc:空间配置器,一般不用传。

核心特性:

  1. 元素唯一,自动去重,不允许重复 key;
  2. 底层红黑树,迭代器遍历等价于中序遍历,输出有序
  3. 迭代器是双向迭代器 ,支持++/--不支持随机访问(不能it+n、不能下标[]
  4. ✨迭代器*itconst T禁止修改元素,修改 key 直接破坏红黑树结构。

2.2 set 核心接口总览

接口 功能 重点说明
insert(val) 插入元素 返回pair<iterator,bool>;已存在则插入失败,second=false
find(val) 查找元素 找到返回迭代器,找不到返回end();\(O(logN)\),优于算法库std::find\(O(N)\)
count(val) 统计个数 set 只能返回 0 或者 1,可以用来间接判断存在性
erase(val / pos / [first,last]) 删除 传值:删除该 key;传迭代器:删除迭代器指向元素;传区间删除[first,last)
lower_bound(val) 返回≥val 第一个元素迭代器 大于等于
upper_bound(val) 返回 > val 第一个元素迭代器 严格大于

lower_boundupper_bound配合,可以截取区间[itlow, itup)做删除或者遍历。

cpp 复制代码
set<int> myset = {10,20,30,40,50,60,70,80,90};
// 删除[30,60]闭区间
auto itlow = myset.lower_bound(30);   // >=30
auto itup  = myset.upper_bound(60);   // >60
myset.erase(itlow, itup);

2.3 multiset:允许元素重复

multiset模板参数、接口和 set 几乎一模一样,唯一区别允许 key 重复。由此带来 3 个高频坑点:

  1. find(x)返回中序遍历第一个等于 x 的迭代器,不是全部;想要遍历全部相同值,需要循环while(pos!=end() && *pos==x)
  2. count(x)返回 x 实际出现的次数,不再只是 0/1;
  3. ⚠️大坑:ms.erase(x)(传值)会删除容器中所有等于 x 的元素;只删一个,必须传迭代器ms.erase(pos)
cpp 复制代码
multiset<int> ms={4,2,7,2,4,8,4,5,4,9};
auto pos = ms.find(4);
if(pos != ms.end())
{
    ms.erase(pos); // ✅只删除一个4
    // ms.erase(4); // ❌会把全部4删掉
}

2.4 set 代码示例

cpp 复制代码
void test_set1()
{
	set<int> s = { 5,1,5,3,4,2,6,83,9,10,22 };
	// 中序遍历,自动排序+去重
	set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		// *it = 1; // 编译报错!不能修改set元素
		cout << *it << " ";
		++it;
	}
	cout << endl;

	s.insert(60);
	s.erase(6);

	auto ret1 = s.find(3); // O(logN)
	// auto ret1 = find(s.begin(), s.end(), 3); // 算法库find O(N),效率差很多

	// lower_bound upper_bound区间删除
	auto it1 = s.lower_bound(3);
	auto it2 = s.upper_bound(9);
	s.erase(it1, it2);
}

2.5 力扣实战例题

  1. 349. 两个数组的交集:两个数组分别构建 set,双迭代器有序遍历求交集;
  2. 142. 环形链表 II:把链表节点指针存入 set,遇到重复节点即为环入口。
cpp 复制代码
//两个数组的交集
class Solution {
public:
	vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
		set<int> s1(nums1.begin(), nums1.end());
		set<int> s2(nums2.begin(), nums2.end());
		// 因为set遍历是有序的,有序值,依次⽐较
		// ⼩的++,相等的就是交集
		vector<int> ret;
		auto it1 = s1.begin();
		auto it2 = s2.begin();
		while (it1 != s1.end() && it2 != s2.end())
		{
			if (*it1 < *it2)
			{
				it1++;
			} else if (*it1 > *it2)
			{
				it2++;
			} else
			{
			ret.push_back(*it1);
			it1++;
			it2++;
			}
		} 
		return ret;
	}
}
cpp 复制代码
//环形链表 II
class Solution {
public:
	ListNode* detectCycle(ListNode* head) {
		set<ListNode*> s;
		ListNode* cur = head;
		while (cur)
		{
			auto ret = s.insert(cur);
			if (ret.second == false)
				return cur;
			cur = cur->next;
		} r
			eturn nullptr;
	}
}

set 存储指针,指针本身作为 key,判断节点是否已经访问过,代码简洁。

三、map /multimap

3.1 map 模板原型

cpp 复制代码
template<class Key, class T, class Compare = less<Key>, class Alloc = allocator<pair<const Key,T>>>
class map;
  • Key:关键字;T:映射的 value;
  • 底层节点存储类型:value_type = pair<const Key, T>

⚠️pair 里面的 first(key)是 const!不能修改 key,但 second(value)可以修改,修改 key 破坏红黑树。

3.2 pair 简介(map 存储键值对的基础)

cpp 复制代码
template<class T1,class T2>
struct pair
{
    T1 first;
    T2 second;
    pair(const T1& a,const T2& b):first(a),second(b){}
};
// 辅助函数make_pair,构造pair对象
template<class T1,class T2>
pair<T1,T2> make_pair(T1 x,T2 y);

map 插入的 4 种等价写法:

cpp 复制代码
map<string,string> dict;
dict.insert(pair<string,string>("sort","排序"));
dict.insert(make_pair("left","左边"));
dict.insert({"insert","插入"}); // C++11 initializer_list
dict.emplace("string","字符串");// emplace原地构造,减少拷贝

3.3 map 核心接口

find/count/lower_bound/upper_bound/erase接口语义和 set 几乎完全一样,区别:

  1. find(key)返回迭代器,迭代器指向pair<const Key,T>it->first是 key,it->second是 value;
  2. 迭代器it->first不能修改,it->second可以修改;
  3. 重磅接口operator[],multimap 没有这个接口!
operator \[\] 底层原理(面试高频)
cpp 复制代码
// 伪代码,标准库内部实现逻辑
mapped_type& operator[](const key_type& k)
{
    // 如果k不存在,insert插入{k,value默认构造对象};
    // 如果k已经存在,insert直接返回已有节点迭代器,second=false
    pair<iterator,bool> ret = insert({k, mapped_type()});
    return ret.first->second;
}

[]三种行为:

  1. 修改dict["left"] = "左边"; key 存在,返回 value 引用赋值;
  2. 插入dict["test"]; key 不存在,插入 key,value 使用类型默认值;
  3. 查找cout << dict["left"]; key 存在读取 value;

⚠️重大坑点:不要用[]单纯判断 key 是否存在! 如果 key 不存在,[]会悄悄插入一条数据,污染 map!判断存在性优先使用find()或者count()

cpp 复制代码
// ❌错误写法,如果"apple"不存在,会插入{"apple",0}
if(countMap["apple"]) {}

// ✅正确判断存在
if(countMap.find("apple") != countMap.end()){}
if(countMap.count("apple")){}

3.4 经典场景:词频统计

两种写法,find版本和[]版本:

cpp 复制代码
void test_map1()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	map<string, int> countMap;

	//写法1 find
	for (auto& str : arr)
	{
		auto it = countMap.find(str);
		if (it != countMap.end())
		{
			it->second++;
		}
		else
		{
			countMap.insert({ str, 1 });
		}
	}

	//写法2 [],代码极度简洁,也是刷题最常用
	for (auto& str : arr)
	{
		countMap[str]++;
	}

	// C++17结构化绑定遍历
	for (auto& [k, v] : countMap)
	{
		cout << k << ":" << v << endl;
	}
}

3.5 multimap:允许 key 重复

  1. key 允许重复,value 可以各不相同;
  2. 不支持operator[] :同一个 key 对应多个 value,[]不知道返回哪一个 value;
  3. find(key)返回中序第一个匹配 key 的迭代器;想要遍历全部同 key,循环迭代器判断。

四、底层原理:红黑树是什么?

set/map 底层是红黑树,一种自平衡二叉搜索树,解决普通 BST 有序插入退化成链表的缺陷,保证增删查稳定\(O(logN)\)。

红黑树五条核心性质:

  1. 每个节点颜色红色或者黑色;
  2. 根节点是黑色;
  3. 所有 NIL 叶子(空哨兵节点)是黑色;
  4. 红色节点的两个子节点必须是黑色(不能连续红节点);
  5. 任意节点到它所有后代 NIL 叶子,路径黑色节点数量相等(黑高相同)。

面试不需要手写红黑树旋转,但是要说出:红黑树通过变色 + 旋转控制黑高,限制树高度,避免 BST 退化。

迭代器失效规则

  1. insert :set/map 插入,原有迭代器全部不会失效,只有新增节点;
  2. erase只有被删除节点的迭代器失效,其余迭代器全部有效

和 vector 完全不同:vector 插入可能全部迭代器失效。原因红黑树只是旋转变色,节点内存地址不会改变。

五、set/map vs unordered_set/unordered_map 对比

特性 set/map(红黑树) unordered_set/unordered_map(哈希表)
底层 红黑树平衡 BST 哈希表
时间复杂度 稳定\(O(logN)\) 平均\(O(1)\),最坏\(O(N)\)哈希冲突
有序性 按键有序,中序遍历输出有序 完全无序,输出顺序不可预测
迭代器 双向迭代器++ -- 前向迭代器,只支持++
接口 支持lower_bound / upper_bound区间查找 不支持区间查找,只能精确查找
key 要求 需要<严格弱序比较 需要哈希函数 + ==相等比较

使用选择:

  • 需要有序输出、范围查找、稳定性能:选set/map
  • 只需要快速查找,不在乎顺序:选unordered系列。

六、高频坑点汇总

  1. set 的迭代器元素是const禁止修改元素 ;map 只能修改second,不能修改first
  2. multiseterase(值)删除全部匹配元素,删除单个必须传迭代器;
  3. map 不要用[]做存在性判断,不存在就会插入;multimap 没有[]
  4. std::find算法库是线性遍历\(O(N)\);优先使用容器自带find() \(O(logN)\);
  5. insert 对于已经存在 key 不会覆盖;[]会覆盖 value;
  6. lower_bound 是>=,upper_bound 是>,区间是左闭右开[itlow,itup)
  7. 迭代器是双向迭代器,不支持it + n,不能下标访问;
  8. 红黑树插入删除迭代器失效范围很小,只有 erase 被删的迭代器失效。

七、面试高频问答

set 和 multiset 区别?map 和 multimap 区别?

set/map key 唯一;multiset/multimap 允许 key 重复;multimap 没有operator[]

map 的 operator \[\] 底层实现?为什么 multimap 不能有 \[\]?

\[\] 内部调用 insert;key 不存在插入默认 value,返回 value 引用;multimap 一个 key 对应多个 value,无法确定返回哪一个 value。

set 为什么不能修改迭代器指向的元素?

set 元素本身就是 key,修改 key 直接破坏红黑树的有序结构,导致容器未定义行为。

map/set 底层是什么?为什么不用普通二叉搜索树?

底层红黑树;普通 BST 有序插入会退化成链表最坏 O (N);红黑树自平衡,保证稳定\(O(logN)\)。

lower_bound upper_bound 作用?区间是什么?

lower_bound 返回第一个 >=key 迭代器;upper_bound 返回第一个 > key 迭代器;区间[itlow,itup)左闭右开,用来遍历、删除区间元素。

map insert 和 \[\] 的区别?

insert:key 存在不会覆盖,返回 pair 标记是否插入成功;\[\]:key 不存在自动插入,key 存在直接修改 value。

八、力扣实战案例简单说明

  1. **138. 复制随机链表:**map 保存原节点与拷贝节点映射,快速处理 random 指针;
  2. **692. 前 K 个高频单词:**map 统计词频,结合 stable_sort/priority_queue 处理排序规则。
cpp 复制代码
//复制随机链表
class Solution {
public:
	Node* copyRandomList(Node* head) {
		map<Node*, Node*> nodeMap;
		Node* copyhead = nullptr, * copytail = nullptr;
		Node* cur = head;
		while (cur)
		{
			if (copytail == nullptr)
			{
				copyhead = copytail = new Node(cur->val);
			} 
			else
			{
			copytail->next = new Node(cur->val);
			copytail = copytail->next;
			} 
			// 原节点和拷⻉节点map kv存储
		    nodeMap[cur] = copytail;
			cur = cur->next;
		} 
		// 处理random
		cur = head;
		Node* copy = copyhead;
		while (cur)
		{
			if (cur->random == nullptr)
			{
				copy->random = nullptr;
			} 
			else
			{
			copy->random = nodeMap[cur->random];
			} 
			cur = cur->next;
			copy = copy->next;
		} 
		return copyhead;
	}
}
cpp 复制代码
//前 K 个高频单词
//解决思路1
class Solution {
public:
	struct Compare
	{
		bool operator()(const pair<string, int>& x, const pair<string, int>& y)
			const
		{
			return x.second > y.second;
		}
	};
	vector<string> topKFrequent(vector<string>& words, int k) {
		map<string, int> countMap;
		for (auto& e : words)
		{
			countMap[e]++;
		} 
		vector<pair<string, int>> v(countMap.begin(), countMap.end());
		// 仿函数控制降序
		stable_sort(v.begin(), v.end(), Compare());
		//sort(v.begin(), v.end(), Compare());
		// 取前k个
		vector<string> strV;
		for (int i = 0; i < k; ++i)
		{
			strV.push_back(v[i].first);
		}
		return strV;
	}
}

//解决思路2

class Solution {
public:
	struct Compare
	{
		bool operator()(const pair<string, int>& x, const pair<string, int>& y)
			const
		{
			return x.second > y.second || (x.second == y.second && x.first <y.first);;
		}
	};
	vector<string> topKFrequent(vector<string>& words, int k) {
		map<string, int> countMap;
		for (auto& e : words)
		{
			countMap[e]++;
		} 
		vector<pair<string, int>> v(countMap.begin(), countMap.end());
		// 仿函数控制降序,仿函数控制次数相等,字典序⼩的在前⾯
		sort(v.begin(), v.end(), Compare());
		// 取前k个
		vector<string> strV;
		for (int i = 0; i < k; ++i)
		{
			strV.push_back(v[i].first);
		} 
		return strV;
	}
};


//解决思路3

class Solution {
public:
	struct Compare
	{
		bool operator()(const pair<string, int>& x, const pair<string, int>& y)
			const
		{
			// 要注意优先级队列底层是反的,⼤堆要实现⼩于⽐较,所以这⾥次数相等,想要字典序⼩的在前⾯要⽐较字典序⼤的为真
			return x.second < y.second || (x.second == y.second && x.first > y.first);
		}
	};
	vector<string> topKFrequent(vector<string>& words, int k) {
		map<string, int> countMap;
		for (auto& e : words)
		{
			countMap[e]++;
		}
		// 将map中的<单词,次数>放到priority_queue中,仿函数控制⼤堆,次数相同按照字典序规则排序
		priority_queue<pair<string, int>, vector<pair<string, int>>, Compare>p(countMap.begin(), countMap.end());
		vector<string> strV;
		for (int i = 0; i < k; ++i)
		{
			strV.push_back(p.top().first);
			p.pop();
		} 
		return strV;
	}
};
相关推荐
程曦曦3 小时前
MySQL 生产库误删 98 张表后的时间点恢复实战:从 binlog 解析到资金对账
linux·数据结构·其他·算法·ubuntu·运维开发
郝学胜-神的一滴3 小时前
Effective Python 条款 13:善用星号解包,告别下标切片拆分的坑
服务器·开发语言·数据结构·python·程序人生·pycharm
Logic1013 小时前
C语言/数据结构位运算题解:异或XOR找出流水线上的“独特零件编号“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
j7~4 小时前
【Linux网络加餐】(篇六)网络版计算器(上):模板方法模式、序列化与 JSON
linux·c++·json·序列化·反序列化·网络版计算机
weixin_307779134 小时前
C++代码实现MATLAB中的ode45函数功能
开发语言·c++·算法·matlab
张小姐的猫4 小时前
【AI大模型接入SDK】 —— 数据管理 & 与Session模块进行联动
数据结构·数据库·c++·人工智能·python·chatgpt
東隅已逝,桑榆非晚5 小时前
vector(模拟实现)
c++·笔记·学习
careathers5 小时前
【数据结构】栈
java·数据结构
mmmmath_35 小时前
LeetCode.018.四数之和
数据结构·算法·leetcode