C++ map和set

前言:本文中会对mapsetmultisetmultimap的常用接口进行介绍,对于mapoperator[]运算符重载的原理进行了讲解,其中mapset介绍较为详细,对关联式容器,键值对的概念及使用也做了介绍.

🌿1. 关联式容器

根据"数据在容器中的排列"特性,STL容器可分为序列式(sequence)和关联式(associative)两种,比如vectorlistdequeforward_listC++11)等,这些容器统称为序列式容器,因为其底层为线性的数据结构,里面存储的是元素本身.那什么是关联式容器,它与序列式容器有什么区别?

关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key,value>的键值对,在数据检索时比序列式容器效率更高.

🥦2. 键值对

键值对pair)是用来表示具有一一对应关系的一种结构,该结构中一般只****包含两个成员变量keyvalue,key代表键值,value表示与key对应的信息.

比如:我们现在要建立一个英汉互译的字典,那该字典中就必须有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该单词,在字典中就能找到与其对应的中文含义.

这是SGI-STL中对于pair的定义

在这里插入图片描述
键值对的使用:

void Test()
{
	pair<string, string> pr("moon", "月亮");
    
    //first表示key值,second表示value
	cout << pr.first << ":" << pr.second << endl;
}

🌳3. 树形结构的关联式容器

根据应用场景的不同,STL总共实现了两种不同结构的关联式容器:树型结构哈希结构树型结构的关联式容器主要有四种mapsetmultimapmultiset,其中multimapmultiset是根据mapset衍生出来的.
这四种容器共同的特点是:都使用红黑树作为其底层结构.

此外,SGI STL还提供了一个不在标准规格之列的哈希结构的关联式容器:hash_table(散列表),以及以此hash table为底层实现机制而完成的hash_set(散列集合)、hash_map(散列映射表)、hash_multiset(散列多键集合)、hash_multimap(散列多键映射表).

🍁4. set

📗4.1 set的介绍

首先来看一下set的一些介绍及特性:

  1. set是按照一定次序存储元素的容器,所有元素都会根据元素的键值自动被排序
  2. set的元素不像map那样可以同时拥有实值(key)和键值(value),set元素的键值就是实值,实值就是键值,并且set不允许两个元素有相同的键值.
  3. set中的元素不能修改,但可以进行插入或删除.
  4. set的底层是用红黑树实现的
  5. set拥有与list相同的某些性质:当客户端对它进行元素新增操作(insert)或删除(erase)操作时,操作之前的所有迭代器,在操作完成后都依然有效.

那么为什么set中的元素不允许修改呢?
我们先来看一看SGI STL源码中的定义:

typedef typename rep_type::const_iterator iterator;

这里的rep_type是这样定义的:

typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;

其实rep_type也就是红黑树,这样也就验证了底层是红黑树的说法,那么其实,set所开放的各种操作接口,RB-tree也都提供了,所以几乎所有的set操作行为,都只是在转调RB-tree的接口而已.

对于set中的元素能不能修改,我们来看看set的迭代器:

typedef typename rep_type::const_iterator iterator;

也就是说,set的迭代器其实是复用了RB-treeconst_iterator,所以set中不能修改元素值.

  1. map/multimap不同,map/multimap中存储的是真正的键值对<key, value>set中只放value,但在底层实际存放的是由<value, value>构成的键值对。
  2. set中插入元素时,只需要插入value即可,不需要构造键值对。
  3. set中的元素不可以重复(因此可以使用set进行去重)。
  4. 使用set的迭代器遍历set中的元素,可以得到有序序列
  5. set中的元素默认按照小于来比较
  6. set中查找某个元素,时间复杂度为:logN
  7. set中的元素不允许修改
  8. set中的底层使用二叉搜索树(红黑树)来实现

📗4.2 set的使用

📖4.2.1 set的模板参数列表

在这里插入图片描述
T: set中存放元素的类型,实际在底层存储<value, value>的键值对。

Compareset中元素默认按照小于来比较

Allocset中元素空间的管理方式,使用STL提供的空间配置器管理

📖4.2.2 set的构造

在这里插入图片描述

set (const Compare& comp = Compare(), const Allocator& =Allocator() );

默认构造(空构造):构造一个空的set

注意:这里的key_compareallocator_type是因为在STL源码中进行了typedef.

在这里插入图片描述

set (InputIterator first, InputIterator last, const Compare&comp = Compare(), const Allocator& = Allocator() );

用迭代器区间[first,last)进行构造.

set ( const set<Key,Compare,Allocator>& x);

set的拷贝构造.

📖4.2.3 set的迭代器

在这里插入图片描述
set的迭代器与其他容器的迭代器种类类似,有正向迭代器反向迭代器const迭代器,这里不做太多介绍,下面会写代码演示.

📖4.2.4 set的容量

在这里插入图片描述

set::empty:判断set是否为空,是则返回true,反之返回false.

在这里插入图片描述
set::size:返回set中有效元素的个数

在这里插入图片描述
set::max_size:返回set中能容纳最大元素的个数

📖4.2.5 set的修改操作

在这里插入图片描述
这里主要介绍inserteraseclearswap操作比较简单,读者可自行了解.

set::insert

在这里插入图片描述

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

功能:在set中插入元素val,实际插入的是<val, val>构成的键值对,如果插入成功,返回<该元素在set中的位置,true>,如果插入失败,说明xset中已经存在,返回<xset中的位置,false>

iterator insert (iterator position, const value_type& val);

功能:在position位置插入val,返回新插入元素的位置.

void insert (InputIterator first, InputIterator last);

功能:插入迭代器区间[first,last)的值.

set::erase
在这里插入图片描述

void erase (iterator position);

功能:删除setposition位置上的元素

size_type erase (const value_type& val);

功能:删除set中值为val的元素,返回删除的元素的个数

void erase (iterator first, iterator last);

功能:删除set[first, last)区间中的元素

📖4.2.6 set的其他操作

在这里插入图片描述
这里主要介绍findcountlower_boundupper_bound这四个接口.

set::find

iterator find (const value_type& val) const;

功能:在set中查找val,如果找到,返回val所在的位置,反之返回end()

set::count

size_type count (const value_type& val) const;

功能:返回set中值为val的元素的个数,这里要特别强调的是,这个函数的返回值只有两个值:01,因为在set中元素不允许重复,所以要么valset中,且只有一个,要么不在,为0个.

所以在set中,count方法也可以用来判断元素是否存在.

int main()
{
	set<int> s;
    
    //如果存在返回1,不存在返回0
	if (s.count(1))
	{
		cout << "1存在" << endl;
	}
	{
		cout << "1不在" << endl;
	}

	return 0;
}

set::lower_bound

iterator lower_bound (const value_type& val) const;

功能:lower_bound会返回>= val的最小值.

set::upper_bound

功能:upper_bound会返回set> val的最小值

例如,对于这样一个0,1,2,3,4,5,6

如果调用s.lower_bound(3),就会返回3
如果调用s.upper_bound(3),就会返回4

使用这两个函数,我们可以实现一个删除[x,y],闭区间的值.

void Test()
{
	set<int> s;
	s.insert(5);
	s.insert(0);
	s.insert(3);
	s.insert(1);
	s.insert(4);
	s.insert(6);
	s.insert(2);
	s.insert(7);
	
	//删除[x,y]区域的值
	int x, y;
	cin >> x >> y;
	auto left = s.lower_bound(x);
	auto right = s.upper_bound(y);
    
    //由于erase方法删除的是[first,end)前闭后开,而upper_bound方法正好返回比y大的那个值,
    //保证了将[x,y]区间内的值删掉
	s.erase(left, right);
}

然后,对于上述set的接口,我们来测试一下:

void Test01()
{
	set<int> s;
	s.insert(5);
	s.insert(0);
	s.insert(3);
	s.insert(1);
	s.insert(4);
	s.insert(6);
	s.insert(2);
	s.insert(7);
	s.insert(2);   //在这里我们重复插入了一个2,但输出时,set中只有一个2,验证set具有去重功能.
	
    cout << "删除前:" << endl;
    //用范围for遍历set -> 遍历结果有序
	for (const auto& e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	cout << "共插入"<<s.size()<<"个元素" << endl;

	if (!s.empty())
	{
		cout << "set不为空" << endl;
	}
	else
	{
		cout << "set为空" << endl;
	}

	set<int>::iterator pos = s.find(3);

	if (pos != s.end())
	{
		s.erase(pos);
	}
    
    cout<<"删除后:"<<endl;
    //用迭代器遍历set
	set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		//set不允许修改key
		//*it = 10; -> 报错
		cout << *it << " ";
		++it;
	}
	cout << endl;

}

运行结果如下:
在这里插入图片描述

🍀5. map

📙5.1 map的介绍

在这里插入图片描述
这是文档中对于map的介绍,其中主要介绍这么几点:

  1. map是关联式容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
  2. map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,keyvalue通过成员类型value_type绑定在一起,为其取别名称为pair: typedef pair value_type;
  3. 在内部,map中的元素总是按照键值key进行比较排序的。
  4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)
  5. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value
  6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))

注意:在map中,键值key通常用于排序和惟一地标识元素,所以在进行insertfinderase等这些操作时,操作对象一般都是key.

📙5.2 map的使用

📖5.2.1 map的模板参数

在这里插入图片描述
Key: 键值对key的类型

T键值对value的类型

Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况
下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则
(一般情况下按照函数指针或者仿函数来传递)

Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器
注意:在使用map时,需要包含头文件<map>

📖5.2.2 map的构造

在这里插入图片描述
默认构造(空构造):

explicit map (const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());

其中的key_compareallocator_type为比较器和空间配置器,不需要我们自己传递.
功能:返回一个空的map

迭代器区间构造:

map (InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());

拷贝构造:

map (const map& x);

这两种构造与上面介绍的set类似,不再细说.

📖5.2.3 map的迭代器

在这里插入图片描述

📖5.2.4 map的元素与容量访问

在这里插入图片描述
这三个接口与上面set类似,可以看set中的叙述.

📖5.2.5 详解map的operator[]运算符重载

这里着重来看这样一个接口:

在这里插入图片描述
也就是map中重载了[]运算符,那么这个运算符如何使用呢?

假如,我们现在有一些水果,需要统计各个水果的个数,就可以使用这样一段代码:

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

int main()
{
	string str[] = { "香蕉", "苹果", "梨", "西瓜", "葡萄", "苹果", "西瓜", "香蕉", "香蕉" };
	map<string, int> countMap;

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

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


	return 0;
}

运行结果:

在这里插入图片描述
我们会发现,我们只使用了countMap[e]++这样一个操作就完成了这个需求,那为什么它可以这样做呢?

在这里插入图片描述
这条语句便是operator[]的实现,但这句代码非常不直观,所以我们将它分解一下:

在这里插入图片描述
key_type表示的是map中键值对<key,value>key的类型,mapped_type表示的是map中键值对<key,value>value的类型.
所以这个函数的功能是,如果传入的kmap中还不存在,那就在map中插入以这个k值为key构造的键值对pair,然后返回这个键值对的value值,如果已经存在,返回的是已经存在的以k为键值的pair的位置.

所以对于countMap[e]++;:如果key存在,就++value,如果不存在,创建key然后++value.
如果只写成countMap[e],那就只插入,不修改.

📖5.2.6 map的修改操作

在这里插入图片描述
map的修改操作接口与set基本没有差别,唯一区别就是map插入的是pair键值对,所以这里对于其他接口不再细说,只说一下map的插入:

在这里插入图片描述

对于map的插入操作,首先,我们需要构建一个pair键值对,然后插入到map中,那么构造键值对除了我们在上面讲的方法之外,还有一种是使用pair中的一个方法:

在这里插入图片描述
由于函数模板可以进行类型自动推演,所以我们使用这个函数,就不用再显式的写出参数,只需要写我们要插入的值即可:

具体用法如下:

//创建一个map
map<string, string> dict;

//使用先构建pair对象,然后插入
dict.insert(pair<string, string>("moon","月亮"));

//使用make_pair函数
dict.insert(make_pair("moon", "月亮"));

map的一些其他接口:

在这里插入图片描述
这些接口也参考set中的讲解.

map的测试:

void Test02()
{
	map<string, string> dict;

	dict.insert(make_pair("moon", "月亮"));
	dict.insert(make_pair("color", "颜色"));
	dict.insert(make_pair("bottle", "瓶子"));
	dict.insert(make_pair("paper", "纸"));
	dict.insert(make_pair("seawater", "海水"));
	dict.insert(make_pair("plank", "木板"));

	string str;   
	
	while (cin >> str)
	{
		map<string, string>::iterator it = dict.find(str);
		if (it != dict.end())
		{
			cout <<it->first<<":"<< it->second << endl;
		}
		else
		{
			cout << "未查询到此单词" << endl;
		}
	}
    
    cout<<"删除前: "<<endl;
	for (const auto& e : dict)
	{
		cout << e.first << ":" << e.second << endl;
	}
    
    //删除key为"moon"的元素
    dict.erase("moon");
    cout<<"删除后: "<<endl;
    for (const auto& e : dict)
	{
		cout << e.first << ":" << e.second << endl;
	}
}

运行结果:
在这里插入图片描述
同时可以观察到,map打印的结果是按照key排序的

🍃6. multiset

对于multiset的介绍,由于它与set太过类似,所以这里只是简单说下它们之间的区别.
在这里插入图片描述
对于multiset,它是对set可以插入重复元素后的衍生版本,所以它与set的唯一区别就是它允许插入重复值,也就是说它只能排序不能去重.

在这里插入图片描述
还有对于count这个接口,在set中,它只会返回01,但在multiset中,允许插入重复元素,所以它的返回值就要根据具体的元素个数而定.

在这里插入图片描述
对于erase这个接口,在set中调用,会将set中存在的唯一的那个value删掉,但如果在multiset中调用,它会删除所有的val值.

对于其他的接口以及性质,multisetset并无区别,大家可以参考set中的去使用.
multiset的底层也是通过红黑树实现的.

注意:使用multiset时不需要重新包含头文件,包含头文件set即可

简单测试multiset

void Test()
{
	int arr[] = { 0, 0, 0, 1, 2, 3, 4, 5 };
	multiset<int> s;

	for (auto& e : arr)
	{
		s.insert(e);
	}

	for (auto& t : s)
	{
		cout << t << " ";
	}
	cout << endl;
}

在这里插入图片描述

🌴7. multimap

对于multimap的介绍,由于它与map太过类似,所以这里只是简单说下它们之间的区别.
在这里插入图片描述
multimapmap的唯一不同就是:map中的key是唯一的,而multimapkey是可以重复的.

multimap中,也有各别接口稍有差别,参考multiset中的描述.

这里只有一点:在multimap中,没有重载operator[],为什么?

我们刚才在map说过,operator[]的功能是:如果key不存在,就用key创建pair并插入,如果存在,就返回当前已经存在的元素位置, 可是在multimap中,是允许插入重复key的,如果已经存在多个key,然后再次插入相同的key值,那应该返回哪一个呢?所以这个接口也不能再对单个元素实现计数功能,也就显得没有意义.

对于其他接口的使用,参考map中的讲解.

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉默.@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值