【C++】map和set的使用

目录

[1. set(集合)](#1. set(集合))

[1.1 插入元素(insert)](#1.1 插入元素(insert))

[1.2 删除元素(erase)](#1.2 删除元素(erase))

[1.3 查找元素(find/count)](#1.3 查找元素(find/count))

[1.4 边界查找(lower_bound/upper_bound)](#1.4 边界查找(lower_bound/upper_bound))

[1.5 遍历元素](#1.5 遍历元素)

[2. multiset](#2. multiset)

[2.1 查找元素(find)](#2.1 查找元素(find))

[2.2 删除元素(erase)](#2.2 删除元素(erase))

[3. map(映射)](#3. map(映射))

[3.1 构造与初始化](#3.1 构造与初始化)

[3.2 插入键值对(insert)](#3.2 插入键值对(insert))

[3.3 访问与修改值( / at / 迭代器)](#3.3 访问与修改值([ ] / at / 迭代器))

[3.4 查找操作(find/count/边界函数)](#3.4 查找操作(find/count/边界函数))

[3.5 删除键值对(erase)](#3.5 删除键值对(erase))

[3.6 遍历元素](#3.6 遍历元素)

[4. multimap](#4. multimap)

[4.1 插入元素(insert)](#4.1 插入元素(insert))

[4.2 删除元素(erase)](#4.2 删除元素(erase))

[4.3 查找元素(find/equal_range)](#4.3 查找元素(find/equal_range))

[5. 练习](#5. 练习)


序列容器和关联容器

在 C++ 标准库中,关联容器是一类以键(key)为核心来存储和访问元素的容器,与序列容器(如 vector、list deque 等是线性存储,访问元素通过位置索引)不同,关联容器的元素存储和访问不依赖位置,而是通过键的特性(如排序规则或哈希值)来管理,因此支持高效的查找、插入和删除操作。
标准库中的关联容器主要分为两类:有序关联容器无序关联容器。有序的比如 map、set、multimap、multiset,它们基于红黑树,元素按键排序;无序的比如 unordered_map、unordered_set 等,基于哈希表,元素无序,但查找更快(平均 O (1))。

1. set(集合)

set 是一种存储唯一元素 的容器,并且元素是有序的(默认升序),且不允许有重复值,重复插入元素会被忽略。

1.1 插入元素(insert)

insert用于向set中添加元素,重复元素会被忽略。其返回值为pair<iterator,bool>:

  • 第一个成员(迭代器):指向插入的元素(或已存在的重复元素);
  • 第二个成员(bool):true表示插入成功,false表示元素已存在;
复制代码
int main() 
{
    set<int> s;

    // 插入单个元素
    auto ret1 = s.insert(3);
    cout << "插入3:" << (ret1.second ? "成功" : "失败") << endl; // 成功

    auto ret2 = s.insert(3);
    cout << "再次插入3:" << (ret2.second ? "成功" : "失败") << endl; // 失败(重复)

    // 插入多个元素
    s.insert({ 1, 2, 4 }); //插入初始化列表

    // 范围插入(从数组插入)
    int arr[] = { 5, 6 };
    s.insert(arr, arr + 2);

    // 遍历结果(自动排序):1 2 3 4 5 6
    for (int x : s) 
        cout << x << " ";

    cout << endl;
    return 0;
}

set的迭代器:全程只读

set 的元素不能修改,因为修改会破坏有序性,set的iterator本质是const_iterator,无论声明为iterator还是const_iterator,都不能修改元素。因为set的元素本身就是排序的"键",修改元素会直接破坏红黑树的有序结构,因此标准库强制迭代器为只读属性。

如需修改set元素,必须先erase旧元素,再insert新元素(相当于重新插入排序)。

1.2 删除元素(erase)

erase用于删除set中的元素,有 3 种常用形式,需注意删除后迭代器会失效:

  • iterator erase(const_iterator position); 删除迭代器pos指向的元素,返回下一个有效迭代器。
  • size_type erase(const value_type& val); 删除值为val的元素,返回删除的元素个数。
  • iterator erase(const_iterator first, const_iterator last); 删除[first, last)范围内的元素,返回下一个有效迭代器。
复制代码
int main()
{
	set<int> s = { 1, 2, 3, 4, 5 };

	size_t del_count = s.erase(3);
	cout << "删除3的个数:" << del_count << endl; // 1(删除成功)

	auto pos = s.find(4);
	if (pos != s.end()) 
	{
		pos = s.erase(pos); //删除4,并更新迭代器(指向5)
		cout << "删除后下一个元素:" << *pos << endl; // 5
	}

	return 0;
}

1.3 查找元素(find/count)

  • iterator find(const value_type& val); 查找值为val的元素,返回指向该元素的迭代器,若不存在,返回end()。
  • size_type count(const value_type& val) const; 返回值为val的元素个数(set中只能是0或1,可用于判断元素是否存在,但find更高效)。
复制代码
int main()
{
	set<int> s = { 10, 20, 30 };

	// 查找元素
	auto it = s.find(20);
	if (it != s.end()) 
	{
		cout << "找到元素:" << *it << endl; // 20
	}
	else 
	{
		cout << "未找到元素" << endl;
	}

	// 判断元素是否存在(count版本)
	if (s.count(30)) 
	{
		cout << "30存在" << endl; // 存在
	}
	if (s.count(40) == 0) 
	{
		cout << "40不存在" << endl; // 不存在
	}

	return 0;
}

int main()
{
	set<int> s = { 1, 2, 3, 4, 5 };

	auto pos1 = find(s.begin(), s.end(), 5); //算法库查找O(n)
	auto pos2 = s.find(5); //set自身实现的查找 O(logn)

	cout << *pos1 << " " << *pos2 << endl;
	return 0;
}

算法库的find,来自头文件<algorithm>头文件的通用算法,它只认识迭代器,不知道容器的底层实现,因此只能做"傻瓜式"的线性遍历。它从begin()到end()逐个比较元素,找到第一个与目标值相等的元素即返回迭代器,否则返回end(),时间复杂度为O(n)。

set自身的find成员函数,是为set的内部结构量身设计的,利用set底层红黑树特性进行查找,时间复杂度为O(logn)。

对set进行查找时,必须优先使用set::find,充分利用其O(logn)的高效特性。

1.4 边界查找(lower_bound/upper_bound)

  • lower_bound(val):返回第一个大于等于val位置的迭代器。
  • upper_bound(val):返回第一个大于val位置的迭代器。
  • equal_range(val):返回pair<lower_bound(val),upper_bound(val)>,表示与val相等的元素范围(set中最多一个元素)。
复制代码
int main()
{
	set<int> myset;
	for (int i = 1; i < 10; i++)
	{
		myset.insert(i * 10); //10 20 30 40 50 60 70 80 90 
	}

	//将25--55之间的值删除	

	//返回第一个 >=25位置的迭代器 30
	auto itlow = myset.lower_bound(25);
	//返回第一个 >55位置的迭代器 60
	auto itup = myset.upper_bound(55);

	//删除这段区间的值  传迭代器区间是左闭右开区间
	myset.erase(itlow, itup);

	for (auto e : myset)
	{
		cout << e << " ";
	}
	cout << endl;

    // equal_range:[70,80)
    auto range = myset.equal_range(70);
    cout << "equal_range的范围:";
    for (auto it = range.first; it != range.second; ++it) 
    {
	    cout << *it << " "; // 70
    }

	return 0;
}

1.5 遍历元素

复制代码
int main()
{
	set<int> s = { 3,1,2,4 }; //默认升序
    //set<int,greater<int>> s = { 3,1,2,4 }; //自定义比较器(降序)

	//1.迭代器遍历
	set<int>::iterator it = s.begin(); //auto it = s.begin();	
	while (it != s.end())
	{
		//*it = 1; 错误!C3892"it":不能给常量赋值
		cout << *it << " ";   //1 2 3 4
		++it;
	}
	cout << endl;

	//2.范围for遍历(底层依赖迭代器)
	for (auto e : s)
	{
		cout << e << " "; //1 2 3 4
	}
	cout << endl;
}

2. multiset

multiset是C++标准库中一种有序关联容器,与set类似,底层基于红黑树实现(保证元素有序),但核心区别是:运行存储重复元素。

插入、删除、查找的时间复杂度均为 O(log n)。

2.1 查找元素(find)

find(val):返回第一个值为val的元素的迭代器(即中序第一个),若不存在,返回end();

复制代码
int main()
{
	multiset<int> ms = { 1, 2, 2, 3, 3, 3 };

	//查找第一个3
	auto it = ms.find(3);
	if (it != ms.end()) 
	{
		cout << "第一个3的位置:" << *it << endl; // 3
	}

	//查找所有3的范围(equal_range)
	auto range = ms.equal_range(3);
	cout << "所有3的元素:";
	for (auto i = range.first; i != range.second; ++i) 
	{
		cout << *i << " "; // 3 3 3
	}
	cout << endl;

	return 0;
}

2.2 删除元素(erase)

erase(const T& val)会删除所有值为val的元素,返回删除的元素个数;

复制代码
int main()
{
	multiset<int> s = { 1, 2, 2, 3, 3, 3 };
	s.erase(3);//删除所有的3
	for (auto e : s)
	{
		cout << e << " "; //1 2 2
	}
	cout << endl;

	return 0;
}

3. map(映射)

在 C++ 标准库中,map 是一种有序关联容器 ,专门用于存储键值对(key-value) 数据。它的核心特点是通过 "键(key)" 快速索引 "值(value)",且键具有唯一性(不可重复)容器会自动按键的规则排序。底层通常基于红黑树(平衡二叉搜索树)实现, 因此插入、删除、查找操作的时间复杂度均为 O(log n)
键值对结构:每个元素是 pair<const Key, Value>类型(key是常量,不可修改,value可修改)。

3.1 构造与初始化

map支持多种初始化方式,最常用的包括:

复制代码
int main() 
{
	// 1. 默认构造
	map<string, int> m1;

	// 2. 初始化列表构造(C++11+)
	map<string, int> m2 = { {"apple", 5}, {"banana", 3}, {"orange", 2} };

	// 3. 拷贝构造
	map<string, int> m3(m2);

	// 4. 自定义比较器
	map<string, int, greater<string>> m4 = { {"a", 1}, {"b", 2} }; // 按key降序:"b" -> "a"

	return 0;
}

3.2 插入键值对(insert)

常见插入形式:

复制代码
int main()
{
	map<string, string> dict;

	// 方式1:显式构造pair对象
	pair<string, string> kv("first", "第一个");
	dict.insert(kv);

	// 方式2:直接构造临时pair对象,插入后临时对象销毁
	dict.insert(pair<string, string>("second", "第二个"));

	// 方式3:make_pair是C++标准库的一个模板函数,可以自动推导参数类型,返回一个对应的pair对象
	dict.insert(make_pair("sort", "排序"));

	// 方式4:初始化列表(C++11+,推荐)
	dict.insert({ "auto", "自动的" });

	return 0;
}

insert用于向map中插入键值对,仅当key,不存在时插入成功;若key已存在,则插入失败(不会覆盖原有value)。

函数原型:

复制代码
// 函数原型(简化)
pair<iterator, bool> insert(const pair<const Key, Value>& value);

参数:单个键值对(pair对象--> pair<const Key, Value>类型);

返回值:pair<iterator, bool>

iterator:若插入成功,指向插入的键值对,若插入失败,指向已存在的同key键值对;

bool:true表示插入成功,false表示插入失败(key已存在);

复制代码
int main() 
{
    map<string, int> m;

    // 插入新键值对(成功)
    auto res1 = m.insert(pair<string, int>("apple", 5));
    if (res1.second) 
    {
        cout << "插入成功:" << res1.first->first << " -> " << res1.first->second << endl; //apple -> 5
    }

    // 插入重复key(失败)
    auto res2 = m.insert(make_pair("apple", 10)); //make_pair构造pair
    if (!res2.second) 
    {
        cout << "插入失败(key已存在):" << res2.first->first << " 当前值:" << res2.first->second << endl; //apple 当前值:5(未被覆盖)
    }

    return 0;
}

3.3 访问与修改值( / at / 迭代器)

运算符的作用是通过key访问对应的value,mapkey的行为分为两种情况:

  1. 当key已存在时,返回key对应value的引用,可以直接读取或修改。
  2. 当key不存在时,会自动插入一个新的键值对 ,其中key为传入的参数,value为其类型的默认构造值(如int为0,string为空串,自定义类需有默认构造函数),返回这个新value的引用。
    注意: 的副作用,访问不存在的key时会自动插入,若仅需"查询"而不希望插入,应使用find或at。
复制代码
int main() 
{
	map<string, int> m = { {"apple", 5}, {"banana", 3} };

	// 访问已存在的key
	cout << m["apple"] << endl; // 输出:5

	// 修改已存在的value(通过引用)
	m["banana"] = 10;
	cout << m["banana"] << endl; // 输出:10

	// 访问不存在的key
	cout << m["orange"] << endl; // 输出:0(int的默认值)

	// 此时map中已自动插入键值对:{"orange", 0}
	cout << m.size() << endl; // 输出:3

	return 0;
}

最适合的场景是:需要便捷地访问并可能修改value,且允许自动插入新键值对。例如:统计频率。

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

	for (const auto& str : arr)
	{
		countMap[str]++;
	}

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

	return 0;
}

at(key):访问key对应的值。若key不在,会抛出out_of_range异常(更安全)。

迭代器:通过it->first访问key,it->second访问/修改value。

复制代码
int main()
{
	map<string, int> m = { {"apple", 5}, {"banana", 3} };

	// 用at访问(更安全)
	try 
	{
		cout << m.at("apple") << endl; // 5
		m.at("pear") = 2;  //抛出异常(pear不存在)
	}
	catch (const out_of_range& e) 
	{
		cout << e.what() << endl; //输出异常信息
	}

	//用迭代器修改value
	auto it = m.find("apple");
	if (it != m.end()) 
	{
		it->second = 20; // 修改value为20
		cout << it->second << endl; // 20
	}

	return 0;
}

3.4 查找操作(find/count/边界函数)

find(key):查找key,返回指向对应键值对的迭代器;若不存在,返回end()。

count(key):返回key出现的次数(map中只能是0或1,用于判断key是否存在)。

边界函数:lower_bound(key)(第一个>=key的键值对)、upper_bound(key)(第一个>key的键值对)equal_range(key)(返回 lower_bound,upper_bound 范围)。

复制代码
int main()
{
	map<int, string> m = { {1, "one"}, {3, "three"}, {5, "five"} };

	//find查找
	auto it = m.find(3);
	if (it != m.end()) 
	{
		cout << "找到:" << it->first << " -> " << it->second << endl; // 3 -> three
	}

	//count判断存在性
	if (m.count(5)) 
	{
		cout << "5存在" << endl;
	}
	if (m.count(2) == 0) 
	{
		cout << "2不存在" << endl; 
	}

	// 边界函数
	auto lb = m.lower_bound(3); // 第一个>=3的元素(3)
	cout << "lower_bound(3):" << lb->first << endl; // 3

	auto ub = m.upper_bound(3); // 第一个>3的元素(5)
	cout << "upper_bound(3):" << ub->first << endl; // 5

	auto range = m.equal_range(3); // [3,5)
	cout << "equal_range范围内的key:";
	for (auto i = range.first; i != range.second; ++i) 
	{
		cout << i->first << " "; // 3
	}
	return 0;
}

3.5 删除键值对(erase)

删除元素时,只有被删除元素的迭代器失效,其他迭代器有效。

  • erase(key):删除key对应的键值对,返回删除的个数(map中只能是0或1)。
  • erase(iterator pos):删除迭代器pos指向的键值对,返回下一个有效迭代器。
  • erase(iterator first, iterator last):删除first, last范围内的键值对,返回下一个有效迭代器。
复制代码
int main()
{
	map<string, int> m = { {"a", 1}, {"b", 2}, {"c", 3}, {"d", 4} };

	//按key删除
	int del_count = m.erase("b");
	cout << "删除b的个数:" << del_count << endl; // 1

	//按迭代器删除
	auto it = m.find("c");
	if (it != m.end()) 
	{
		it = m.erase(it); // 删除c,返回下一个迭代器(指向d)
		cout << "删除后下一个key:" << it->first << endl; // d
	}

	//范围删除(删除a和d)
	auto start = m.find("a");
	auto end = m.end();
	m.erase(start, end); // 删除[a, end)范围内的元素

	//此时m为空

	return 0;
}

3.6 遍历元素

复制代码
int main()
{
	map<string, string> dict;

	dict.insert({ "auto","自动的" }); 
	dict.insert({ "first", "第一个" });
	dict.insert({ "second", "第二个" });
	dict.insert({ "sort", "排序" });

	map<string, string>::iterator it = dict.begin();

	while (it != dict.end())
	{
		cout << it->first << ":" << it->second << endl;
		++it;
	}
	cout << endl;

	return 0;
}

4. multimap

multimap与map类似,用于存储键值对,并按key自动排序,核心区别是:允许键值冗余。
运算符:由于key不唯一,无法通过 确定访问哪个value,因此multimap没有 运算符。

高效操作:底层基于红黑树实现,插入、删除、查找的时间复杂度均为 O(log n)(n为元素总数)。

4.1 插入元素(insert)

multimap的insert总是成功(因为允许重复key),返回值为指向新插入元素的迭代器。

复制代码
int main() 
{
    multimap<string, string> mm; 

    // 插入相同key的多个键值对
    mm.insert({ "数学", "高数1" });
    mm.insert({ "数学", "高数2" });
    mm.insert(make_pair("数学", "线性代数"));
    mm.insert(pair<string, string>("数学", "概率论"));

    mm.insert({ "计算机", "C++编程" });
    mm.insert({ "计算机", "数据结构" });

    // 遍历(按key升序,相同key相邻)
    for (auto& p : mm) 
    {
        cout << p.first << ":" << p.second << endl;
    }

    return 0;
}

4.2 删除元素(erase)

erase(const Key& key):删除所有key对应的元素,返回删除的元素个数;

复制代码
int main()
{
	multimap<string, int> m = { {"a", 1}, {"a", 2}, {"a", 3}, {"b", 4},{"c", 5} };

	//按key删除
	int del_count = m.erase("a");
	cout << del_count << endl; //3

	return 0;
}

4.3 查找元素(find/equal_range)

find(key):返回第一个key匹配的元素的迭代器(中序第一个),若不存在,返回end()。

equal_range(key):返回pair<iterator, iterator>,表示所有key匹配的元素范围([first, second))。

复制代码
int main()
{
	multimap<string, int> m = { {"a", 1}, {"a", 2}, {"a", 3}, {"b", 4},{"c", 5} };

	auto it = m.find("a");//查找第一个a
	if (it != m.end()) 
	{
		cout << it->second << endl; //1
	}
	cout << endl;

	auto range = m.equal_range("a");
	for (auto i = range.first; i != range.second; ++i) 
	{
		cout << i->second << endl; //1 2 3
	}

	return 0;
}

5. 练习

【两个数组的交集】

【环形链表II】

【随机链表的复制】

【前k个高频单词】

相关推荐
玖玥拾9 分钟前
C/C++ 数据结构(一)基础概念、线性表链表
c语言·数据结构·c++·链表
星恒随风10 分钟前
C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念
开发语言·c++·笔记·学习
郝学胜-神的一滴13 分钟前
Qt 高级开发 031:QListWidget图标布局实战
开发语言·c++·qt·程序人生·软件构建·用户界面
QiLinkOS17 分钟前
极客精神与商业思维的融合实践(3)
c语言·c++·人工智能·算法·开源协议
牛油果子哥q17 分钟前
队列(Queue)深度精讲,先进先出原理、顺序/链式/循环队列、STL queue底层、栈队列互模拟与面试考点全解
开发语言·c++·面试
暖阳华笺22 分钟前
【数据结构与算法】哈希专题
数据结构·c++·算法·leetcode·哈希算法
大白话_NOI28 分钟前
【洛谷 P1024 】[NOIP2001 提高组] 一元三次方程求解 - 详细分析与C++实现
c++·算法
随意起个昵称29 分钟前
区间dp-进阶题目1(进阶合并)
c++·算法·动态规划
王老师青少年编程30 分钟前
2022年CSP-X复赛真题及题解(T2:移动棋子)
c++·真题·csp·信奥赛·复赛·csp-x·移动棋子
玖玥拾30 分钟前
C/C++ 数据结构(三)链表核心算法
c语言·数据结构·c++·链表