C++:STL容器之关联式容器set和map的使用

一、关联式容器
在之前我已经学习过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、就是接口的上的区别。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值