c++ map_set 特性

本文介绍了C++中的set和map容器,重点讲解了它们的插入、删除、查找以及区间操作,包括insert的不同方式,键值对的使用,如lower_bound,upper_bound,equal_range,以及map中[]操作符的应用。
摘要由CSDN通过智能技术生成

map 和 set 也是 c++ 中两个很重要的关联式容器,其底层结构都为搜索二叉树,set属于 k 模型, map 属于 kv 模型
序列式容器:vector/list/deque/… 单纯为了存储数据
关联式容器:map/set/… 数据之间存在某种关联关系(不只是存储数据)

1. set

还是在 cplusplus 中 set 的部分进行学习
在这里插入图片描述
这里我们主要针对和之前容器不相同的部分进行学习

1.1. insert

在这里插入图片描述
三种方式
第一种,插入一个值,放在树中合适位置
第二种,指定位置插入值,不建议使用,可能会出现问题
第三种,插入一段迭代器区间,特殊情况下使用
insert 还是按照之前学的容器写代码

#include<iostream>
#include<set>
using namespace std;

void test_set1()
{
	set<int> s;
    s.insert(5);
    s.insert(6);
    s.insert(2);
    s.insert(3);
    s.insert(1);
    s.insert(8);
    s.insert(4);

	set<int>::iterator it = s.begin();
	while(it != s.end())
	{
		cout << *it << "  ";
		++it;
	}
	cout << endl;
}

int main()
{
	test_set1();
	return 0;
}

在这里插入图片描述
在输入无序数据的情况下,最后输出的结果是升序的。
我们再试试一组数据

void test_set1()
{
	set<int> s;
	s.insert(1);
	s.insert(2);
	s.insert(1);
	s.insert(3);
	s.insert(1);
	s.insert(2);

	set<int>::iterator it =s.begin();
	while(it != s.end())
	{
		cout << *it << "  ";
	}
	cout << endl;
}

在这里插入图片描述
这组数据输入后,我们可以看见,同一个数据只插入了一次,这也是之前二叉搜索树的特性,一般情况下,同一个数据只能插入一个。
如果需要插入的位置已经有值,按照我们的理解,应该会返回一个 false,我们需要用 bool 接受
在这里插入图片描述
但是接受时这里就报错了
在这里插入图片描述
我们再看这里的返回值,有bool,但不止是 bool,返回的是一个 pair

1.2. 键值对

这里我们先简单了解一下用法,后面学习慢慢体会
在这里插入图片描述
pair是一个类,其 中分别含有 first, second 成员。
一般用于需要两个参数的地方。
在这里插入图片描述
SGU-STL 中关于键值对的定义

template<class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;

T1 first;
T2 second;
pair():first(T1()), second(T2())
{}
pair(const T1& a, const T2& b): first(a), second(b)
{}
};

这里我们的 bool 是作为 pair 中的第二个参数传入的,所以我们要这样写

pair<set<int>::iterator, bool> ret = s.insert(2);
cout << ret.second << endl;

在这里插入图片描述
我们直接输出 返回的pair类型中的第二个参数second即可,这里可以看见,插入失败,返回 false,输出0。
另一个参数是 set< int >::iterator 类型,返回的是个迭代器,如果插入成功,就会返回 插入位置的迭代器。
虽然这里在定义的时候写的只有 iterator

pair<iterator, bool> insert(const value_type& val);

因为这个函数是在类中实现的,直接写迭代器知道是哪个类的,但是在使用的时候要写清楚,是什么类型。
当然,偷懒的话可以写 auto

auto ret = s.insert(7);

但是这样写就不好分清楚第一模版参数和第二模版参数的类型,时间中不太建议用 auto。
set 支持范围for,只要支持迭代器就支持范围for

1.3. erase,find

在这里插入图片描述
erase 也支持3种传入方式

  1. 删除某个迭代器位置的值
  2. 删除某个值
  3. 删除一段迭代器区间
void test_set2()
{
	set<int> s;
	s.insert(1);
	s.insert(2);
	s.insert(6);
	s.insert(3);
	s.insert(5);
	s.insert(9);
	s.insert(8);
	s.insert(7);
	set<int>::iterator it1 = s.begin();
	while(it1 != s.end())
	{
		cout << *it << "  ";
		++it1;
	}
	cout << endl;
	
	set<int>::iterator find = s.find(3);
	s.erase(find);
	
	it1 = s.begin();
	while(it1 != s.end())
	{
		cout << *it1 << "  ";
		++it1;
	}
}

在这里插入图片描述
这里我们直接使用 find(),找到 3 这个位置的迭代器,然后对这个位置删除。
如果删除迭代器节点时,节点不存在会发生什么?
当我们删除 30 时
在这里插入图片描述
这里崩了
我们看看 find 有什么特性
在这里插入图片描述
这里我们可以看见,find 在未找到的情况下,会返回 end() , 直接删除这个位置肯定会有问题。
如果通过删除值进行删除

s.erase(30);

在这里插入图片描述
我们看到,没删也没崩。
在这里插入图片描述
这里 erase 返回的是一个 size_type 类型
在这里插入图片描述
在成员类型这里,我们可以看见,size_type 就是 size_t 类型。
前面是 void 类型,传入迭代器直接删除该位置,但是这里是 size_t 类型,会删除查找到的节点,至于这个返回类型有什么用,且看下回分解。

1.4. lower_bound && upper_bound

在这里插入图片描述在这里插入图片描述在这里插入图片描述
这两个函数的作用是取区间

void test_set3()
{
	std::set<int> myset;
	std::set<int>::iterator itlow, itup;

	for(int i = 1; i < 10; i ++)
	{
		myset.insert(i * 10);
	}
	itlow = myset.lower_bound(30);
	itup = myset.upper_bound(60);

	myset.erase(itlow, itup);

	std::cout << "myset contains:" ;
	for(std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
	{
		std::cout << ' ' << *it;
		std::cout << '\n';
	}
}

在这里插入图片描述
我们看到,这里输出结果是 10, 20。70, 80, 90。
lower_bound 返回的是 <= val 的迭代器位置
upper_bound 返回的是 > val 的迭代器位置
如果我们给 这两个函数传入不存在的值呢?

myset.lower_bound(25);

在这里插入图片描述
我们看到还是这个结果。
这里记住,返回的区间是 左闭右开。
up 返回的是开区间, low 返回的是闭区间

1.5. equal_range

在这里插入图片描述
这个函数返回的是一段区间,这个返回类型是由一个 pair 返回的。这个区间的特点是,区间内的都是相同数据

void test_set4()
{
	std::set<int> myset;
	for(int i = 1; i <= 5; i++)
	{
		myset.insert(i * 10);
	}
	std::pair<std::set<int>::const_iterator, std::set<int>::const_iterator> ret;
	ret = myset.equal_range(30);
	std::cout << "the lower bound points to:" << (*ret).first << '\n';
	std::cout << "the upper bound points to:" << (*ret).second << '\n';
}

在这里插入图片描述
注:这里也是 左闭右开 的范围
first 是 >= val 的值的迭代器
second 是 < val 的值的迭代器

1.6. count

在这里插入图片描述
count 也算比较熟的了,返回的是某个值的个数。
但是因为这里 set 每个值只能存在一个,所以 count() 只能是 1 或是 0
因此 count 可以用来判断某个值在不在 set 中

void test_set4()
{
	//...
	if(s.count(3))
	{
		cout << "3 在" << endl;
	}
	else
	{
		cout << "3 不在" << endl;
	}
}

比如这里,直接使用 count() 判断 3 在不在 set 内
这里还要注意,set 中的值是不支持修改的

set<int>::iterator it1 = s.begin();
while(it1 != s.end())
{
	*it1 = 20;
	//...
}

在这里插入图片描述
插入的值,是有 const 修饰的,我们不能直接修改。

2. multiset

上面我们说,set 这个搜索二叉树,不支持相同数据的插入,所以在 set 的头文件中,还有一个支持多个相同元素插入的 set----multiset
在这里插入图片描述
在这里插入图片描述
变异的搜索二叉树,允许多个相同的值进入同一棵树中,同时,它和上面的 set 大部分函数相同,不过有些函数用法不太相同。
这里我们插入相同的数据试试

void test_set1()
{
	multiset<int> s;
	s.insert(1);
	s.insert(2);
	s.insert(1);
	s.insert(4);
	s.insert(1);
	s.insert(1);
	s.insert(7);
	multiset<int>::iterator it = s.begin();
	while(it != s.end())
	{
		cout <<*it << "  ";
		it++;
	}
	cout << endl;
}

换成 multiset,这里就能存相同的数据,而且因为 multiset 和 set 函数基本一致,只需要稍微修改一下类型 multiset 就能使用 set 的代码。
这里节点的插入,节点值相同时,插入在相同节点的左边或者右边都可以。
在这里插入图片描述
如果都插入在右边,按理来说会形成这样的结构,虽然不合理,但是后面我们会学习 AVL树,红黑树等,会进行旋转改变结构,形成下面的结构
在这里插入图片描述
所以这里我们暂时理解,插左边或者右边是没有区别的。
如果我们再插入一个5
在这里插入图片描述
就会插入在这里,相同的数据不一定在一起,只要想通数据在中序在能一起遍历即可。

2.1. equal_range

set 中,它会返回一段相同数据的区间,但是因为 set 中每个数据只存在一个,所以这个函数 对set 没什么意义。
但是这里对 multiset 就不一样了,我们可以直接把相同数据所在的区间取出来,然后进行操作。
这里,我们使用的是 erase 来验证。

auto ret = s.equal_range(1);
s.erase(ret.first, ret.second);
it = s.begin();
while(it != begin())
{
	cout << *it << "  ";
	++it;
}
cout << endl;

在这里插入图片描述
这里 ret 的类型比较长,可能不好记,理解万岁

std::pair<std::multiset<int>::const_iterator, std::multiset<int>::const_iterator> ret;

这样就能通过获取迭代器区间来进行删除操作了

2.2. erase

其实这里,我们使用 equal_range 来 erase 意义并不大。

s.erase(1);

如果我们不按照区间删除,直接在 erase 传入 1
在这里插入图片描述
我们看到,这里也实现了把 1 全部删除的操作。
前面在 set 中学习 erase,我们直到,erase 的返回值是 size_t 类型,这个 size_t 的值在这里就有意义了

szie_t n = s.erase(1);
cout << n << endl;

在这里插入图片描述
这里的 返回值 n,就是删除数据的个数。

3. map

map 也有两个,一个是map,一个是 multimap
map 属于 kv 型的数据结构
之前我们解决 kv 型的问题需要自己先实现这个结构,但是这里我们直接使用 map 容器即可。
map 和 set 底层都是 搜索二叉树,所以大部分函数都类似。
在这里插入图片描述

3.1. insert

在这里插入图片描述
这里我们要注意,这里返回值是 pair 类型,传入的类型却只有一个,我们知道 kv 模型应该需要插入两个参数 key 和 value 的,但是这里为什么只有一个值?
在这里插入图片描述
这里我们可以看见, value_type 也是 pair 类型,pair 中的两个模版参数是 key_type 和 mapped_type。
如果我们想要插入数据,我们就要这样写

map<string, string> dict;
dict.insert(pair<string, string>("sort","排序"));

在这里插入图片描述
这里需要注意,传入的 key 是 const 修饰的,而 value 是没有 const 修饰的,这里一般要求只能修改 value, 不能对 key 进行修改。
但是在模版中,模版参数不同,类型就应该不同。

pair<string, string>;
pair<const key_value, mapped_type>;

两个类型不一样,但是为什么能通过呢?

2.2. map 中的键值对

在这里插入图片描述
pair 虽然东西不大,但是内容齐全,构造,析构,赋值运算符重载都有
在这里插入图片描述
这里的构造函数,有无参构造,带参构造,拷贝构造。
我们这里是直接调用 pair 的拷贝构造函数
string 能拷贝构造 const string 类型,所以这里能直接使用。
除了 string 去初始化 const string,只要是类型相近的,这里都可以使用

dict.insert(pair<const char*, const char*>("sort", "排序"));

只要是符合初始化 const string 类型的类型,都能使用。
除了在 insert 中使用匿名对象调用拷贝构造,我们还有其他方法
make_pair
在这里插入图片描述
这里,能直接返回一个 pair 的匿名对象。
这里还有个很方便的地方,这里的 make_pair 使用了函数模版,这里的好处是能自动推演类型

dict.insert(make_pair("left", "左"));

这里我们输入了 left, 左,make_pair 接受到参数后自动推演类型,就不需要我们自己去写传入类型了。

dict.insert<pair<const char*, const char*>("sort", "排序"));
dict.insert(pair<string,string>("sort,"排序");
dict.insert(make_pair("left","左"));

在这里插入图片描述
使用迭代器访问数据,可以这样写

map<string, string>::iterator it = dict.begin();
while(it != dict.end())
{
	cout << (*it).first << "  ";
	out << (*it).second << "  ";
	++it;
}

于是我们能输出下面的结果。
在这里插入图片描述
当 map 中的类型为自定义类型时,可以使用 -> 符号
显示调用

it.operator->()->first;

在这里插入图片描述
因为 map 可以使用迭代器,所以也支持范围 for

for(auto kv : dict)
{
	cout <<kv.first << "  " << kv.second;
}

但是这里要注意,这里最好使用引用,因为这里实现的 map 中的 pair 含有两个 string,如果直接拷贝过去,就会进行两次深拷贝,这样消耗太大。
所以这里最好传入引用,且在不修改值的情况下,最好传入 const

for(const auto& kv : dict)

但是如果不加 const,我们直接传入引用,那不就能对 树 中的值直接进行修改?

for(auto& kv : dict)
{
	kv.first += 'x';
	kv.second += 'x';
	cout << kv.first << "  " << kv.second;
}

在这里插入图片描述
但是这里修改 key 的时候,这里报错了。
因为 map 中的 pair 第一个参数本身就是 const 修饰过的,不能随意改变,而 second 本身没有const 修饰,是可以修改的。
key 是树中查找的线索,key被随意修改,树的结构很可能会被破坏。
但是 value 是无所谓的,一般不参与查找。

2.3. operator[]

首先我们想要实现 map 统计次数

void test_map2()
{
	string arr = { "苹果", "西瓜", "苹果", "西瓜", "苹果" , "西瓜", "苹果" , "西瓜", "苹果" ,"香蕉" };
	map<string, int> countMap;
	for(auto& str : arr)
	{
		auto ret = countMap.find(str);
		if(ret == countMaap.end())
		{
			countMap.insert(make_pair(str,1);
		}
		else
		{
			ret->second++;
		}
	}
}

这里我们先遍历 arr, 未找到相同的字符串则先插入字符串,找到字符串就对该位置的 value++;
但是这样玩太麻烦了,这里就要注意 operator[] 这个操作
在这里插入图片描述

void test_map2()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果" , "西瓜", "苹果" , "西瓜", "苹果" ,"香蕉" };
	map<string, int> countMap;
	for(auto& str, arr)
	{
		countMap[str]++;
	}
	for(auto& kv : countMap)
	{
		cout << kv.first << "  " << kv.second << endl;
	}
}

在这里插入图片描述
只需要这么一句代码就能结束战斗。
在这里插入图片描述
这里可能不好理解
我们一步一步剖析
在这里插入图片描述

make_pair(k, mapped_type())

这步是创建匿名对象

this->insert(   );

这步是往对象中插入数据,需要分情况
insert() 插入成功会返回 插入成功节点的 pair
插入失败会返回 重复值 位置的 pair。在这里插入图片描述
我们需要的是这里的 iterator,所以要指明,要 pair 中的第一个元素

(Pair).first;

此时的 iterator 就是插入/重复位置的迭代器,所以对迭代器解引用后,使用其中的 second
在这里插入图片描述
这里的 iterator 指向的就是 pair 类型。

*(iterator).second;

下面的函数可能好理解一点

V& operator[](const K& key)
{
	pair<iterator, bool> ret = insert(key, V());
	return ret.first->second;
}

学会使用 [] 的操作符,我们现在就能直接用了

void test_map1()
{
	map<string, string> dict;
    dict.insert(pair<const char*, const char*>("sort", "排序"));
    dict.insert(pair<string, string>("right", "右"));
    dict.insert(make_pair("left", "左"));

    dict["erase"];
    cout << dict["erase"] << endl;
    dict["erase"] = "删除";
    cout << dict["erase"] << endl;
    dict["test"] = "测试";
    dict["left"] = "左边,剩余";
    map<string, string>::iterator it = dict.begin();
    while(it != dict.end())
    {
    	cout << it.operator->()->first << "  ";
    	cout << (*it).second << endl;
    	++it;
    }
    for(auto& kv : dict)
    {
    	cout << kv.first << "  "  << kv.second;
    }
}

在这里插入图片描述

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
unordered_set是C++标准库中的一个容器类,它提供了一种存储唯一元素的无序集合。 unordered_set使用哈希表来实现元素的存储和查找,因此它具有快速的插入、删除和查找操作的特点。通过使用哈希函数,unordered_set可以将元素分布在不同的存储桶中,从而实现高效的查找。 在unordered_set中,每个元素被视为键值(key),并且它们是唯一的,不能重复。 unordered_set中的元素的顺序是不确定的,它们不按照插入的顺序进行存储。 引用提供了一些unordered_set的常用成员函数和迭代器的使用方法。可以使用begin()和end()函数来获取unordered_set的迭代器的起始位置和结束位置。cbegin()和cend()函数返回的是const迭代器,它们不允许修改unordered_set中的元素。local_iterator则用于遍历unordered_set中某个特定存储桶中的元素。 总结来说,unordered_set是一个存储唯一元素的无序集合的容器类,它使用哈希表来实现元素的存储和查找。在使用unordered_set时,可以利用迭代器来访问和操作其中的元素。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++常用语法——unordered_set](https://blog.csdn.net/LiuXF93/article/details/120899401)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C++ STL 之 unordered_set 使用(包括unordersd_map)](https://blog.csdn.net/qq_32172673/article/details/85160180)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值