set、map、multiset、multimap的介绍及使用以及区别,注意事项

目录

关联式容器

键值对

set

set的介绍

set的构造函数与其定义

 set的常用的成员函数

 erase

lower_bound  

upper_bound

equal_range

multiset

find

count

erase

 map

map的介绍

 set的构造函数与其定义

insert

返回值

参数列表(map)

find

返回值

erase

map的[ ]运算符重载

迭代器

map的其他成员函数

multimap


关联式容器

在介绍set、map、multiset、multimap之前我们先了解什么是关联式容器,

C++STL包含了序列式容器关联式容器

序列式容器:序列容器以线性序列的方式存储元素。它没有对元素进行排序,元素的顺序和存储它们的顺序相同。一般来说,有 5 种标准的序列容器,每种容器都具有不同的特性。

  • array<T,N> (数组容器) :是一个长度固定的序列,有N个T类型的对象,不能增加或删除元素。
  • vector<T> (向量容器) :
    是一个长度可变的序列,用来存放T类型的对象。必要时,可以自动增加容量,但只能在序列的末尾高效地增加或删除元素。
  • deque<T> (双向队列容器) :
    是一个长度可变的、可以自动增长的序列,在序列的两端都不能高效地增加或删除元素。
  • list<T> (链表容器) :
    是一个长度可变的、由 T 类型对象组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素。访问容器中任意元素的速度要比前三种容器慢,这是因为
  • list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
  • forward list<T> (正向链表容器) :是一个长度可变的、由 T 类型对象组成的序列,它以单链表的形式组织元素,是一类比链表容器快、更节省内存的容器,但是它内部的元素只能从第一个元素开始访问。

注意,其实除此之外,stack<T> 和 queue<T> 本质上也属于序列容器,只不过它们都是在 deque 容器的基础上改头换面而成,通常更习惯称它们为容器适配器, 

 关联式容器: 里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。比如:set、map、unordered_set、unordered_map等。

  • 关联式容器存储的元素,都是一个一个的“键值对”( <key,value> ),这是和序列式容器最大的不同。除此之外,序列式容器中存储的元素默认都是未经过排序的,而使用关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序。

推荐文章:

C++ STL关联式容器(详解)-CSDN博客



 下面开始这篇的讲解set、map、multiset、multimap。

如果查阅相关资料可以得知,set、map、multiset、multimap的底层是用红黑树来实现的,关于红黑树是什么,后面会单独写一篇文章进行详细讲解,在这里只需要其底层是红黑树即可(红黑树是比二叉搜索树更有的树)。

键值对

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

比如说我们去买菜,每种菜都对应着不同的单价,这就是一一对应关系,可以通过其菜名知道其多少钱一斤。

在SGI-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)
	{}
};

set

set的介绍

  1. set是按照一定次序存储元素的容器,使用set的迭代器遍历set中的元素,可以得到有序序列。

  2. set当中存储元素的value都是唯一的,不可以重复,因此可以使用set进行去重。

  3. set默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。

  4. set中的元素不能被修改,因为set在底层是用二叉搜索树来实现的,若是对二叉搜索树当中某个结点的值进行了修改,那么这棵树将不再是二叉搜索树。

  5. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对,因此在set容器中插入元素时,只需要插入value即可,不需要构造键值对。

  6. set在底层是用红黑树实现的,所以在set当中查找某个元素的时间复杂度为    logN

set的构造函数与其定义

这里就不给官方的函数声明截图了,直接展示使用案例

bool fncomp(int lhs, int rhs) 
{ 
	return lhs < rhs;
}
struct classcomp 
{
	bool operator() (const int& lhs, const int& rhs) const
	{
		return lhs < rhs;
	}
};
int main()
{
	set<int> first;                           // 最基本的定义,其里面为空
	int myints[] = { 10,20,30,40,50 };
	set<int> second(myints, myints + 5);        // 利用数组定义

	set<int> third(second);                  // 拷贝构造

	set<int> fourth(second.begin(), second.end());  // 使用迭代器构造

	set<int, classcomp> fifth;                 // 使用仿函数定义set是升序还是降序
	                                           // 小于是升序,大于是降序
	bool(*fn_pt)(int, int) = fncomp;
	set<int, bool(*)(int, int)> sixth(fn_pt);  // 利用函数指针的方式定义是升序还是降序

	return 0;
}

 set的常用的成员函数

set当中常用的成员函数如下:

因为set与map的函数设计底层全用的一套模板,所以各个函数会在map中详细解释其返回值与别的细节。

其实底层set与map全是用一个模板进行封装而得

成员函数功能
insert插入指定元素
erase删除指定元素
find查找指定元素//返回的是迭代器,没有找到返回end()
size获取容器中元素的个数
empty判断容器是否为空//是空返回真
clear清空容器
swap交换两个容器中的数据
count获取容器中指定元素值的元素个数
 erase

最常用的也就是第二种,第一种的传参可以传find的返回值。第三种是删除某个特定的区间,但着个通常还是与别的函数一起使用。比如下面这三个函数


lower_bound  

upper_bound

equal_range

然而set函数还有几个成员函数是我们没有见过的几个函数

因为set是有序的,所以这三个函数就达到了返回大于/小于/等于某个值的部分;

就比如一个set函数内存储着  1,2,6,8,9,11;

如果想返回比6小的部分,就可以用 lower_bound,解释起来很简单,但是里面还是有很多的细节的,下面用代码来解释细节部分。

首先前两个函数的返回值为迭代器。其使用大多是与erase同用

int main()
{
	set<int> myset;
	set<int>::iterator itlow, itup;

	for (int i = 1; i < 10; i++) myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90


	//itlow = myset.lower_bound(30);              //
	itlow = myset.lower_bound(25);                // >= val值位置的iterator 30
	itup = myset.upper_bound(70);                 // >  val值位置的iterator 80

	// 删除区间:[30,80)
	myset.erase(itlow, itup);                     // 10 20 80 90

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


    //运行结果:
    //10 20 80 90
	return 0;
}

第三个成员函数的返回值略有不同

pair的first与second所接收的是两个不同的迭代器,但也有可能相同

  • first接收>=val的迭代器
  • second接收>val的迭代器
int main()
{
	set<int> myset;
	set<int>::iterator itlow, itup;

	for (int i = 1; i <= 5; i++) myset.insert(i * 10);   // myset: 10 20 30 40 50


	auto ret = myset.equal_range(35);

	cout << "the lower bound points to: " << *ret.first << '\n';   // >= val
	cout << "the upper bound points to: " << *ret.second << '\n';  // > val

	//运行结果:
	//  the lower bound points to : 40
	//	the upper bound points to : 40
	return 0;
}

multiset

multiset容器与set容器的底层实现一样,都是红黑树,其次,multiset容器和set容器所提供的成员函数的接口都是基本一致的,但set是有序,并且没有重复的,那么与之对应的就是multiset无序,并且有重复的。用官方的话来说就是multiset允许键值冗余,即multiset容器当中存储的元素是可以重复的。

用法与set几乎完全相同,这里不在举例说明。

但还是有很多细节要注意的,就比如说因为multiset是无序存在重复元素的,那么find如果要查找重复的元素找到了要返回哪一个呢?

因此两个容器中成员函数find和count的意义也有所不同:

find

成员函数find功能
set对象返回值为val的元素的迭代器
multiset对象返回底层搜索树中序的第一个值为val的元素的迭代器

count

成员函数count功能
set对象值为val的元素存在则返回1,不存在则返回0(find成员函数可代替)
multiset对象返回值为val的元素个数(find成员函数不可代替)

erase

对于第二个同样的erase也会删除多个,然后返回值删除的个数。

count同样是如此,找在不在,并且返回再次对象中有几个这样的值

 map

map的介绍

  1. 底层同样是用红黑树来写的。

    所以在map当中查找某个元素的时间复杂度为logN               

  2. map是关联式容器,它按照特定的次序(按照key来比较)存储键值key和值value组成的元素,使用map的迭代器遍历map中的元素,可以得到有序序列。

  3. 在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,并取别名为pair。

  4. 在map中的键值key不能被修改,但是元素的值value可以被修改,因为map底层的二叉搜索树是根据每个元素的键值key进行构建的,而不是值value。

  5. 与set相同的是其默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。

  6. map容器支持下标访问符,即在[]中放入key,就可以找到与key对应的value。

 set的构造函数与其定义

这里就不给官方的函数声明截图了,直接展示使用案例

bool fncomp(char lhs, char rhs) { return lhs < rhs; }

struct classcomp {
	bool operator() (const char& lhs, const char& rhs) const
	{
		return lhs < rhs;
	}
};

int main()
{
	map<char, int> first;// 空,也是最简单的定义

	first['a'] = 10;
	first['b'] = 30;
	first['c'] = 50;
	first['d'] = 70;

	map<char, int> second(first.begin(), first.end());// 用迭代器,使用一个区间定义

	map<char, int> third(second); // 拷贝构造定义

	map<char, int, classcomp> fourth;                  // 使用仿函数定义set是升序还是降序
	                                                   // 小于是升序,大于是降序

	bool(*fn_pt)(char, char) = fncomp;
	map<char, int, bool(*)(char, char)> fifth(fn_pt); // 利用函数指针的方式定义是升序还是降序

	return 0;
}

以下的知识在set中同样适用,只需要进行转化为set的特点即可。 

insert

返回值

用的最多的也就是这个了。其返回值的pair是返回的迭代器,其产生的效果与set相似,如果插入成功就返回这个位置的迭代器,反之如果这个数已经存在,那么也是返回这个数位置的迭代器。

pair的second为一个bool类型。插入成功返回true,失败为false。 

参数列表(map)

insert函数的参数显示是value_type类型的,实际上value_type就是pair类型的别名:

typedef pair<const Key, T> value_type;

 因此,我们向map容器插入元素时,需要用key和value构造一个pair对象,然后再将pair对象作为参数传入insert函数。

 方式一: 构造匿名对象插入。

int main()
{
	map<int, string> m;
	
	m.insert(pair<int, string>(2, "two"));
	m.insert(pair<int, string>(1, "one"));
	m.insert(pair<int, string>(3, "three"));
	for (auto e : m)
	{
		cout << "<" << e.first << "," << e.second << ">" << " ";
	}
	cout << endl; //<1,one> <2,two> <3,three>
	return 0;
}

但是这种方式会使得我们的代码变得很长,尤其是没有直接展开命名空间的情况下,因此我们最常用的是方式二。

方式二: 调用make_pair函数模板插入。
在库当中提供以下make_pair函数模板

template <class T1, class T2>
pair<T1, T2> make_pair(T1 x, T2 y)
{
	return (pair<T1, T2>(x, y));
}

我们只需向make_pair函数传入key和value,该函数模板会根据传入参数类型进行自动隐式推导,最终构造并返回一个对应的pair对象。

int main()
{
	map<int, string> m;
	//方式二:调用函数模板make_pair,构造对象插入
	m.insert(make_pair(2, "two"));
	m.insert(make_pair(1, "one"));
	m.insert(make_pair(3, "three"));
	for (auto e : m)
	{
		cout << "<" << e.first << "," << e.second << ">" << " ";
	}
	cout << endl; //<1,one> <2,two> <3,three>
	return 0;
}

find

返回值

查找函数是根据所给key值在其中当中进行查找,若找到了,则返回对应元素的迭代器,若未找到,则返回容器中最后一个元素下一个位置的正向迭代器,即也就是end()。

int main()
{
	map<int, string> m;
	m.insert(make_pair(2, "two"));
	m.insert(make_pair(1, "one"));
	m.insert(make_pair(3, "three"));
	//获取key值为2的元素的迭代器
	map<int, string>::iterator pos = m.find(2);
	if (pos != m.end())
	{
		cout << pos->second << endl; //two
	}
	return 0;
}

erase

用的最多的也就是第二个,这个返回值没有什么特的,返回值为实际删除的元素个数。

int main()
{
    map<int, string> m;

    m.insert(make_pair(2, "two"));
    m.insert(make_pair(1, "one"));
    m.insert(make_pair(3, "three"));

    auto it = m.begin();
    m.erase(it); // 删除了  2, "two"

    m.erase(1); // 删除了   1, "one"
    //   返回值为1

    m.erase(m.begin(), m.end());// 删除了   3, "three"
    return 0;
}

map的[ ]运算符重载

这个成员函数也是map与set与众不同的一点,set中没有这个函数,其的功能特别的强大。

其在官方中的函数原型如下:

mapped_type& operator[] (const key_type& k);

[ ]运算符重载函数的参数就是一个key值,而这个函数的返回值如下:

(*((this->insert(make_pair(k, mapped_type()))).first)).second

就这样看着不太好理解,我们整理一下,实际上[ ]运算符重载实现的逻辑实际上就是以下三个步骤:

  1. 调用insert函数插入键值对。
  2. 拿出从insert函数获取到的迭代器。
  3. 返回该迭代器位置元素的值value。

对应分解代码如下:

mapped_type& operator[] (const key_type& k)
{
	//1、调用insert函数插入键值对
	pair<iterator, bool> ret = insert(make_pair(k, mapped_type()));
	//2、拿出从insert函数获取到的迭代器
	iterator it = ret.first;
	//3、返回该迭代器位置元素的值value
	return it->second;
}

 那么这个函数的价值体现在哪里呢?我们来看看下面这段代码:

统计出现次数:

int main()
{
	string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	map<string, int> ht;
	for (auto& e : arr)
	{
		ht[e]++;
	}

	for (auto& kv : ht)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	cout << endl;

	//打印结果:
    //苹果 : 6
    //甜瓜 : 1
    //西瓜 : 3
    //香蕉 : 3
	return 0;
}

说明一下:

  1. 如果k不在map中,则先插入键值对<k, V()>,然后返回该键值对中V对象的引用。其中的v()是默认构造,比如说如果v为int类型,那么就会默认构造为0,char为’0‘,指针为nullptr.
  2. 如果k已经在map中,则返回键值为k的元素对应的V对象的引用。注意是引用!!!

同样还可以修改其中的value:

int main()
{
	string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	map<string, int> ht;
	for (auto& e : arr)
	{
		ht[e]++;
	}

	for (auto& kv : ht)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	cout << endl;

	//打印结果:
    //苹果 : 6
    //甜瓜 : 1
    //西瓜 : 3
    //香蕉 : 3

	ht["苹果"] = 100;

	//打印结果:
	//苹果 : 100
	//甜瓜 : 1
	//西瓜 : 3
	//香蕉 : 3
	return 0;
}

迭代器

迭代器的遍历其实是比较麻烦的,因为set,map的底层使用红黑树来写的,不是简单的进行++/--就可以了,走起来还是不叫麻烦的,这里就不说了,会在红黑树中解释。迭代器使用起来还是没难度的,也不举例了。

再次提醒: 编译器在编译时会自动将范围for替换为迭代器的形式,因此支持了迭代器实际上就支持了范围for。

map的其他成员函数

成员函数功能
size获取容器中元素的个数
empty判断容器是否为空
clear清空容器
swap交换两个容器中的数据
count获取容器中指定key值的元素个数
#include <iostream>
#include <string>
#include <map>
using namespace std;

int main()
{
	map<int, string> m;
	m.insert(make_pair(2, "two"));
	m.insert(make_pair(1, "one"));
	m.insert(make_pair(3, "three"));
	//获取容器中元素的个数
	cout << m.size() << endl; //3
	//容器中key值为2的元素个数
	cout << m.count(2) << endl; //1
	//清空容器
	m.clear();
	//容器判空
	cout << m.empty() << endl; //1
	//交换两个容器中的数据
	map<int, string> tmp;
	m.swap(tmp);
	return 0;
}

multimap

multimap容器与map容器的底层实现一样,也都是平衡搜索树(红黑树),其次,multimap容器和map容器所提供的成员函数的接口都是基本一致的,这里也就不再列举了,multimap容器和map容器的区别与multiset容器和set容器的区别一样,multimap允许键值冗余,即multimap容器当中存储的元素是可以重复的。

同样其因为允许存在重复的K,所以erase还是与set的操作是相似的,不在说明,有问题可以看set部分,也可以评论,看见必解释。

由于multimap容器允许键值冗余,因此两个容器中成员函数find和count的意义也有所不同:

成员函数find功能
map对象返回值为键值为key的元素的迭代器
multimap对象返回底层搜索树中序的第一个键值为key的元素的迭代器
成员函数count功能
map对象键值为key的元素存在则返回1,不存在则返回0(find成员函数可代替)
multimap对象返回键值为key的元素个数(find成员函数不可代替)

 其次,由于multimap容器允许键值冗余,调用[ ]运算符重载函数时,应该返回键值为key的哪一个元素的value的引用存在歧义,因此在multimap容器当中没有实现[ ]运算符重载函数

  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值