《解剖map和set:隐藏在红黑树中的设计哲学》

 前言

本篇博客将详细介绍C++的map和set

💖 个人主页熬夜写代码的小蔡

本篇文章所涉及的代码在Gitte仓库

码云——map和set

🖥 文章专栏:C++

若有问题 评论区见

🎉欢迎大家点赞👍收藏⭐文章 ​

d6b44a6e06316a12d6dec7f29fc29d7b.gif

ca618b2856ce45c78a02cc2a85b3b0f6.gif

一.序列式容器和关联式容器

⾯我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,⽐如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
关联式容器也是⽤来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是⾮线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。本章节讲解的map和set底层是红⿊树,红⿊树是⼀颗平衡⼆叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构

二.set

2.1set的文档介绍

https://legacy.cplusplus.com/reference/set/https://legacy.cplusplus.com/reference/set/

2.2set类的介绍

set 是一个有序的、不重复的集合容器。它的特点是:

  • 自动去重:插入重复元素会被忽略。
  • 自动排序:元素默认按升序排列。

2.3set的构造与迭代器 

set的⽀持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。

2.3.1默认构造函数

set<int> s1;// 创建一个空的set,存储int类型

2.3.2范围构造函数 

vector<int> a1 = { 1,2,5,3,9,7 };
set<int> s2(a1.begin(), a1.end());// 去重且排序

2.3.3拷贝构造函数

set<int> s3(s2);

2.3.4自定义排序规则构造函数

set<int, greater<int>> s4; // 降序排列
s4.insert(3);
s4.insert(1);
s4.insert(4);

 2.4set的增删查

2.4.1set的增加

//插入单个元素
set<int> s;
s.insert(3); // 插入元素3
s.insert(1); // 插入元素1
s.insert(4); // 插入元素4
// s = {1, 3, 4}

//插入多个元素
vector<int> vec = { 2, 5, 2, 6 };
s.insert(vec.begin(), vec.end()); // 插入vec中的元素
// s = {1, 2, 3, 4, 5, 6}

//插入初始化列表
s.insert({ 7, 8, 9 }); // 插入初始化列表
// s = {1, 2, 3, 4, 5, 6, 7, 8, 9}

2.4.2set的查找

set<int> s; 
// 查找val,返回val所在的迭代器,没有找到返回end()
auto it = s.find(5);
if (it != s.end()) 
{
	std::cout << "找到元素:" << *it << std::endl;
}
else {
	std::cout << "未找到元素!" << std::endl;
}


//count 方法用于检查元素是否存在,返回值为 1(存在)或 0(不存在)。
if (s.count(5) > 0) 
{
	std::cout << "元素存在!" << std::endl;
}
else 
{
	std::cout << "元素不存在!" << std::endl;
}

 2.4.3set的删除

set<int> s;

//通过值删除元素:
s.erase(3); // 删除元素3
// s = {1, 2, 4, 5, 6, 7, 8, 9}
//通过迭代器删除元素:
auto it = s.find(4); // 查找元素4
if (it != s.end()) {
	s.erase(it); // 删除找到的元素
}
// s = {1, 2, 5, 6, 7, 8, 9}
//通过迭代器范围删除元素:
auto it1 = s.find(5);
auto it2 = s.find(8);
s.erase(it1, it2); // 删除[5, 8)范围内的元素
// s = {1, 2, 8, 9}
//删除所有元素
s.clear(); // 清空set
// s = {}

 2.5set的lower_bound和upper_bound

2.5.1lower_bound和upper_bound的介绍

lower_boundset 中的一个成员函数,用于查找第一个 大于等于 指定值的元素。如果找到,则返回指向该元素的迭代器;如果未找到,则返回 end()

  • 特点

    • 查找的是 ≥ key 的第一个元素。
    • 适用于查找插入位置或范围查询的起始位置。

upper_boundset 中的另一个成员函数,用于查找第一个 大于 指定值的元素。如果找到,则返回指向该元素的迭代器;如果未找到,则返回 end()

  • 特点

    • 查找的是 > key 的第一个元素。
    • 适用于查找范围查询的结束位置。

 2.5.2lower_bound和upper_bound的区别

//单个元素查找
set<int> s = { 1, 3, 5, 7, 9 };

auto it1 = s.lower_bound(5);
auto it2 = s.upper_bound(5);

if (it1 != s.end()) {
    cout << "lower_bound(5): " << *it1 << std::endl; // 输出: 5
}
if (it2 != s.end()) {
	cout << "upper_bound(5): " << *it2 << std::endl; // 输出: 7
}


//范围查找
set<int> s = { 1, 3, 5, 7, 9 };

auto it1 = s.lower_bound(3); // 指向3
auto it2 = s.upper_bound(7); // 指向9

for (auto it = it1; it != it2; ++it) {
	cout << *it << " "; // 输出: 3 5 7
}

 三.map

3.1map的介绍

 map的文档介绍

https://legacy.cplusplus.com/reference/map/https://legacy.cplusplus.com/reference/map/

3.2 map类的介绍

 map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key⽀持⼩于⽐较,如果不⽀持或者需要的话可以⾃⾏实现仿函数传给第⼆个模版参数,map底层存储数据的内存是从空间配置器申请的。⼀般情况下,我们都不需要传后两个模版参数。map底层是⽤红⿊树实现,增删查改效率是 O(logN) ,迭代器遍历是⾛的中序,所以是按key有序顺序遍历的。

3.3pair类型介绍 

std::pair 是 C++ STL 中的模板类,用于将两个值捆绑为一个单一对象,map底层的红⿊树节点中的数据,使⽤pair<Key, T>存储键值对数据。

3.3.1核心结构

template <class T1, class T2>
struct pair {
    T1 first;   // 存储键(Key)
    T2 second;  // 存储值(Value)
    // ... 构造函数及其他成员函数
};

3.3.2,pair在map中的应用场景

3.3.2.1定义映射元素

std::map<K, V> 中的每个元素本质都是 pair<const K, V>

  • Key 不可变性first 是 const K 类型,禁止修改(维护红黑树的有序性)
  • Value 可变性second 允许自由修改
     
3.3.2.2插入元素 

必须通过 pair 或等价方式插入:

map<int, string> myMap;

// 方法1:直接构造 pair
myMap.insert(pair<int, string>(1, "Apple"));

// 方法2:使用 make_pair(自动推导类型)
myMap.insert(make_pair(2, "Banana"));

// 方法3:C++11 统一初始化
myMap.insert({ 3, "Cherry" });  // 隐式转换为 pair
3.3.2.3访问与遍历

当遍历 map 时,迭代器指向的元素即为 pair 对象:

for (auto& e : myMap) {
	cout << "Key: " << e.first   // 键(不可修改)
		<< ", Value: " << e.second; // 值(可修改)
}

// 迭代器版本
for (auto it = myMap.begin(); it != myMap.end(); ++it) {
	it->second = "Fruit"; // 允许修改 Value
	// it->first = 10;    ❌ 编译错误!Key 是 const
}

 3.4map的构造

3.4.1默认构造

map<int, string> Mymap;// 创建一个空的 map,键为 int,值为 string

 3.4.2范围构造函数

vector<pair<int,string>> a= { {1, "Apple"}, {2, "Banana"} };
map<int, string> Mymap2 = { a.begin(),a.end() };// 从 vector 构造 map

3.4.3拷贝构造函数 

map<int, string> Mymap3 = { {1, "Apple"}, {2, "Banana"} };
map<int, string> Mymap4(Mymap3);

3.4.4初始化列表构造 

map<int, string> myMap = {
{1, "Apple"},
{2, "Banana"},
{3, "Cherry"}
}; // 初始化列表构造

 3.5set的增删查

3.5.1set的增加

// 插入单个键值对
myMap.insert(make_pair(1, "Apple")); // 方法1:make_pair
myMap.insert({ 2, "Banana" });              // 方法2:初始化列表

// 插入一组键值对
vector<pair<int, string>> vec = { {3, "Cherry"}, {4, "Durian"} };
myMap.insert(vec.begin(), vec.end());     // 方法3:范围插入

3.5.2set的删除

/使用 erase 删除指定键
	myMap.erase(1); // 删除键为 1 的元素
	//使用 erase 删除迭代器指向的元素
	auto it = myMap.find(2); // 查找键为 2 的元素
	if (it != myMap.end()) 
	{
		myMap.erase(it);     // 删除该元素
	}
	//使用 erase 删除范围内的元素
	auto it1 = myMap.find(3);
	auto it2 = myMap.find(5);
	if (it1 != myMap.end() && it2 != myMap.end()) 
	{
		myMap.erase(it1, it2); // 删除 [it1, it2) 范围内的元素
	}

3.5.3set的查找

//使用 find 查找键
auto it = myMap.find(3);
if (it != myMap.end()) {
	cout << "Found: " << it->second << endl;
}
else {
	cout << "Not found" << endl;
}
//使用 count 统计键
if (myMap.count(4) > 0) {
	cout << "Key 4 exists" << endl;
}

3.6 map的数据修改

前⾯我提到map⽀持修改mapped_type 数据,不⽀持修改key数据,修改关键字数据,破坏了底层搜索树的结构。
map第⼀个⽀持修改的⽅式时通过迭代器,迭代器遍历时或者find返回key所在的iterator修改,map
还有⼀个⾮常重要的修改接⼝operator[],但是operator[]不仅仅⽀持修改,还⽀持插⼊数据和查找数据,所以他是⼀个多功能复合接⼝

3.6.1增加数据

当你使用 [] 运算符时,如果指定的键不存在,map 会自动插入一个新的键值对。

map<string, int> scores;

scores["Alice"] = 90;  // 键 "Alice" 不存在,自动插入 {"Alice", 90}
scores["Bob"] = 85;    // 键 "Bob" 不存在,自动插入 {"Bob", 85}

for (const auto& pair : scores) {
	cout << pair.first << ": " << pair.second << endl;
}

3.6.2查找数据

当你使用 [] 运算符时,如果键存在,map 会返回该键对应的值。

map<string, int> scores = { {"Alice", 90}, {"Bob", 85} };

int aliceScore = scores["Alice"];  // 查找键 "Alice" 对应的值
cout << "Alice's score: " << aliceScore << endl;

3.6.3修改数据

当你使用 [] 运算符时,如果键已经存在,map 会直接修改该键对应的值。

map<string, int> scores = { {"Alice", 90}, {"Bob", 85} };

scores["Alice"] = 95;  // 修改键 "Alice" 对应的值
cout << "Alice's new score: " << scores["Alice"] << endl;

3.7 multimap和map的差异


multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么
insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如find时,有多个key,返回中序第⼀个。其次就是multimap不⽀持[],因为⽀持key冗余,[]就只能⽀持插⼊了,不能⽀持修改 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值