关联容器和顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的,顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,关联容器支持高效的关键字查找和访问,俩个主要的关联容器类型是map和set,map中的元素是一些关键字--值(key--value)对,关键字起到索引的作用,值则表示与索引相关联的数据,set中每个元素只包含一个关键字,set底层是基于map实现的,set支持高效的关键字查询操作----检查一个给定关键字是否在set中。
标准库提供8个关联容器,这8个容器不同体现在三个维度上:1.map或set 2.关键字重复(multi)或不重复 3.按顺序保存或无序保存(unordered),类型map和multimap定义在#include <map>中,set和multiset定义在#include <set>中,无序容器定义在#include <unordered_map> 和 #include <unordered_set>中
按关键字有序保存元素
有序的关联容器底层实现是红黑树,无序容器底层实现是哈希表
map //关联数组:保存关键字---值对
set //关键字即值,即只保存关键字的容器
multimap //关键字可重复出现的map
multiset //关键字可重复出现的set
无序集合 无序容器使用哈希函数来组织元素
unordered_map //用哈希函数组织的map
unordered_set //用哈希函数组织的set
unordered_multimap //哈希组织的map,关键字可以重复出现
unordered_multiset //哈希组织的set,关键字可以重复出现
-
map
map中存放的是pair类型的对象,pair是一个模版类型,保存俩个名为first和second的数据成员,map所使用的pair用first成员保存关键字,用second成员保存对应的值
初始化map:
初始化一个map时,必须提供关键字类型和值类型
//pair初始化
map<string, int> m = { pair<string,int>("ss",11),pair<string,int>("dd",22) };
for (auto mem : m)
cout << mem.first << "--" << mem.second << endl; //dd--22 ss--11
//列表初始化
map<string, int> m2 = { {"qq",55},{"tt",99},{"oo",88} };
for (auto mem2 : m2)
cout << mem2.first << "--" << mem2.second << endl; //oo--88 qq--55 tt--99
//迭代器初始化
map<string, int> m3(m.cbegin(), m.cend());
for (auto mem3 : m3)
cout << mem3.first << "--" << mem3.second << endl; //dd--22 ss--11
//拷贝构造初始化
map<int, int> m1{ {1,2},{2,2},{3,4} };
map<int, int> m2(m1);
for (auto i : m2)
cout << i.first << "," << i.second << endl; //1,2 2,2 3,4
嵌套map的初始化以及插入数据
当多个map进行嵌套式,可以先拆出单个map的初始化以及插入方式,然后再写多个嵌套的方式,这样会更容易书写正确
map<int, int> m1 = { pair<int,int>(1,2) };
map<int, int> m2 = { {1,2} };
map<int, int> m6 = { make_pair(1,2) };
map<int, map <int, int>> m3 = { pair<int,map<int,int>>(1,map<int,int>{pair<int,int>(1,2)}) };
map<int, map <int, int>> m4 = { {1,map<int,int>{pair<int,int>(1,2) }} };
m4.insert(pair<int, map<int, int>>(1, map<int, int>{make_pair(1, 2)}));
m4.insert(make_pair(1, map<int, int>{pair<int, int>(1, 2)}));
m4.insert(make_pair(1, map<int, int>{make_pair(1, 2)}));
map<int, map <int, int>> m5 = { pair<int,map<int,int>>(1,map<int,int>{ {1,2}}) };
特性:
非multimap修饰的关联容器中的key唯一,如果key值为内置类型,则在放入元素时,放入的元素和原有的元素有key值相同的情况时,将不会保存后放入的元素,如果key值为自定义类型,则会根据定义的比较函数或运算符重载函数中的比较元素进行去重的处理。
情况一:key值为内置类型
map<int, int> m{ {1,7},{6,3},{2,5},{3,2} };
for (auto i : m) //1,7 2,5 3,2 6,3
cout << i.first << "," << i.second <<" ";
cout << endl;
map<int, int> m2{ { 1,7 },{ 1,3 },{ 2,5 },{ 3,2 } };
for (auto i : m2) //1,7 2,5 3,2
cout << i.first << "," << i.second <<" ";
情况二:key值为自定义类型
因为是根据类型A中的b字段进行的比较,所以元素中b字段相同的元素会根据加入的顺序进行"去重"处理
class A
{
public:
A() = default;
A(int _a, int _b) : a(_a), b(_b) {}
int a;
int b;
bool operator < (const A&another) const
{
return this->b > another.b;
}
};
int main()
{
map<A, int> m{ {A(8,1),1},{A(3,5),2},{A(1,7),7},{A(4,6),3} };
for (auto i : m) //1,7---7 4,6---3 3,5---2 8,1---1
cout << i.first.a << "," << i.first.b << "---" << i.second << endl;
cout << "-----" << endl;
map<A, int> m2{ { A(8,5),1 },{ A(3,5),2 },{ A(1,7),7 },{ A(4,6),3 } };
for (auto i : m2) //缺少 3,5---2
cout << i.first.a << "," << i.first.b << "---" << i.second << endl;
}
自定义关键字类型的排序比较方法:
关联容器(unordered修饰的除外)中的元素是有序的,它们根据关联容器的key值进行排序,如果key值是内置类型则默认按照less模版,也就是从小到大排序,如果key值是自定义类型,则如果想要定义此类型的关联容器,则必须先定义此自定义类型的比较函数,如果此自定义类型没有比较函数,则不能定义该类型的关联容器。
情况一:使用标准库函数定义关键字为内置类型的比较方法
默认的方式是使用less<int>进行从小到大的升序排序,可以指定为greater<int>进行降序排序
//等同于map<int,int,less<int>>
map<int, int> m{ { 1,7 },{ 6,3 },{ 2,5 },{ 3,2 } };
for (auto i : m) //1,7 2,5 3,2 6,3
cout << i.first << "," << i.second << " ";
cout << endl;
map<int, int,greater<int>> m2{ { 1,7 },{ 6,3 },{ 2,5 },{ 3,2 } };
for (auto i : m2) //6,3 3,2 2,5 1,7
cout << i.first << "," << i.second << " ";
情况二:使用比较函数初始化没有重载<运算符的自定义类型的关联容器
如果自定义类型A中没有定义<运算符,我们可以使用一个比较函数来定义此类型的关联容器,并且在初始化时用decltype来指出第三个参数,当用decltype来获得一个函数指针类型时,必须加一个*来指出我们要使用一个给定函数类型的指针,而且需要用比较函数cmp来初始化此对象,这表示我们向m添加元素时,通过调用cmp来为这些元素排序,可以用&cmp代替cmp作为构造函数的参数,因为当我们使用一个函数名字时,在需要的情况下它会自动转化为一个指针,所以&cmp和cmp效果一样,还需要注意的是此方法必须在初始化时用比较函数作为构造函数的参数进行初始化,之后再添加元素,不能直接使用列表初始化,否则会出错。
class A
{
public:
A() = default;
A(int _a, int _b) : a(_a), b(_b) {}
int a;
int b;
};
bool cmp(const A&a1, const A&a2)
{
return a1.b > a2.b; //1,4--1 4,3--6 2.2--2 3,1--4
//return a1.b < a2.b; //3,1--4 2,2--2 4,3--6 1,4--1
}
int main()
{
//这里不能直接列表初始化{}
map<A, int, decltype(cmp)*> m(cmp); //必须要用cmp来初始化,不然输出有误
m.insert({ A(1,4),1 });
m.insert(pair<A, int>(A(2, 2), 2));
m.insert({ A(3,1),4 });
m.insert({ A(4,3),6 });
for (auto i : m)
cout << i.first.a << "," << i.first.b << "---" << i.second << endl;
}
情况三:使用重载了<运算符的自定义类型来初始化关联容器
有三种方法:1.做友元函数 2.做成员函数 3.做普通函数
class A
{
public:
A() = default;
A(int _a, int _b) : a(_a), b(_b) {}
int a;
int b;
//运算符重载做友元
//friend bool operator <(const A&a1, const A&a2)
//{
// return a1.b > a2.b;
//}
//运算符重载做成员
//bool operator <(const A&another) const
//{
// return this->b > another.b;
//}
};
//运算符重载函数作普通函数
bool operator <(const A&a1, const A&a2)
{
//return a1.b > a2.b; //1,4--1 4,3--6 2,2--2 3,1--4
return a1.b < a2.b; //3,1--4 2,2--2 4,3--6 1,4--1
}
int main()
{
map<A, int> m{ { A(1,4),1 },{ A(2, 2), 2 },{ A(3,1),4 },{ A(4,3),6 } };
for (auto i : m)
cout << i.first.a << "," << i.first.b << "---" << i.second << endl;
}
情况四:使用仿函数来初始化关联容器
class A
{
public:
A() = default;
A(int _a, int _b) : a(_a), b(_b) {}
int a;
int b;
};
class mySort //不能定义成与系统中自带函数相同的名字
{
public:
bool operator() (const A&a1, const A&a2) const
{
return a1.b > a2.b; //1,4--1 4,3--6 2.2--2 3,1--4
//return a1.b < a2.b; //3,1--4 2,2--2 4,3--6 1,4--1
}
};
int main()
{
//如果mySort定义成sort则会报错,因为系统中也有sort函数,导致sort不明确
//可以列表初始化
map<A, int, mySort> m{ { A(1,4),1 } ,pair<A, int>(A(2, 2), 2) ,{ A(3,1),4 } ,{ A(4,3),6 } };
//m.insert({ A(1,4),1 });
//m.insert(pair<A, int>(A(2, 2), 2));
//m.insert({ A(3,1),4 });
//m.insert({ A(4,3),6 });
for (auto i : m)
cout << i.first.a << "," << i.first.b << "---" << i.second << endl;
}
需要注意的是仿函数的名字不能与程序中自带的相关函数重名,否则会报出使用不明确的错误,例如将名字从mySort改为sort,则会报错。
-
set
初始化set:(其他操作类比map)
//列表初始化
set<string> s = { "aaa","bbb","ccc","ddd","eee" };
for (auto mem : s)
cout << mem << endl; //aaa bbb ccc ddd eee
//迭代器初始化
vector<string> v{ "rr","tt","yy","uu" };
set<string> s2(v.cbegin(), v.cend());
for (auto mem2 : s2)
cout << mem2 << endl; //rr tt uu yy
//拷贝构造初始化
set<int> s{1,2,3};
set<int> s2(s);
关联容器额外的类型别名:
key_type //此容器类型的关键字类型
mappend_type //每个关键字关联的类型(也就是对应的值):只适用于map
value_type //对于set,与key_type相同 对于map,为pair<const key_type,mapped_type>
对于set类型,key_type和value_type是一样的,因为set中保存的值就是关键字,而map中,每个元素是一个pair对象,包含一个关键字和一个关联的值,由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的。
set<string>::value_type v1; //v1是一个string
set<string>::key_type v2; //v2是一个string
map<string,int>::value_type v3; //v3是一个 pair<const string,int>
map<string,int>::key_type v4; //v4是一个string
map<string,int>::mapped_type v5; //v5是一个int
关联容器迭代器:
由于关联容器不能改变它的关键字成员的值,所以也不能通过迭代器来改变关键字的值,对于map而言不能改变其pair的first成员,但可以改变second成员,对于set而言,set的迭代器是const的,所以set只允许访问其中的元素,而不能修改其中的值,当使用一个迭代器遍历一个map、multimap、set、multiset时,迭代器按关键字升序遍历元素。
关联容器和算法:
我们通常不对关联容器使用泛型算法,关键字是const这一特性意味着不能将关联容器传递给修改或重排元素的算法,因为这类算法需要向元素写入值,关联容器可用于只读取元素的算法,但是,很多这类算法都要搜索序列,由于关联容器中元素不能通过关键字进行快速查找,因此对其使用泛型搜索算法几乎总是个坏主意,例如,如果使用泛型的find算法,则会进行顺序搜索,如果使用关联容器定义的find成员,则会通过一个给定的关键字直接获取元素,会比第一个方法快的多。
添加元素:
m.insert(v) //v是value_type类型的对象,
m.emplace(args) //args用来构造一个元素
对于map和set,只有当元素的关键字不在m中时才插入(或构造)元素,函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值,对于multimap和multiset,总会插入(或构造)给定元素,并返回一个指向新元素的迭代器。
m.insert(b,e) //b和e是迭代器,表示一个m::value_type类型值的范围
m.insert(il) //il是这种值的花括号列表,函数返回void
m.insert(p,v)
m.emplace(p,args)
//类似insert(v) 或emplace(args) ,但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置,返回一个迭代器,指向具有给定关键字的元素
检测insert的返回值:
insert(或emplace)返回的值依赖于容器类型和参数,对于不包含重复(multi)关键字的容器,添加单一元素的版本返回一个pair,用于告诉我们插入操作是否成功,pair的first成员是一个迭代器,指向容器中的元素,second成员是一个bool值,指出元素是插入成功该是已经存在于容器中,如果关键字已在容器中,则insert什么事也不做,且返回值中的bool部分为false,如果关键字不存在,元素被插入容器中,且bool值为true。
map<int, int> m{ {1,1},{2,1},{3,1} };
auto p1=m.insert({ 5,2 });
if (p1.second == false)
cout << p1.first->first << "," << p1.first->second << " add fail" << endl;
else
cout << p1.first->first << "," << p1.first->second << " add success" << endl;
for (auto c : m)
cout << c.first << "," << c.second << " ";
cout << endl;
对于map,返回的pair中的first参数为map的迭代器,指向一个,map中的pair,所以p1.first->first为关键字,p1.first->second为值。
set<int> s;
s.insert({ 1,2,3,4 });
auto p1 = s.insert(4);
if (p1.second == false)
cout << *(p1.first) << " add fail" << endl;
else
cout << *(p1.first) << " add success" << endl;
for (auto i : s)
cout << i << " ";
cout << endl;
auto p2 = s.insert(5);
if (p2.second == false)
cout << *(p2.first) << " add fail" << endl;
else
cout << *(p2.first) << " add success" << endl;
for (auto i : s)
cout << i << " ";
对于set,返回的pair中first参数为set的迭代器,所以*(p1.first)为set中关键字的
删除元素:
m.erase(k) //从m中删除每个关键字为k的元素,返回一个size_type值,值为删除的元素的数量
m.erase(p) //从m中删除迭代器p指定的元素,p必须指向m中一个真实元素,不能等于m.end()。返回一个指向p之后元素迭代器,若p指向m中的尾元素,则返回m.end()
m.erase(b,e) //删除迭代器b和e所表示的范围中的元素,返回e
map的下标操作:
map和unordered_map容器提供了下标运算符和一个对应的at函数,set类型不支持下标,因为set中没有与关键字相关联的"值",我们也不能对一个multimap或一个unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联,与其他下标运算符不同的是,如果关键字并不在map中,则会为它创建一个元素并插入到map中,关联的值将进行值初始化。
m[k] //返回关键字为k的元素(mapped_type对象),如果k不在m中,则添加一个关键字为k的元素,对其进行值初始化
m.at(k) //返回关键字为k的元素,带参数检查:若k不在m中,抛出一个out_of_range异常
map的下标运算符与我们用过的其他下标运算符的一个不同之处是其返回类型,通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的,但对map则不然,当对一个map进行下标操作时,会获得一个mapped_type对象,但当解引用一个map迭代器时,会得到一个value_type对象,与其他下标运算符相同的是,map的下标运算符返回一个左值,由于返回的是一个左值,所以既可以读也可以写元素。
map<int, int> m;
for (int i = 0; i < 5; i++)
++m[i];
for (auto i : m)
cout << i.first << "," << i.second << endl;
//0,1 1,1 2,1 3,1 4,1
访问元素:
m.find(k) //返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器
m.count(k) //返回关键字等于k的元素的数量,对于不允许重复关键字的容器,返回值永远是0或1
lower_bound和upper_bound不适用于无序容器
m.lower_bound(k) //返回一个迭代器,指向第一个关键字不小于k的元素
m.upper_bound(k) //返回一个迭代器,指向一个关键字大于k的元素
m.equal_range(k) //返回一个迭代器pair,表示关键字等于k的元素的范围,若k不存在,pair的俩个成员均等于m.end()
无序容器:
适用unordered来修饰的关联容器,在新标准中定义了4个,这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符,在关键字类型的元素没有明显的序关系的情况下,无序容器是非常有用的,在某些应用中,维护元素的序代价非常高昂,此时无序容器也很有用。
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素,无序容器使用一个哈希函数将元素映射到桶,为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶,容器将一个特定哈希值的所有元素都保存在相同的桶中,如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中,因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。