目录
关联式容器
在介绍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> ),这是和序列式容器最大的不同。除此之外,序列式容器中存储的元素默认都是未经过排序的,而使用关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序。
推荐文章:
下面开始这篇的讲解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的介绍
-
set是按照一定次序存储元素的容器,使用set的迭代器遍历set中的元素,可以得到有序序列。
-
set当中存储元素的value都是唯一的,不可以重复,因此可以使用set进行去重。
-
set默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。
-
set中的元素不能被修改,因为set在底层是用二叉搜索树来实现的,若是对二叉搜索树当中某个结点的值进行了修改,那么这棵树将不再是二叉搜索树。
-
与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对,因此在set容器中插入元素时,只需要插入value即可,不需要构造键值对。
-
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的介绍
- 底层同样是用红黑树来写的。
所以在map当中查找某个元素的时间复杂度为logN
-
map是关联式容器,它按照特定的次序(按照key来比较)存储键值key和值value组成的元素,使用map的迭代器遍历map中的元素,可以得到有序序列。
-
在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,并取别名为pair。
-
在map中的键值key不能被修改,但是元素的值value可以被修改,因为map底层的二叉搜索树是根据每个元素的键值key进行构建的,而不是值value。
-
与set相同的是其默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。
-
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
就这样看着不太好理解,我们整理一下,实际上[ ]运算符重载实现的逻辑实际上就是以下三个步骤:
- 调用insert函数插入键值对。
- 拿出从insert函数获取到的迭代器。
- 返回该迭代器位置元素的值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;
}
说明一下:
- 如果k不在map中,则先插入键值对<k, V()>,然后返回该键值对中V对象的引用。其中的v()是默认构造,比如说如果v为int类型,那么就会默认构造为0,char为’0‘,指针为nullptr.
- 如果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容器当中没有实现[ ]运算符重载函数