二叉搜索双壁——map和set

一、引言

在 C++ 编程里,map 和 set 是两个特别好用的 "智能工具箱",专门帮我们存数据、找数据、管数据 ,比普通数组、列表方便太多,而且自带两个超实用的 "隐藏技能":自动排序 + 自动去重

可以把它们简单理解成两种不同的 "记录本":

**Set 就是一个 "独一无二的有序名单"**你往里面放数字、字符串都行,它会自动帮你做两件事:

  • 绝不重复:同样的东西放两次,它只留一个;
  • 自动排好序:放进去是乱的,拿出来自动按大小 / 先后排整齐。它只存一个值,适合用来做 "去重、查重、有序列表"。

Map 就是一个 "带名字的有序字典" 它存的是一对一对的数据(名字 + 内容),比如 "学号→姓名""单词→解释":

  • 名字(key)不能重复,还会自动排序;
  • 想查数据,直接报 "名字",瞬间就能找到对应的内容。它存键 + 值,适合用来做 "映射、查找、对应关系"。

两个工具底层都用了高效的树形结构,找数据特别快,数据再多也不卡顿,是写程序时处理数据最常用、最省心的两个容器。

两个容器的底层都是红黑树,是优化版本的高效的二叉搜索树,关于二叉搜索树的内容在这里:🔜二叉搜索树

二、set的相关接口

头文件<set>,性质:只能存储key值不同的元素,并按照指定顺序排列成树。

一、构造和遍历:

1.构造:

构造分为三种:

首先介绍一下set的模板参数:

1.存储数据类型

2.定义内部数据存储顺序的比较仿函数(默认是排升序,即每个节点"左小右大")

3.空间配置器(一般不用传参,使用缺省值)

其次是构造函数的使用:

1.空构造

例如:set<int> a;

2.c数组风格构造

例如:set<int> arr={1,2,3,4};

3.迭代器区间构造

将已有容器或数组中的元素赋值到set中:传入参数是(容器第一个元素迭代器或数组首元素地址,容器最后一个元素下一个位置的迭代器或数组末尾元素下一个元素的地址)

4.拷贝构造

利用已有的set对象来构造一个新的set对象

2.迭代器和遍历:

使用迭代器++方式的遍历默认走的是中序遍历,一定是有序的。

set的迭代器遍历只支持读,不支持改。

3.代码演示:

cpp 复制代码
//构造升序排列的set
//c数组风格构造
cout << "------------set1----------------" << endl;
set<int> set1 = { 1,2,3,4,5,6,7,8,9,10 };
set<int>::iterator it1 = set1.begin();
while (it1 != set1.end())
{
	cout << *it1 << " ";
	it1++;
}
cout << endl;//1 2 3 4 5 6 7 8 9 10

//迭代器区间构造
cout << "-----------------set2--------------" << endl;
vector<int> arr = { 1,2,3,4,5,6,7,8,9,10 };
set<int> set2(arr.begin(), arr.end());
set<int>::iterator it2 = set2.begin();
while (it2 != set2.end())
{
	cout << *it2 << " ";
	it2++;
}
cout << endl;//1 2 3 4 5 6 7 8 9 10

//构造降序排列的set
cout << "------------set3----------------" << endl;
set<int, greater<int> >set3 = { 1,2,3,4,5,6,7,8,9,10 };
set<int, greater<int>>::iterator it3 = set3.begin();  

while (it3 != set3.end())
{
	cout << *it3 << " ";
	it3++;
}
cout << endl;//10 9 8 7 6 5 4 3 2 1

//迭代器区间构造
cout << "-----------------set4--------------" << endl;
vector<int>arr2 = { 1,2,3,4,5,6,7,8,9,10 };
set<int, greater<int>> set4(arr2.begin(), arr2.end());
set<int, greater<int>>::iterator it4 = set4.begin();  // 类型匹配
while (it4 != set4.end())
{
	cout << *it4 << " ";
	it4++;
}
cout << endl;//10 9 8 7 6 5 4 3 2 1
//拷贝构造
cout << "---------------set5----------------" << endl;
set<int> set5(set1);
set<int, greater<int>>::iterator it5 = set5.begin();  // 类型匹配
while (it5 != set5.end())
{
	cout << *it5 << " ";
	it5++;
}
cout << endl;//1 2 3 4 5 6 7 8 9 10

二、增删查:

插入insert

1.插入一个值 返回值是pair类型:

pair第二个参数是一个布尔值,取决于插入的成功或失败,由于set只允许存储不同信息的元素,所以插入失败意味着set中已有该元素,返回该已有元素的迭代器作为pair第一个参数,插入成功就返回新插入节点的迭代器。

2.指定迭代器位置的附近插入一个元素,可以认为传参的迭代器是你给编译器的一个提示,编译器会根据二叉搜索树的结构插入元素并返回新插入节点的迭代器。

3.插入一个迭代器区间指示的元素:相当于将该区间涵盖的元素排序加去重存储到set中

代码示例:

cpp 复制代码
set<int> a;
//插入一个值
a.insert(3);
auto it = a.begin();
while (it != a.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;//3
//指定迭代器位置的附近插入一个元素
a.insert(it, 4);
it = a.begin();
while (it != a.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;//3 4
//插入一段迭代器区间指示的元素
vector <int> v1 = { 1,2,3,4,5 };
a.insert(v1.begin(), v1.end());
it = a.begin();
while (it != a.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;//1 2 3 4 5

查找和计数

find:

给定值返回相应的迭代器位置,如果不存在就返回set的末尾元素的下一个位置的迭代器

count:

给定值判断这个值是否在set中,存在返回1,反之返回0;

lower_bound :传入一个值,返回中序遍历情况下第一个满足>=这个值节点的迭代器

upper_bound :传入一个值,返回中序遍历情况下第一个满足> 这个值节点的迭代器

equal_range:传入值,返回两个参数都是指示这个值的迭代器的pair(意义不大,主要是为了适配multiset)

删除erase

1.按迭代器位置删除,搭配find使用

2.按值删除,返回删除元素的个数,1个表示删除成功,0表示set中无该元素

3.删除一段区间,通常搭配 lower_bound,upper_bound 使用

代码示例:

cpp 复制代码
set<int> s = { 1,2,3,4,5,6,7,23,44,78 };
for (int i = 1; i <20 ; i *= 2)
{
	auto it=s.find(i);
	if (it == s.end())
		cout << i << "不存在" << endl;
	else
		cout << i << "存在" << endl;
}
cout << "-------------------------" << endl;
/*1存在
  2存在
  4存在
  8不存在
  16不存在*/

//按值删除
for (int i = 1; i < 20; i *= 2)
{
	auto it = s.erase(i);
	if (it == 0)
		cout << i << "不存在" << endl;
	else
		cout << i << "删除成功" << endl;
}
/*  1删除成功
	2删除成功
	4删除成功
	8不存在
	16不存在
*/

//迭代器区间插入
s.insert({ 1,2,4 });//s:1,2,3,4,5,6,7,23,44,78

auto start = s.lower_bound(1);//找>=1的第一个位置
auto finish = s.upper_bound(7);//找>1的第一个位置
s.erase(start, finish);//按区间删除1-7,传参区间左闭右开。
auto it = s.begin();
while (it != s.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;//23 44 78

//按迭代器删除
s.insert({ 1,2,3,4,5,6,7 });//s:1,2,3,4,5,6,7,23,44,78
for (int i = 1; i < 20; i *= 2)
{
	auto it = s.find(i);
	if (it == s.end())
	{
		;
	}
	else
		s.erase(it);
}

it = s.begin();
while (it != s.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;// 3 5 6 7 23 44 78
//看1是否在set中
cout << s.count(1) << endl;//0

三、multiset相关接口

multiset是set的一对孪生兄弟,和set的区别就是它可以存储不同key值的元素

所以大部分接口都和set相同,这里介绍二者有不同之处的接口:

1.构造和区间插入

multiset的构造和区间插入不会删除重复值。

2.count

与set的count相比,当所给定的key值在multiset中时,multiset的count返回的是这个key值相同的所有元素的个数,不存在具有这个key值的元素时,返回值还是0/

3.find

与set相比,multiset之中find返回的所有迭代器都是中序遍历第一个出现那个节点所对应的迭代器,

4.erase

multiset的按值删除会删除所有key值为这个值的节点

5.equal_range 返回值为key的这段元素的迭代器区间(左闭右开)构成的pair

multiset和set的差异示意代码:

cpp 复制代码
#include <iostream>
#include <set>
using namespace std;

// 辅助打印函数
template<typename T>
void print(const T& container) {
    for (auto& val : container) {
        cout << val << " ";
    }
    cout << endl;
}

int main() {
    //==================== set:不允许重复 ====================
    set<int> s;

    // 1. 插入:重复元素会失败
    cout << "=== set 测试 ===" << endl;
    auto ret1 = s.insert(10); // 成功
    auto ret2 = s.insert(20); // 成功
    auto ret3 = s.insert(10); // 重复,失败

    // insert 返回 pair<迭代器, 是否成功>
    cout << "插入10是否成功:" << boolalpha << ret3.second << endl;

    cout << "set 内容:";
    print(s); // 自动排序:10 20

    // 2. find:返回唯一元素的迭代器
    auto it = s.find(10);
    if (it != s.end())
        cout << "找到了:" << *it << endl;

    // 3. count:只能是 0 或 1
    cout << "count(10) = " << s.count(10) << endl;

    // 4. erase(值):只删一个
    s.erase(10);
    cout << "删除10后:";
    print(s);
    cout << endl;

    //==================== multiset:允许重复 ====================
    multiset<int> ms;

    cout << "=== multiset 测试 ===" << endl;
    ms.insert(10);
    ms.insert(20);
    ms.insert(10); // 重复也能插入成功
    ms.insert(10);

    cout << "multiset 内容:";
    print(ms); // 自动排序,保留重复:10 10 10 20

    // 1. find:返回第一个匹配元素
    auto pos = ms.find(10);
    if (pos != ms.end())
        cout << "第一个10:" << *pos << endl;

    // 遍历所有相同元素
    cout << "所有10:";
    while (pos != ms.end() && *pos == 10) {
        cout << *pos << " ";
        ++pos;
    }
    cout << endl;

    // 2. count:返回实际个数(可大于1)
    cout << "count(10) = " << ms.count(10) << endl;

    // 3. erase(值):删除所有等于该值的元素
    ms.erase(10);
    cout << "erase(10) 后:";
    print(ms);

    // 4. 只想删除一个:用迭代器删除
    ms.insert(10);
    ms.insert(10);
    cout << "重新插入两个10:";
    print(ms);

    auto one = ms.find(10);
    if (one != ms.end()) {
        ms.erase(one); // 只删这一个
    }
    cout << "只删除一个10后:";
    print(ms);

    //==================== 核心区别总结====================
    /*
    set 与 multiset 使用时的核心区别:
    1. 唯一性:
        set   不允许重复元素,插入重复会失败;
        multiset 允许重复元素,插入总是成功。

    2. insert 返回值:
        set   返回 pair<迭代器, bool>,可判断是否插入成功;
        multiset 只返回迭代器,无需判断成功与否。

    3. find:
        set   找到唯一元素;
        multiset 找到第一个相同元素,后续要自己遍历。

    4. count:
        set   结果只能是 0 或 1;
        multiset 返回元素实际个数(≥0)。

    5. erase(值):
        set   删除这一个值(最多1个);
        multiset 删除所有等于该值的元素。

    共同点:
        都自动升序排序;底层都是红黑树;迭代器用法一致。
    */

    return 0;
}

四、map相关接口

头文件:<map>

map的性质:存放的是键值对,有两个数据key,val,key不可以重复,map的每一个元素是一个pair<key,val>

一,map的构造和遍历

map的构造:

1.默认构造,不手动调用。

2.迭代器区间构造,根据已有的存储数据类型为pair<key,val>的容器或结构体数组构造,传入指示容器首位的两个迭代器或指针,注意是左闭右开。

3.拷贝构造,使用已有的map进行构造

4.补充:c结构体风格构造。

和结构体数组的定义方式类似.,具体操作见代码演示.

map的遍历:

和set一样,map只可以使用迭代器进行中序遍历,遍历的结果对key而言是有序的,但是值得注意的是,由于pair<key,val>没有重载operator<< >>所以不能对map的单个节点直接进行输入输出,由于pair中key,val是公有的,并且不能修改key,所以输出要用pair指针->first/second pair对象.first/second

代码演示:

cpp 复制代码
	//迭代器区间构造
	pair<string, int> p[] = { {"apple",1},{"peach",4},{"orange",6} };
	map<string, int> map1(p, p + 3);
	for (auto e : map1)
	{
		cout << e.first << ":" << e.second << endl;
	}
//apple:1
//orange : 6
//peach : 4
	//c结构体风格构造
	map<string, int> map2= { {"apple",1},{"peach",4},{"orange",6} };
	auto it = map2.begin();
	while (it != map2.end())
	{
		cout << it->first << ":" << it->second << endl;
		it++;
	}
//apple:1
//orange : 6
//peach : 4
	//拷贝构造
	map<string, int> map3(map2);
	for (auto e : map3)
	{
		cout << e.first << ":" << e.second << endl;
	}
//apple:1
//orange : 6
//peach : 4

二,map的增删查

插入insert

使用方式和set大同小异,只不过把key值换成了pair<key,val>类型的对象。

1.插入一个pair<key,val>类型的对象,返回值是pair类型:

pair第二个参数是一个布尔值,取决于插入的成功或失败,由于set只允许存储不同信息的元素,所以插入失败意味着set中已有该元素,返回该已有元素的迭代器作为pair第一个参数,插入成功就返回新插入节点的迭代器。

2.指定迭代器位置的附近插入一个pair<key,val>类型的对象,可以认为传参的迭代器是你给编译器的一个提示,编译器会根据二叉搜索树的结构插入元素并返回新插入节点的迭代器。

3.插入一个迭代器区间指示的元素(pair<key,val>类型的对象):相当于将该区间涵盖的元素排序加去重存储到set中。

代码演示:

cpp 复制代码
	//插入一个pair<key,val>类型的对象
	map<string, int> index;
	index.insert({ "apple",1 });
	pair<string, int> p1 = { "peach",6 };
	index.insert(p1);
	index.insert(make_pair("orange", 3));
	for (auto it : index)
	{
		cout << it.first << ": " << it.second << endl;
	}
//apple: 1
//orange : 3
//peach : 6
	//指定迭代器位置的附近插入一个pair<key,val>类型的对象
	index.insert(index.begin(), { "banana",4 });
	for (auto it : index)
	{
		cout << it.first << ": " << it.second << endl;
	}
//apple: 1
//banana: 4
//orange : 3
//peach : 6
	index.clear();//容器清空
	index.insert({ {"apple",1},{"peach",4},{"orange",6} });
	for (auto it : index)
	{
		cout << it.first << ": " << it.second << endl;
	};
//apple: 1
//orange : 6
//peach : 4
	pair<string, int> p[] = { {"pineapple",1},{"watermelon",4},{"grape",6} };
	index.insert(p, p + 3);
	for (auto &it : index)
	{
		cout << it.first << ": " << it.second << endl;
	};
//apple: 1
//grape : 6
//orange : 6
//peach : 4
//pineapple : 1
//watermelon : 4

查找和计数

find

给定key值返回相应的迭代器位置,如果不存在就返回set的末尾元素的下一个位置的迭代器

count:

给定值判断这个值是否在set中,存在返回1,反之返回0;

lower_bound :传入一个值,返回中序遍历情况下第一个满足>=这个值节点的迭代器

upper_bound :传入一个值,返回中序遍历情况下第一个满足> 这个值节点的迭代器

equal_range:传入值,返回两个参数都是指示这个值的迭代器的pair(意义不大,主要是为了适配multimap)

删除erase

和set几乎一模一样:

1.按迭代器位置删除,搭配find使用

2.按值删除,返回删除元素的个数,1个表示删除成功,0表示set中无该元素

3.删除一段区间,通常搭配 lower_bound,upper_bound 使用

由于和set操作别无二异,因此不再做代码展示,触类旁通即可

三、operator

在pair<key,val>中key充当索引关键字的作用,在map中存储具有唯一性,可以根据key来查找val,就好比数组的下标一样。

所以实现【】的重载,可以和数组的【】作类比,得出它在map中的使用原理:

1.map中无key,调用

插入一个val为val的默认构造值的元素节点,返回值pair中第一个元素为当前元素的迭代器以方便实现复制和修改

2.map中有key,还是调用

返回值pair中第一个元素为当前元素的迭代器以方便实现复制和修改。

代码示例:

cpp 复制代码
	map<string, int> index;
	index.insert({ "apple",5 });
	index["apple"] = 13;
	index["peach"] = 23;
	index["banana"] = 15;
	for (auto e : index)
	{
		cout << e.first << ":" << e.second << endl;
	}
//apple:13
//banana : 15
//peach : 23

五、multimap相关接口

multimap是map的一对孪生兄弟,和map的区别就是它可以存储不同key值的元素

所以大部分接口都和map相同,这里介绍二者有不同之处的接口:

1.构造和区间插入

multiset的构造和区间插入不会删除key重复的值。

2.count

与set的count相比,当所给定的key值在multimap中时,multimap的count返回的是这个key值相同的所有元素的个数,不存在具有这个key值的元素时,返回值还是0.

3.find

与map相比,multimap之中find返回的所有迭代器都是中序遍历第一个出现那个节点所对应的迭代器,

4.erase

multimap的按值删除会删除所有key值为这个值的节点

5.equal_range 返回值为key的这段元素的迭代器区间(左闭右开)构成的pair

6.[] 运算符

  • map支持 [] 访问 / 修改
  • multimap不支持 [](因为 key 不唯一,不知道取哪个)

代码示例:

cpp 复制代码
#include <iostream>
#include <map>  // multimap 包含在这里
using namespace std;

void testMultimap()
{
	// 1. 创建 multimap(key 可重复)
	multimap<string, int> mm;

	// 2. 插入:允许 key 重复(multimap 最核心特点)
	mm.insert({ "apple", 1 });
	mm.insert({ "apple", 2 });  // 重复 key,插入成功!
	mm.insert({ "apple", 3 });
	mm.insert({ "banana", 10 });
	mm.insert({ "orange", 20 });

	cout << "=== 插入重复 key 后 ===" << endl;
	for (auto& e : mm) {
		cout << e.first << " : " << e.second << endl;
	}
	// 结果:apple 出现 3 次

	// 3. multimap 不支持 [] 操作!
	// mm["apple"];  // ❌ 报错!

	// 4. find:找到第一个匹配的 key
	cout << "\n=== find 查找 apple(返回第一个)===" << endl;
	auto pos = mm.find("apple");
	if (pos != mm.end()) {
		cout << pos->first << " : " << pos->second << endl;
	}

	// 5. 遍历所有相同 key 的元素(multimap 特有)
	cout << "\n=== 遍历所有 apple ===" << endl;
	while (pos != mm.end() && pos->first == "apple") {
		cout << pos->first << " : " << pos->second << endl;
		pos++;
	}

	// 6. count:统计 key 出现次数
	cout << "\napple 出现次数:" << mm.count("apple") << endl;

	// 7. erase(key):删除所有同 key 的元素
	mm.erase("apple");
	cout << "\n=== erase(\"apple\") 后 ===" << endl;
	for (auto& e : mm) {
		cout << e.first << " : " << e.second << endl;
	}

	// 8. 清空
	mm.clear();
}

int main()
{
	testMultimap();
	return 0;
}
相关推荐
流浪0011 小时前
C++篇:深入理解 C++ 智能指针:从裸指针到 RAII 的蜕变
开发语言·c++
QiLinkOS1 小时前
合肥气链科技有限公司创办与未来技术应用
c语言·数据结构·c++·人工智能·单片机·嵌入式硬件·算法
瑞雪兆丰年兮1 小时前
[从0开始学Java|第十六、十七天]项目阶段(拼图小游戏)
java·开发语言
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第85题】【Mysql篇】第15题:MySQL 的事务中,幻读是怎么解决的?
java·开发语言·数据库·mysql·面试
Solis程序员1 小时前
TreeMap 核心原理与实战
java·数据结构·算法
yaoxin5211231 小时前
423. Java 日期时间 API - DayOfWeek 和 Month 枚举
开发语言·python
Dlrb12111 小时前
数据结构-内核链表
linux·数据结构·链表·内核链表·inline·容器宏
秋雨梧桐叶落莳1 小时前
iOS——抽屉视图详解
开发语言·macos·ui·ios·objective-c·cocoa
郝学胜-神的一滴1 小时前
Qt 高级开发 016:半内存管理机制
开发语言·c++·qt·程序人生·用户界面