一、关联式容器
在之前我已经学习过STL中的部分容器,比如:vector、list、dequeue、等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。
那什么是关联式容器呢?它和序列式容器又什么区别呢?
关联式容器也是用来存放数据的,与序列式容器不同的是,它里面存储的是<key,value>结构的键值对,在数据检索时比序列式容器效率高。
二、键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与键值对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词对应中文含义,而且,英文单词与其中含义是一一对应的关系,即通过该单词,在字典中就可以找到与其对应的中文含义。
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)
{}
};
三、树形结构的关联式容器 搜索二叉树 == 排序二叉树
根据应用场景的不同,STL共实现了两种不同结构的关联式容器:树形结构和哈希结构。树形结构的关联式容器主要有四种:map,set,multimap,multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树),作为其底层实现,容器中的元素是一个有序的序列。
3.1 set
3.1.1 set的使用
#include<iostream>
#include<map>
#include<set>
using namespace std;
void test_set()
{
set<int> s;
s.insert(4);
s.insert(2);
s.insert(3);
s.insert(1);
s.insert(5);
s.insert(6);
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
int main()
{
test_set();
system("pause");
return 0;
}
执行上面的代码,通过运行结果我们可以发现,这个走的是搜索树的中序遍历。
因为搜索树的特点就是它的左子树别根节点小,右子树比根节点大,所以走中序遍历,更好是有序的。
void test_set()
{
set<int> s;
s.insert(4);
s.insert(2);
s.insert(3);
s.insert(1);
s.insert(5);
s.insert(6);
s.insert(6);
s.insert(6);
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
执行上面的代码,运行的结果,任然是1,2,3,4,5,6.
插入许多6但最终,s中只要一个6,因此set可以用在查找,那么我们来看一下的下面的两种查找方式有什么区别呢?
//auto it = s.find(3);
auto it = find(s.end(), s.end(),3);
if (it != s.end())
{
cout << "找到了" << endl;
}
那么我们上面的代码都是查找,有什么区别呢?
auto it = s.find(3); //时间复杂度是O(logN),走树的高度次
auto it = find(s.begin(), s.end(),3); //时间复杂度是O(N),它是适用于STL所有的容器的查找,底层使用暴力搜索来实现的
其实这两种时间复杂度的区别是非常大的,当数据量的的时候,比如要在全中国的所有人中要查找一个人,N是13亿对,使用第一种查找方式,最多只需要查找31次,而使用第二种查查找方式,最多需要查找13亿次,这两者之间的差别是非常大的。
it = s.find(3);
//s.erase(it);
s.erase(3);
for (auto &e : s)
{
cout << e << " ";
}
cout << endl;
上面的演示的是set的删除操作,可以使用迭代器来删除,也可以使用val来删除。
这两种删除的区别是,使用迭代器删除时,有就删除,如果set中没有就会报错; 而使用val来删除,有就会删除,没有就不删除。
set<int> s1;
s1.insert(1);
s1.insert(2);
s1.insert(3);
set<int> s2;
s2.insert(4);
s2.insert(5);
s2.insert(6);
s1.swap(s2);
swap(s1, s2);
接下来我们,讨论一下上面的两种交换方式的区别,第一种交换,只需要把指向s1和s2的根节点的指针一交换就好了,而第二种交换方式在会先生成set的对象,在进行赋值,会进行深拷贝,然后在进行赋值,在进行深拷贝,代价比较大,所以使用set自带的swap是比较明智的。
最后我们会发现,set没有修改的接口,有的小伙伴觉得 我们可以使用迭代器来修改,但实际上set的迭代器返回的是const类型的迭代器,不支持修改,那为什么set不支持修改呢,这是因为,set的底层是一颗搜索树,如果单纯的对一个节点进行修改,那么就不是一颗搜索树了。
3.1.2 set的作用
set是一种key的模型,key模型的作用:1、查找关键字在不在。(应用:机器检票)。 2、排序+去重(插入时,如果这个值有了就不在插入)。
3.2 multiset
Multisets are containers that store elements following a specific order, and where multiple elements can have equivalent values.
multiset和set的区别在于,multiset可以存储值相同的元素,其它的接口的使用都是相同的。
multiset<int> ms;
ms.insert(1);
ms.insert(3);
ms.insert(6);
ms.insert(3);
ms.insert(4);
ms.insert(5);
ms.insert(2);
for (auto& e : ms)
{
cout << e << " ";
}
cout << endl;
而在multiset中查找3会怎样呢?
multiset<int> ms;
ms.insert(1);
ms.insert(3);
ms.insert(6);
ms.insert(3);
ms.insert(4);
ms.insert(5);
ms.insert(2);
auto it = ms.find(3);//找到的是中序的第一个3,也就是说找到第一个3后,还继续查找
if (it != ms.end())
{
cout << "找到了" << endl;
while (*it == 3)
{
cout << *it << endl;
++it;
}
}
auto it1 = find(ms.begin(), ms.end(), 3);//而这个找到第一个3之后,就不会向后继续查找。
multiset在实际中的使用场景是,数据有重复,比如要做一个点名册,就要使用multiset,因为名字有可能会重复。
3.3 map
3.3.1 map的使用
map和set的调用接口是比较相似的,区别在于,map插入时:
插入的是一个value_type,而value_type又被定义为一个pair。
map<string, string> dir;
dir.insert(pair<string, string>("sort","排序"));
dir.insert(pair<string, string>("string", "字符串"));
使用上面的代码,写起来还是比较麻烦的,所以还有另外一种写法,make_pair。
void test_map()
{
map<string, string> dir;
dir.insert(pair<string, string>("sort","排序"));
dir.insert(make_pair("string", "字符串"));
}
那这两种写法差异在于,第二种写起来简单一点,那为什么make_pair写起来简单呢?
这是因为,第一种写法是模板类型,第二种写法是函数模板。
template<class T, class V>
struct pair
{
K first;
V second;
//构造函数
pair(K _k, V _v)
:k(_k)
, v(_v)
{}
};
template<class T, class V>
inline pair<K, V> make_pair(const K& key, const V& value)
{
return pair<K, V>(k, v);
}
第一种写法,是拿类型构造一个匿名对象,传过去。
第二种写法,是一个函数模板,通过类型推演,不用自己制定类型,在返回。
void test_map()
{
map<string, string> dir;
dir.insert(pair<string, string>("sort","排序"));
dir.insert(make_pair("string", "字符串"));
map<string, string>::iterator it = dir.begin();
while (it != dir.end())
{
cout << (*it).first << ":" << (*it).second << endl;
++it;
}
cout << endl;
}
上面是进行Map的插入和迭代器遍历,很简单
解下来我们来看一个问题:
string str[] = { "苹果", "苹果", "苹果", "苹果", "香蕉", "橘子", "葡萄", "葡萄", "黄瓜" };
//统计水果出现的次数
方法一:迭代器遍历
map<string, int> count_map;
for (auto &str : strs)
{
auto it = count_map.find(str);
if (it != count_map.end())
{
it->second++;
}
else
{
count_map.insert(make_pair(str, 1));
}
}
for (auto &m : count_map)
{
cout << m.first << ":" << m.second << endl;
}
方法二:operator[]
for (const auto& str : strs)
{
count_map[str]++;
}
for (auto &m : count_map)
{
cout << m.first << ":" << m.second << endl;
}
那为什么[]可以用来做这件事呢?
mapped_type& operator[](const key_type& k)
{
return (*((this->insert(make_pair(k, mapped_type()))).first)).second;
}
使用operator[]时,如果这个key没有,就插入,并且value使用缺省值,就是调用value类型的默认构造函数,对于内置类型也一样
如果没有就,不插入,并且返回该insert返回的pair的value(iterator)的second引用。
这样就可以解释上面的代码能统计出水果的次数了。
比如:当苹果第一次来时,insert时,先调用make_pair生成一个pair,这是value,也就是int调用int的默认构造函数,在插入,然后插入完成后,返回pair中的value是已经插入的pair的迭代器,所以通过该迭代器就可以拿到second,在加加就好了。
最后,也就是说返回的是已经有的key的value的引用。
方法三:通过insert来实现
for (const auto& str : strs)
{
auto ret = count_map.insert(make_pair(str, 1));//auto可以使代码写起来简单,但降低了可读性
if (ret.second == false)
{
ret.first->second++;
}
}
这是因为,insert返回的是一个pair类型的对象,如果已经有这个对象,就不插入,返回的pair的second是false,如果没有,返回的pair的second是true,并且把该对象插入到map中.
3.3.2 map的作用
1、key/value 查找关键字在不在。
2、key/value 通过关键字查找映射的关联信息。
3.4 multimap
multimap和map的使用是完全的一样的,区别在于,插入一个(left,左边),在插入一个(left,“嗯嗯”)也是可以插入进去的。
所以呢,它的insert的返回值不是一个pair,而是一个inerator。
此外呢,还有一个区别是,没有operator,这是因为,multimap中会存在多个相同的key,那这是返回那个key的value的引用呢,就会存在歧义。
因此总结一下:map和multimap的区别在于:
1、map不允许key的冗余,multimap允许key的冗余 。 2、就是接口的上的区别。