Loading

欢乐C++ —— 9. 关联容器概览

简述

在之前的顺序容器概览中,已经简单的介绍了STL 和容器的概念,这节主要看看基础的关联容器操作。

关联容器与顺序容器的主要区别:1. 关联容器中的元素是按关键字保存并访问元素,而顺序容器的元素是按照它们在容器中的位置来顺序保存和访问的。2. 关联容器保存的是 键值对 或 键,而顺序容器存放的是 值。3. 顺序容器不会自动排序,而有序的关联容器内部保持有序。

常见的关联容器

以上类型分别包含在4个头文件 <map> <set> <unordered_map> <unordered_set>

关联容器支持高效的关键字查找和访问。两个基础的关联容器类型是map 和 set ,map 保存键值对,而set 只保存键。

对于有序容器,关键字类型必须支持比较,默认通过<比较,也可以在创建容器时自定义比较操作。

因为关联容器内部对元素存放策略,所以map 不可以修改pair和pair的key 成员(但可以修改second成员(值)),set 不可以修改关键字。

pair

map 一般都通过pair 存储键值对。 pair 模板类定义在 中,有两个成员first (对应key)和 second (对应value)。

pair支持<, >, <=, >=, == 等操作:< 的含义是当 first_1< first_2 || second_1<second_2 时为真,其它操作大都调用<运算符。

容器内置类型
  • key_type 表示此容器的关键字类型(键类型) 关键字类型都不可修改
  • value_type 表示容器存放的元素类型 。map 是pair 类型, 而set 的 value_type 和 key_type 相同
  • mapped_type 表示此容器的关联类型(值类型) 仅 map 有。
底层实现
  • map set multimap multiset 红黑树
  • unordered_map unordered_set unordered_mulitimap unordered_mulitiset 哈希链表

容器操作

添加元素

大部分操作和顺序容器一样,不过没有按位置插入的操作(push_back ….) 但可以使用insert , emplace 。

  • 向 map 添加元素,要注意 map 的元素类型是 pair 类类型。如果 map 内已经有这个关键字,则不做操作。

    • 向multimap 添加元素,返回容器中指向这个元素的迭代器。

    函数的返回值也是一个pair类型,不过这个pair.first 是 map 的迭代器类型;pair.second 是标记添加元素是否成功的 bool 值。

  • 向set 添加元素,没有返回值,并且如果set 中有这个关键字,则不做操作。

    • 向multiset 添加元素,没有返回值。
#include <iostream>

#include <vector>
#include <string>

#include <map>
#include <set>
#include <unordered_set>
#include <unordered_map>
#include <utility>
#include <algorithm>

using namespace std;

int main( ) {
	map<int, string> m_map;
	{
		m_map.insert(make_pair(1, "str:1"));
		m_map.insert({ 4,"str:4" });
		m_map.insert(pair<int, string>(6, "str:6"));
		m_map.insert(map<int, string>::value_type(8, "str:8"));
	}
	pair<map<int, string>::iterator, bool> it;

	it = m_map.emplace(10, "str:10");
	cout << it.first->first << "  " << it.first->second << "  " << it.second << endl;
	it = m_map.emplace(10, "new str:10");
	cout << it.first->first << "  " << it.first->second << "  " << it.second << endl;
	it = m_map.try_emplace(10, "str:999");
	cout << it.first->first << "  " << it.first->second << "  " << it.second << endl;

	for_each(m_map.begin( ), m_map.end( ), [ ] (auto&i) {cout << i.first << ' ' << i.second << endl; });
	cout << "————map over————" << endl;

	multimap<int, string> multi_map;
	auto it2 = multi_map.insert({ 1,"123" });
	cout << it2->first << "  " << it2->second << endl;
	it2 = multi_map.insert({ 1,"new str123" });
	cout << it2->first << "  " << it2->second << endl;

	cout << "————multimap over————" << endl;

	set<int> m_set;
	m_set.insert(1);
	m_set.insert({ 1,2,3,4,5 });

	for_each(m_set.begin( ), m_set.end( ), [ ] (auto &i) {cout << i << ' '; });
	cout << endl << "————set over————" << endl;

	multiset<int> multi_set;
	multi_set.insert({ 1,1,1 });

	for_each(multi_set.begin( ), multi_set.end( ), [ ] (auto &i) {cout << i << ' '; });
	cout << endl << "————multiset over————" << endl;
	return 0;
}
删除元素

关联容器的erase 可以分为两种:

  • 按关键字删除,这种会返回非负整数,表示成功删除的元素个数。
  • 按迭代器删除,和顺序容器一样,可以指定一个元素,也可以删除一个范围,都返回删除的元素的后一个位置的迭代器。
访问元素
image-20200504105821792

其中,lower_bound() 和 upper_bound() 不适用于无序容器。而equal_range 之所以适合无序容器跟无序容器底层有关。

map 特有的下标操作

两个操作都只适用于非const 的 map 和 unordered_map 。两个操作不同之处在于当关键字key 不在容器中的操作。

  • **map[key] **:返回容器中key 映射的值的引用。若容器中没有则添加一个{键为key,值为值初始化}的键值对,再返回其值的引用。
  • **map.at(key) **:返回容器中key 映射的值的引用。若容器中没有会抛出 out_of_range 。
无序容器特有操作

在某些应用场景,维护元素有序的代价较大,这时可以考虑无序容器。

相比较有序关联容器利用比较运算符组织元素,无序关联容器使用哈希函数和 == 。其底层存储 表现为一组桶,每个桶可以有多个元素。其性能依赖于哈希函数和桶的大小、数量。

无序容器提供的一组管理桶的函数,我们能在必要的时候手动调整容器底层。

image-20200504122006648

无序容器对关键字的类型是有要求的,1. 其必须支持 == 操作。2. 其必须有hash 函数。

一般而言,内置的类型都支持 == 操作,并且标准库为常见的类型设计了hash 函数。但如果想存储自定义类型时,就需要注意我们须要定制这两个操作。

以下面为例,第一个类型参数为是键类型(MyClass )第二个参数是值类型(int) 第三个类型参数是函数对象类型,表示接受参数为MyClass 返回值为int 的可调用对象,第四个参数是 == 比较运算符,由于我的MyClass中写了==操作,所以这块就可以省略。

unordered_multimap<MyClass,int,function<int(MyClass)>> qwe;

单词计数例子

#include <iostream>
#include <map>
#include <set>
#include <string>
using namespace std;
int main( ) {
	map<string, size_t> word_count;
	string word;
	while ( cin >> word ) {
		++word_count[word];
	}

	for ( auto &pair : word_count ) {
		cout << pair.first << ": " << pair.second << endl;
	}
	return 0;
}

定义一个map,相当于定义了一个关键字-值 映射关系,本例中是 string 与 size_t 的映射关系,它们两个实际上被保存在标准库 pair 结构里面,pair.first 键 保存的是string pair.second 值 保存的是 size_t 。之后的while 循环向map里面添加元素。因为map 重写了 [ ] 运算符,当map 有这个键,则直接返回值的引用。当map中没有这个关键字,会新创建一个键值对并且返回值的引用

对这个进行小的扩展,例如我们只想统计某些单词的个数,并且忽略大小写和标点。例如 “exp,” “exp.” “Exp” 都应该算作同一个单词。我们可以顶一个set 专门记录要统计的单词,每次输入检测是否在set 中

#include <iostream>
#include <map>
#include <set>
#include <string>
#include <algorithm>
using namespace std;
int main( ) {
	map<string, size_t> word_count;
	set<string> include = { "yes","no"};
	
	string word;
	
	while ( cin >> word ) {
		string trans;
		transform(word.begin( ), word.end( ), back_inserter(trans), tolower);

		if ( include.find(trans) == include.end( ) ) {
			trans.erase(trans.end( ) - 1);
			if ( include.find(trans) == include.end( ) ) {
				continue;
			}
		}
		++word_count[trans];
	}

	for ( auto &pair : word_count ) {
		cout << pair.first << ": " << pair.second << endl;
	}
	return 0;
}

/* 结果
apple No, no. No Yes. yes,
^Z
no: 3
yes: 2
*/
image-20200418090200857

初看有点懵,实际上注意关联容器迭代器的限制,multiset没有提供push_back 的函数,所以不能使用back_inserter

总结

image-20200504183625870
posted @ 2020-03-22 14:31  沉云  阅读(154)  评论(0编辑  收藏  举报