C++ Primer 总结索引 | 第十一章:关联容器

1、关联容器和顺序容器 有着根本的不同:关联容器中的元素是 按关键字来保存和访问的。与之相对,顺序容器中的元素是 按它们在容器中的位置 来顺序保存和访问的

虽然关联容器的很多行为与顺序容器相同,但其不同之处反映了关键字的作用

2、关联容器 支持高效的关键字 查找和访问。两个主要的关联容器类型是 map和set。map中的元素 是一些关键字-值对:关键字起到索引的作用,值则表示 与索引相关联的数据。set中 每个元素只包含一个关键字;set 支持高效的关键字查询操作 —— 检查一个给定关键字 是否在set中
例如,在某些文本处理过程中,可以用一个set来保存想要忽略的单词

3、标准库提供8个关联容器:这8个容器间的不同 体现在三个维度上:每个容器
(1)或者是一个 set,或者是一个map;
(2)或者要求 不重复的关键字,或者 允许重复关键字;
(3)按顺序保存元素,或无序保存。允许重复关键字的容器的名字中 都包含单词multi;不保持关键字按顺序存储的容器的名字 都以单词unordered开头。因此一个unordered_multiset是一个 允许重复关键字,元素无序保存的集合,而一个set 则是一个要求不重复关键字,有序存储的集合。无序容器 使用哈希函数来组织元素

类型map和multimap定义在 头文件map中;set和multiset定义在头文件set中;无序容器 则定义在头文件unordered_map 和 unordered_set 中

关联容器类型

按关键字有序保存元素

容器解释
map关联数组:保存关键字 - 值对
set关键字 即值,即只保存关键字的容器
multimap关键字可重复出现的map
multiset关键字可重复出现的set

无序集合

容器解释
unordered_map用哈希函数组织的map
unordered_set用哈希函数组织的set
unordered_multimap哈希组织的map:关键字可以重复出现
unordered_multiset哈希组织的set:关键字可以重复出现

4、无论在 有序容器中 还是在 无序容器中,具有 相同关键字的元素 都是相邻存储的

1、使用关联容器

1、map是 关键字-值对的集合。map类型 通常被称为关联数组。关联数组 与“正常”数组类似,不同之处在于 其下标不必是整数。通过一个关键字 而不是位置 来查找值

set就是 关键字的简单集合。当只是想知道 一个值是否存在时,set是 最有用的

2、使用map:单词计数程序

// 统计每个单词在输入中出现的次数
//string到size_t的空map
map<string, size_t> word_count;
string word;
while (cin >> word)
	++word_count[word]; // 提取word的计数器并将其加1,注意
for (const auto &w : word_count) // 对map中的每个元素
	// 打印结果
	cout << w.first << " occurs " << w.second
		 << ((w.second > 1) ? " times" : " time") << endl;

类似 顺序容器,关联容器 也是模板。为了定义一个map,我们必须指定 关键字 和 值的类型。map保存的每个元素中,关键字是string类型,值是size_t类型。当对 word_count 进行 下标操作时,我们使用 一个string作为下标,获得 与此string相关联的size_t类型的计数器

如果word还未在map中,下标运算符 会创建一个新元素,其关键字为 word值为0。不管元素 是否是新创建的,我们将其值加1

一旦 读取完所有输入,范围for语句 就会遍历map,从map中 提取一个元素时,会得到 一个pair类型的对象。pair是 一个模板类型,保存两个名为 first 和 second的(公有)数据成员。map所使用的 pair用first成员保存关键字,用second成员 保存对应的值

3、使用set:忽略 常见单词,如"the"、“and”、“or” 等。我们可以使用 set保存 想忽略的单词,只对 不在集合中的单词统计出现次数

// 统计输入中 每个单词出现的次数
map<string, size_t> word_count; // string到size_t的空map
set<string> exclude = {"The", "But", "And", "Or", "An", "A",
					   "the", "but", "and", "or", "an", "a"};
string word;
while (cin >> word)
// 只统计不在exclude中的单词
	if (exclude.find(word) == exclude.end())
		++word_count[word]; // 获取并递增word的计数器

set也是模板。为了定义一个set,必须指定其元素类型,本例中是string。与顺序容器类似,可以对一个关联容器的元素 进行列表初始化

// 只统计不在exclude中的单词
if (exclude.find(word) == exclude.end())

find调用 返回一个迭代器。如果 给定关键字在set中,迭代器 指向该关键字。否则,find返回 尾后迭代器

4、描述map 和 vector的不同:
对于 vector 这样的 顺序容器,元素在其中 按顺序存储,每个元素 有唯一对应的位置编号,所有操作 都是按编号(位置)进行的。例如,获取元素(头、尾、用下标获取任意位置)、插入删除元素(头、尾、任意位置)、遍历元素(按元素位置顺序逐一访问)。底层数据结构是数组、链表,简单但已能保证上述操作的高效。而对于依赖值的元素访问,例如查找(搜索)给定值(find),在这种数据结构上的实现 是要通过遍历完成的,效率不佳

而 map 这种关联容器,就是 为了高效实现 “按值访问元素” 这类操作 而设计的。为了 达到这一目的,容器中的元素 是按关键字值存储的,关键字值与元素数据 建立起对应关系,这就是 “关联” 的含义。底层数据结构是 红黑树、哈希表等,可高效实现 按关键字值查找、添加、删除元素等操作

5、分别给出 最适合使用 list、vector、deque、map以及set的例子
若元素很小(例如 int),大致数量预先可知,在程序运行过程中不会剧烈变化,大部分情况下只在末尾添加或删除,需要频繁访问任意位置的元素,则 vector 可带来最高的效率。若需要频繁在头部和尾部添加或删除元素,则 deque 是最后的选择

如果元素较大(如大的类对象),数量预先不知道,或是程序运行过程中频繁变化,对元素的访问更多是顺序访问全部或很多元素,则 list 很合适

map 很适合对一些对象按它们的某个特征进行访问的情形。典型的例如按学生的姓名来查询学生信息,即可将学生姓名作为关键字,将学生信息作为元素值,保存在 map 中。再比如统计单词出现的次数

set,顾名思义,就是集合类型。当需要保存特定的值集合 —— 通常是满足/不满足某种要求的值集合,用 set 最为方便。比如黑名单

6、编写 单词计数程序,扩展你的程序,忽略大小写和标点。例如,“example."、“example,” 和 “Example” 应该递增相同的计数器

#include <iostream>
#include <set>
#include <map>
#include <string>

using namespace std;

void trans(string& s) { // 并非练习 set 的使用,而是字符串的处理
	for (int i = 0; i < s.size(); i++) {
		if (s[i] >= 'A' && s[i] <= 'Z') {
			s[i] += 'a' - 'A'; // a比A ASCII码值大,转小写
		}
		if (s[i] == '.' || s[i] == ',') // 去掉标点
			s.erase(i, 1);
	}
}

int main()
{
	map<string, size_t> words;
	string s;
	set<string> exclude = { "the", "but", "and", "or", "an", "a" };
	while (cin >> s) {
		trans(s);
		if (exclude.find(s) == exclude.end())
			++words[s];
	}
	for (const pair<string, size_t>& p : words) {
		cout << p.first << " " << p.second << endl;
	}
	return 0;
}

std::string的erase方法 可以用来移除字符串的一部分,有几种不同的重载方式允许不同类型的操作:
1、移除特定位置的字符:

string& erase(size_t pos = 0, size_t len = npos);

这个版本的 erase方法 从位置pos开始移除len个字符。如果len是std::string::npos(默认值),则会移除 从pos开始到字符串末尾的所有字符

2、移除特定迭代器指向的字符:

iterator erase(iterator position);

这个版本 接受一个指向要移除字符的迭代器position,然后 移除该位置的字符,并返回 指向被移除字符之后字符的迭代器

3、移除一个范围内的字符:

iterator erase(iterator first, iterator last);

这个版本 接受两个迭代器first和last,移除从first到last(但不包括last)的所有字符,并 返回一个迭代器,指向原位置last 所指向的第一个未被删除的字符

2、关联容器概述

1、关联容器(有序的和无序的)都支持 2、容器概览 5 中介绍的普通容器操作
关联容器 不支持顺序容器的位置相关的操作,例如push_front 或 push_back。原因是 关联容器中元素 是根据关键字存储的,这些操作对关联容器 没有意义

2、关联容器 确实支持构造函数和插入操作,但它们的工作方式与顺序容器有所不同

1)关联容器的构造函数
初始化列表构造函数、范围构造函数以及拷贝和移动构造函数等

std::map<int, std::string> mapExample = {
    {1, "one"},
    {2, "two"},
    {3, "three"}
};

2)插入操作
对于插入操作,关联容器提供了insert和emplace方法,可以用来添加新元素

insert方法可以接受一个值(对于map和multimap,这将是一个std::pair),一个范围,或一个初始化列表
emplace方法尝试就地构造元素,减少不必要的拷贝或移动操作,参数是用于构造容器元素的构造函数的参数

std::map<int, std::string> mapExample;

// 使用insert
mapExample.insert(std::make_pair(4, "four"));

// 使用insert的初始化列表
mapExample.insert({{5, "five"}, {6, "six"}});

// 使用emplace
mapExample.emplace(7, "seven");

附:emplace_back 和 push_back区别

1)参数类型:
push_back() 接受的是 容器中元素类型的一个对象,而emplace_back()接受的是 构造元素所需的参数

2)效率:
push_back() 需要将一个已经构造好的对象 进行拷贝或者移动操作,而emplace_back() 则直接 在容器中构造新的对象,可以减少一次拷贝或移动的开销,因此在性能上通常更高效

3)灵活性:
emplace_back() 可以 利用构造函数的多态性,可以传递给它的参数比push_back()更加灵活,因为它不需要一个已经构造好的对象,而是直接使用参数来构造新对象

3、关联容器 还支持一些顺序容器不支持的操作 和 类型别名。此外,无序容器 还提供一批用来调整哈希性能的操作

关联容器的迭代器 都是双向的

2.1 定义关联容器

1、定义一个map时,必须 既指明关键字类型 又指明值类型:而定义一个set 时,只需 指明关键字类型,因为set中没有值
每个关联容器都定义了 一个默认构造函数,它创建 一个指定类型的空容器
也可以 将关联容器初始化为 另一个同类型容器的拷贝,或是 从一个值范围 来初始化关联容器,只要这些值 可以转化为容器所需类型就可以
在新标准下,我们也可以对关联容器进行值初始化

map<string, size_t> word_count; // 空容器
// 列表初始化
set<string> exclude = {"the", "but", "and", "or", "an", "a",
					   "The", "But", "And", "Or", "An", "A"};
// 三个元素;authors 将姓映射为名
map<string, string> authors = { {"Joyce", "James"},
								{"Austen", "Jane"},
								{"Dickens", "Charles"} };

将每个关键字-值对包围在 花括号中:{key,value} 来指出 它们一起构成了 map中的一个元素。在每个花括号中,关键字 是第一个元素,值 是第二个

2、初始化multimap 或 multiset:一个map 或 set中的关键字 必须是唯一的,即,对于 一个给定的关键字,只能有一个元素的关键字 等于它。容器multimap 和 multiset 没有此限制,它们 都允许多个元素 具有相同的关键字
例如:一个特定单词则可具有多个与之关联的词义

具有 唯一关键字的容器 与 允许重复关键字的容器之间的区别。首先,我们将创建一个 名为ivec的保存int的vector,它包含20个元素:0到9每个整数 有两个拷贝。我们 将使用此vector 初始化一个set 和 一个multiset:

// 定义一个有20个元素的vector,保存0到9每个整数的两个拷贝
// iset包含来自ivec的不重复的元素;miset包含来自ivec的所有20个元素
set<int> iset(ivec.cbegin(), ivec.cend());
multiset<int> miset(ivec.cbegin(), ivec.cend());

cout << ivec.size() << endl; // 打印出20
cout << iset.size() << endl; // 打印出10,对应 ivec 中每个不同的元素
cout << miset.size() << endl;// 打印出20

3、set和list:两者都可以保存元素集合

如果只需要顺序访问这些元素,或是按位置访问元素,那么应使用 list
如果需要快速判定是否有元素等于给定值,则应使用 set

4、定义一个map,关键字是家庭的姓,值是一个vector,保存家中孩子(们)的名。编写代码,实现添加新的家庭以及向已有家庭中添加新的孩子;扩展map,添加一个pair的vector,保存孩子的名和生日

#include <iostream>
#include <set>
#include <map>
#include <string>
#include <vector>

using namespace std;

void addChild(map<string, vector<pair<string, string>>>& m, const string& xin, const string& name, const string& birth) {
	m[xin].push_back(pair<string, string>(name, birth));
}

void addFamily(map<string, vector<pair<string, string>>>& m, const string& xin) {
	if (m.find(xin) == m.end())
		// m[xin] = vector<pair<string, string>>(); // 整个空的vector
		m[xin]; // 等价
}

int main()
{
	map<string, vector<pair<string, string>>> m;
	addChild(m, "王", "五", "2000.1.1"); // 直接m[min]的时候就生成空vector了
	addFamily(m, "张");
	addFamily(m, "李");
	addFamily(m, "张");
	addChild(m, "张", "三", "2000.12.21");
	addChild(m, "张", "四", "2001.7.5");
	addChild(m, "张", "五", "2001.9.9");
	addChild(m, "李", "四", "2002.5.6");
	for (pair<string, vector<pair<string, string>>> p : m) {
		for (auto pss : p.second) {
			cout << p.first << pss.first << " " << pss.second << " ";
		}
		cout << endl;
	}
	return 0;
}

5、使用 vector 保存不重复单词,需要用 find 查找新读入的单词是否在 vector 中,若不在(返回尾后迭代器),才将单词加入 vector
而使用 set,检查是否重复的工作是由 set 模版负责的,程序员无须编写对应代码,程序简洁很多

更深层次的差别,vector 是无序线性表,find 查找指定值只能采用顺序查找方式,所花费的时间与 vector.size() 呈线性关系。而 set 是用红黑树实现的,花费的时间与 vector.size() 呈对数关系。当单词数量已经非常多时,set 的性能优势是巨大的
当然,vector 也不是毫无用处。它可以保持单词的输入顺序,而 set 则不能,遍历 set,元素是按值的升序被遍历的

2.2 关键字类型的要求

1、对于有序容器 ——map、 multi map、set以及multiset,关键字类型 必须定义 元素比较的方法。默认情况下,标准库 使用关键字类型的<运算符 来比较两个关键字

传递给排序算法的可调用对象(函数对象、函数指针 或 lambda表达式 作为参数,用于指定元素的比较方式)必须满足与关联容器中关键字一样的类型要求
可调用对象例子:sort(vec.begin(), vec.end(), Compare()); sort(vec.begin(), vec.end(), compare); sort(vec.begin(), vec.end(), [](int a, int b) { return a < b; });
例如,如果你有一个std::set<int>,那么传递给排序算法的比较函数 应该接受 两个int类型的参数,并返回一个bool值

2、有序容器的关键字类型:可以 向一个算法提供我们自己定义的比较操作,与之类似,也可以 提供自己定义的操作 来代替关键字上的<运算符。所提供的操作 必须在关键字类型上 定义一个严格弱序

一个严格弱序必须满足以下三个条件:
1)严格性:对于任意的元素 a,a 不能与自身相等。即,compare(a, a) 应返回 false
2)反对称性:如果 compare(a, b) 返回 true,那么 compare(b, a) 应返回 false。换句话说,如果 a 小于 b,则 b 不小于 a
3)传递性:如果 compare(a, b) 返回 true 且 compare(b, c) 返回 true,那么 compare(a, c) 应返回 true。换句话说,如果 a 小于 b 且 b 小于 c,则 a 小于 c

如果两个关键字是等价的(即,任何一个都不“小于等于”另一个),那么容器将它们视作相等 来处理。当用作map的关键字时,只能有一个元素 与这两个关键字关联,我们可以用两者中任意一个 来访问对应的值

2、使用关键字类型的比较函数:用尖括号指出 要定义哪种类型的容器,自定义的操作类型 必须在尖括号中 紧跟着元素类型 给出(例子见后)

在尖括号中出现的每个类型,就仅仅是一个类型而已。当我们创建一个容器(对象)时,才会以构造函数参数的形式 提供真正的比较操作(其类型 必须与 在尖括号中指定的类型相吻合,例子见后)

不能直接定义一个 Sales_data 的 multiset,因为Sales_data没有<运算符。但是,可以用 compareIsbn 函数来 定义一个multiset。此函数在sales_data对象的ISBN成员上 定义了一个 严格弱序

bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
	return lhs.isbn() < rhs.isbn();
}

为了使用 自己定义的操作,在定义multiset时 我们必须提供两个类型:关键字类型 Sales_data,以及 比较操作类型——应该是一种函数指针类型,可以指向compareIsbn。当定义 此容器类型的对象时,需要提供 想要使用的操作的指针
在本例中提供 一个指向compare Isbn的指针:

// bookstore 中多条记录 可以有相同的ISBN
// bookstore 中的元素 以ISBN的顺序进行排列
multiset<sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

使用decltype来指出 自定义操作的类型。当用decltype来获得 一个函数指针类型时,必须加上一个*来 指出我们要使用 一个给定函数类型的指针
用 compareIsbn 来初始化 bookstore 对象,这表示当我们向bookstore添加元素时,通过调用compareIsbn来为这些元素排序即,bookstore中的元素将 按它们的ISBN成员的值排序。可以用 compareIsbn 代替 &compareIsbn 作为构造函数的参数,因为当我们使用一个函数的名字时,在需要的情况下它会自动转化为一个指针
当然,使用&compare Isbn的效果也是一样的

不使用decltype 重新定义 bookstore:

using compareType = bool (*)(const Sales_data &lhs, const Sales_data &rhs);
// 等价于 typedef bool(*compareType)(const Sales_data &lhs, const Sales_data &rhs);
std::multiset<Sales_data, compareType> bookstore(compareIsbn);

定义一个变量,通过bookstore 的multiset 调用begin()来初始化这个变量。写出变量的类型

using compareType = bool (*)(const Sales_data &sales_data1, const Sales_data &sales_data2);
std::multiset<Sales_data, compareType> bookstore(compareIsbn);
std::multiset<Sales_data, compareType>::iterator bookstore_iter = bookstore.begin();

3、定义一个map,将单词与一个行号的list关联,list中保存的是单词所出现的行号
stringstream用法(两处合在一起)

#include <string>
#include <list>
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>

using namespace std;

void trans(string& s) {
	for (int i = 0; i < s.size(); i++) {
		if (s[i] >= 'A' && s[i] <= 'Z')
			s[i] += 'a' - 'A';
		if (s[i] == '.' || s[i] == ',')
			s.erase(i, 1);
	}
}

int main(int argc, char ** argv)
{
	if (argc != 2) {
		cerr << "segment ERROR" << endl;
		return -1;
	}
	ifstream ifs(argv[1]);
	if (!ifs) {
		cerr << "file load ERROR" << endl;
		return -1;
	}
	string s;
	int line = 0;
	map<string, list<int>> mymap;
	while (getline(ifs, s)) {
		line++;
		istringstream iss(s); // stringstream用法
		string word;
		while (iss >> word) { // stringstream用法
			trans(word);
			mymap[word].push_back(line);
		}
	}
	for (pair<string, list<int>> p : mymap) {
		cout << p.first << " ";
		for (int i : p.second) {
			cout << i << " ";
		}
		cout << endl;
	}
	return 0;
}

4、以定义一个vector::iterator 到 int 的map吗?list::iterator 到 int 的map呢
由于有序容器 要求关键字类型 必须支持比较操作 <,因此map<vector<int>::iterator, int> m1;是可以的,因为 vector 的迭代器支持比较操作
map<list<int>::iterator, int> m2;则不行,因为 list 的元素不是连续存储,其迭代器不支持比较操作

vector的迭代器 比较运算符的行为 是基于其在容器中的存储位置的大小关系。具体来说:如果一个迭代器 在另一个迭代器之前,那么它的存储位置在容器中 更靠前,因此它比另一个迭代器小
即:迭代器之间的比较能够反映出它们在容器中的相对位置关系

2.3 pair类型

1、名为pair的标准库类型,它定义在 头文件utility中

pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板:当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型;两个类型不要求一样

pair<string, size_t> word_count; // 保存一个string和一个size_t
pair<string,vector<int>> line;  // 保存string和vector<int>

2、pair的默认构造函数 对两个数据成员 进行值初始化。因此,line保存一个空string和一个空vector,word_count中的size_t成员值为0,而string成员 被初始化为空

也可以为每个成员提供初始化器:pair<string, string> author{ "James", "Joyce" };
这条语句 创建一个名为 author 的 pair,两个成员 被初始化为"James" 和 “Joyce”

3、与其他标准库类型不同,pair的数据成员是public的
两个成员分别命名为first和second。我们用普通的成员访问符号 来访问它们

for (const auto &w : word_count) // 对map中的每个元素
	// 打印结果
	cout << w.first << " occurs " << w.second
		 << ((w.second > 1) ? " times" : " time") << endl;

w是指向map中某个元素的引用。map的元素是pair

4、pair上的操作

操作解释
pair<T1, T2> p;p是一个pair,两个类型分别为T1 和 T2 的成员都进行了 值初始化
pair<T1, T2> p(v1, v2)p是一个成员类型为T1 和 T2 的pair:first 和 second成员 分别用v1 和 v2 进行初始化
pair<T1, T2> p = { v1, v2 };等价于 p(v1, v2)
make_pair (v1, v2)返回一个用v1 和 v2初始化的pair。pair的类型 从v1和v2的类型 推断出来
p.first返回p的名为first的(公有)数据成员
p.second返回p的名为second的(公有)数据成员
p1 relop p2关系运算符(<、>、<=、>=)按字典序定义:例如,当p1.first < p2.first!(p2.first < p1.first) && p1.second < p2.second 成立时,p1<p2 为true。关系运算利用元素的<运算符来实现
p1 == p2, p1 != p2当first和second成员 分别相等时,两个pair相等。相等性判断 利用元素的==运算符实现

5、创建pair对象的函数:想象有一个函数 需要返回 一个pair。在新标准下,我们可以对返回值 进行列表初始化

pair<string, int> process(vector<string> &v)
{
	// 处理V
	if (!v.empty())
		return { v.back(), v.back().size() }; // 列表初始化
	else
		return pair<string, int> ();          // 隐式构造空的pair返回值
}

在较早的C++版本中,不允许 用花括号包围的初始化器 来返回pair这种类型的对象 必须显式构造返回值

if (!v.empty())
	return pair<string, int> (v.back(), v.back().size());

还可以用 make_pair 来生成 pair对象,pair的两个类型 来自于make_pair的参数

if (!v.empty())
	return make_pair(v.back(), v.back().size());

分别采用不同的方法创建pair。例:编写程序,读入string和int的序列,将每个string 和 int存入pair中,pair 保存在一个vector中

#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main()
{
	string s;
	int n;
	pair<string, int> p;
	vector<pair<string, int>> vec;
	while (cin >> s >> n) {
		p = { s, n }; // 等价于 make_pair(s, n) / pair<string, int>(s, n)
		vec.push_back(p);
	}
	for (auto p : vec) {
		cout << p.first << " " << p.second << endl;
	}
	return 0;
}

3、关联容器操作

1、这些类型表示容器关键字和值的类型:关联容器额外的类型别名

类型别名解释
key_type此容器类型的关键字类型
mapped_type每个关键字关联的类型;只适用于map
value_type对于set,与key_type相同;对于map,为pair<const key_type,mapped_type>

每个元素是一个pair对象,包含一个关键字 和 一个关联的值

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

与顺序容器一样,我们 使用作用域运算符 来提取一个类型的成员

3.1 关联容器迭代器

1、当 解引用一个关联容器迭代器时,我们会得到一个类型为 容器的 value_type 的值的引用。对map而言,value_type是一个pair类型,其first成员 保存const的关键字,second成员 保存值:

map_it->first = "new key";
++map_it->second; // 正确:我们可以通过迭代器 改变元素

一个map的value_type是 一个pair,我们可以改变 pair的值,但不能改变 关键字成员的值

2、set的迭代器是const的:虽然set类型 同时定义了iterator和const_iterator类型,但两种类型都只允许 只读访问set中的元素,不能修改。与不能改变一个map元素的关键字一样,一个set中的关键字也是 const 的

set<int> iset = {0,1,2,3,4,5,6,7,8,9};
set<int>::iterator set_it = iset.begin();
if (set_it != iset.end()) {
	*set_it = 42; 			 // 错误:set中的关键字是只读的
	cout << *set_it << endl; // 正确:可以读关键字
}

3、遍历关联容器:map 和 set类型 都支持begin和end操作。与往常一样,我们可以用这些函数获取迭代器

// 获得一个指向首元素的迭代器
auto map_it = word_count.cbegin();
// 比较当前迭代器和尾后迭代器
while (map_it != word_count.cend()) {
	// 解引用迭代器,打印关键字-值对
	cout << map_it->first << " occurs "
		 << map_it->second << " times" << endl;
	++map_it; // 递增迭代器,移动到下一个元素
}

本程序的输出是 按字典序排列的。当使用一个迭代器 遍历一个map、multimap、set 或 multiset 时,迭代器 按关键字升序遍历元素

循环中map_it 的类型:map<string, size_t>::const_iterator map_it = word_count.cbegin();

4、关联容器和算法:不对关联容器 使用泛型算法。关键字是const 这一特性意味着 不能将关联容器传递给 修改或重排容器元素的算法,因为这类算法 需要向元素写入值,而set类型中的元素 是const的,map中的元素是 pair,其第一个成员是 const的

关联容器 可用于 只读取元素的算法。但是,很多这类算法 都要搜索序列。由于关联容器中的元素 不能通过 它们的关键字 进行(快速)查找,因此对其使用泛型搜索算法几乎总是个坏主意

可以用泛型find算法 来查找一个元素,但此算法 会进行顺序搜索。使用 关联容器定义的专用的find成员 会比调用 泛型find快得多

如果我们真要对一个关联容器使用算法,要么是将它当作一个源序列 要么当作一个目的位置
例如,可以用 泛型copy算法将元素 从一个关联容器拷贝到 另一个序列(源序列)
类似的,可以调用 inserter 将一个插入器 绑定到一个关联容器(目的位置)。通过使用inserter,我们可以将关联容器当作一个目的位置 来调用另一个算法

// 使用copy算法将关联容器的元素复制到vector中(源序列)
copy(mySet.begin(), mySet.end(), back_inserter(myVector));

// 将vector中的元素复制到set中(目的位置)
copy(vec.begin(), vec.end(), inserter(mySet, mySet.end()));

假定 c 是一个string的multiset,v 是一个string 的vector,解释下面的调用。指出每个调用是否合法

copy(v.begin(), v.end(), inserter(c, c.end())); 	// 合法
copy(v.begin(), v.end(), back_inserter(c)); 		// 非法,set中没有push_back()
copy(c.begin(), c.end(), inserter(v, v.end())); 	// 合法
copy(c.begin(), c.end(), back_inserter(v)); 		// 合法

5、初始化 map(或 multimap):使用一个map迭代器 编写一个表达式,将一个值赋予一个元素

#include <iostream>
#include <string>
#include <vector>
#include <map>

using namespace std;

int main() {
	 // map<int, int> m(10, 12); 这么初始化报错
	map<int, int> m = { {1,11} };
	auto iter = m.begin();
	iter->second = 10;
	cout << iter->first << " " << iter->second << endl;
	return 0;
}

初始化 map(或 multimap) 有多种方法,包括使用初始化列表、使用范围构造函数、使用迭代器范围、以及使用插入方法等
1)初始化列表:使用初始化列表 是最简单的方法,可以在创建 map对象时直接提供一组键值对。示例如下:

std::map<int, std::string> myMap = {
    {1, "one"},
    {2, "two"},
    {3, "three"}
};

2)范围构造函数:使用范围构造函数 可以从另一个容器(例如 vector或 initializer_list)初始化 map。示例如下:

std::vector<std::pair<int, std::string>> pairs = {
    {1, "one"},
    {2, "two"},
    {3, "three"}
};
std::map<int, std::string> myMap(pairs.begin(), pairs.end());

3)迭代器范围:可以 使用迭代器范围 初始化 map,其中包含了 另一个 map的一部分或全部元素。示例如下:

std::map<int, std::string> originalMap = {
    {1, "one"},
    {2, "two"},
    {3, "three"}
};
std::map<int, std::string> myMap(originalMap.begin(), originalMap.end());

4)逐个插入:可以逐个插入元素到 map中,使用 insert 或 emplace方法。示例如下:

std::map<int, std::string> myMap;
myMap.insert(std::make_pair(1, "one"));
myMap.insert(std::make_pair(2, "two"));
myMap.emplace(3, "three");

3.2 添加元素

1、关联容器的insert成员 向容器中添加一个元素 或 一个元素范围。由于 map和set(以及对应的无序类型)包含不重复的关键字,因此插入一个 已存在的元素对容器没有任何影响(去重)

vector<int> ivec = {2,4,6,8,2,4,6,8};    // ivec有8个元素
set<int> set2;                           // 空集合
set2.insert(ivec.cbegin(), ivec.cend()); // set2有4个元素
set 2.insert((1,3,5,7,1,3,5,7}); 		 // set2现在有8个元素

insert有两个版本,分别接受 一对迭代器,或是 一个初始化器列表,这两个版本的行为 类似对应的构造函数——对于一个给定的关键字,只有第一个 带此关键字的元素 才被插入到容器中

2、向map添加元素:对一个map 进行insert操作时,必须记住 元素类型是pair。通常,对于 想要插入的数据,并没有一个现成的pair对象。可以在insert的参数列表中创建一个pair:

// 向word_count 以pair的方式 插入word的4种方法
word_count.insert({word,1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word,1));
word_count.insert(map<string,size_t>::value_type(word,1)); // 相比三种方式 初始化pair 多的一种

3、关联容器insert操作

insert函数解释
c.insert(v), c.emplace(args)v是value_type类型的对象;args用来构造一个元素。对于map和set,只有当元素的关键字不在c中时才插入(或构造)元素。函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及 一个指示 插入是否成功的bool值。对于multimap 和 multiset,总会插入(或构造)给定元素,并返回一个指向新元素的迭代器
c.insert(b, e), c.insert(il)b和e是迭代器,表示一个c::value_type类型值的范围:il是这种值的花括号列表。函数返回void。对于map和set,只插入关键字不在c中的元素。对于multi_map 和 multiset,则会 插入范围中的每个元素
c.insert(p, v), c.emplace(p, args)类似insert(v) (或 emplace(args)),但将迭代器p 作为一个提示,指出从哪里开始搜索 新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素

4、检测insert的返回值:insert(或emplace)返回的值 依赖于 容器类型和参数
对于 不包含重复关键字的容器,添加 单一元素的insert和emplace版本 返回一个pair,告诉我们插入操作是否成功
pair的first成员 是一个迭代器,指向 具有给定关键字的元素;second成员 是一个bool值,指出元素 是插入成功 还是已经存在于容器中。如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为 false。如果关键字不存在,元素被插入容器中,且bool值 为true

用insert 重写单词计数程序

// 统计每个单词在输入中出现次数的一种更繁琐的方法
map<string, size_t> word_count; // 从string到size_t的空map
string word;
while (cin >> word) {
	// 插入一个元素,关键字等于word,值为1;
	// 若word已在word_count中,insert什么也不做
	auto ret = word_count.insert({word,1});
	// 等价于:pair<map<string,size_t>::iterator, bool> ret = word_count.insert(make_pair(word, 1));
	// pair不是容器,没有迭代器,注意返回类型
	if (!ret.second)		 // word已在word_count中
		++ret.first->second; // 递增计数器
}

5、展开递增语句:通过添加一些括号来反映出 运算符的优先级:++((ret.first) -> second); // 等价的表达式
ret 保存insert返回的值,是一个pair
ret.first 是pair的第一个成员,是一个map迭代器,指向 具有给定关键字的元素
ret.first-> 解引用此迭代器,提取map中的元素,元素也是一个pair
ret.first->second map中元素的值部分
++ret.first->second 递增此值

6、由于一个multi容器中的关键字 不必唯一,在这些类型上 调用insert 总会插入一个元素

multi_map<string, string> authors;
// 插入第一个元素,关键字为Barth,John
authors.insert({"Barth,John", "Sot-Weed Factor"});
// 正确:添加第二个元素,关键字也是Barth,John
authors.insert({"Barth,John", "Lost in the Funhouse"));

对允许 重复关键字的容器,接受单个元素的insert操作 返回一个指向新元素的迭代器
这里无须返回 一个bool值,因为 insert总是向这类容器中 加入一个新元素

7、给定一个map<string, vector>,对此容器的插入一个元素的insert版本,写出其参数类型和返回类型

map<string, vector<int>> m1;
pair<string, vector<int>> p1({"aaa", {1,2,3,4}});
pair<map<string, vector<int>>::iterator, bool> ret = m1.insert(p1);
// pair<map<string, vector<int>>::iterator, bool> ret = m1.insert({"aaa", {1,2,3,4}});

3.3 删除元素

1、关联容器定义了 三个版本的erase,与顺序容器一样,我们可以通过传递给 erase一个迭代器 或 一个迭代器对 来删除一个元素 或者 一个元素范围
这两个版本的 erase 与 对应的顺序容器的操作 非常相似:指定的元素被删除,函数返回void

关联容器 提供一个额外的erase操作,它接受一个key_type参数。此版本 删除所有匹配给定关键字的元素(如果存在的话),返回 实际删除的元素的数量

//删除一个关键字,返回删除的元素数量
if (word_count.erase(removal_word))
	cout << "ok:" << removal_word << "removed\n";
else cout << "oops:" << removal_word << "not found!\n";

对于 保存不重复关键字的容器,erase的返回值 总是0或1

2、从关联容器删除元素

函数解释
c.erase(k)从c中 删除每个关键字为k的元素。返回一个size_type值,指出 删除的元素的数量
c.erase§从c中删除迭代器p 指定的元素。p必须指向c中 一个真实元素,不能等于c.end()。返回 一个指向p之后元素的迭代器,若p指向 c中的尾元素,则返回 c.end()
c.erase(b, e)删除迭代器对b和e所表示的范围中 的元素。返回e

3.4 map的下标操作

1、map和unordered_map容器 提供了 下标运算符 和 一个对应的at函数。set类型 不支持下标,因为set中 没有与关键字相关联
的“值”。不能对一个 multimap 或一个 unordered_multimap 进行下标操作,因为这些容器中 可能有多个值与一个关键字相关联

map下标运算符 接受一个索引(即,一个关键字),获取 与此关键字相关联的值。但是,与其他下标运算符不同的是,如果关键字并不在map中,会为它 创建一个元素 并插入到map中,关联值将进行值初始化

map<string, size_t> word_count;// empty map
//插入一个关键字为Anna的元素,关联值进行值初始化;然后将1赋予它
word_count["Anna"] = 1;

map 和 unordered_map 的下标操作

下标操作解释
c[k]返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其进行值初始化
c.at(k)访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常

2、使用下标操作的返回值:通常情况下,解引用一个迭代器所返回的类型 与 下标运算符返回的类型 是一样的。但对map
则不然:当对一个map 进行下标操作时,会获得 一个mapped_type对象;但当解引用一个map迭代器时,会得到一个value_type对象

与其他下标运算符相同的是,map的下标运算符 返回一个左值,所以 既可以读也可以写元素

cout << word_count["Anna"];  // 用Anna作为 下标提取元素;会打印出1
++word_count["Anna"];		 // 提取元素,将其增1
cout << word_count["Anna"];  // 提取元素并打印它:会打印出2

3、定义一个map,然后写出一个可以用来对map进行下标操作的类型以及下标运算符将会返会的类型

对 map 进行下标操作,应使用其 key_type,即关键字的类型
而下标操作返回的类型是 mapped_type,即关键字关联的值的类型

示例如下:
map 类型:map<string, int>
用来进行下标操作的类型:string
下标操作返回的类型:int

#include <iostream>
#include <string>
#include <vector>
#include <map>

using namespace std;

int main()
{
	map<string, int> m{ {"ashergu", 23} };    // 两个大括号初始化
	map<string, int>::key_type k = "ashergu"; // key_type
	map<string, int>::mapped_type v = m[k];   // mapped_type
	cout << k << " " << m[k] << endl;
	return 0;
}

3.5 访问元素

1、关联容器 提供多种查找一个指定元素的方法。应该使用哪个操作 依赖于 我们要解决什么问题
如果我们所关心的 是一个特定元素是否已在容器中,可能find 是最佳选择。对于 不允许重复关键字的容器,可能 使用find 还是count没什么区别。但对于 允许重复关键字的容器,count还会做更多的工作:如果 元素在容器中,它还会统计 有多少个元素有相同的关键字
如果不需要计数,最好使用find

set<int> iset = {0,1,2,3,4,5,6,7,8,9};
iset.find(1); 	// 返回一个迭代器,指向key==1的元素
iset.find(11);  // 返回一个迭代器,其值等于iset.end()
iset.count(1);  // 返回1
iset.count(11); // 返回0

2、在一个关联容器中 查找元素的操作
lower_bound 和 upper_bound 不适用于 无序容器
下标 和 at操作 只适用于 非const的map 和 unordered_map

函数解释
c.find(k)返回一个迭代器,指向 第一个关键字为k的元素,若k不在容器中,则返回 尾后迭代器
c.count(k)返回关键字 等于k 的元素的数量。对于不允许重复关键字的容器,返回值 永远是0或1
c.lower_bound(k)返回一个迭代器,指向 第一个关键字 不小于k的元素
c.upper_bound(k)返回一个迭代器,指向 第一个关键字 大于k的元素
c.equal_range(k)返回一个迭代器pair,表示 关键字等于k的元素的范围。若k不存在,pair的两个成员 均等于c.end()

3、对map 使用find代替下标操作
使用下标操作 有一个严重的副作用:如果关键字 还未在map中,下标操作 会插入一个具有给定关键字的元素
只是想知道 一个给定关键字 是否在map中,而不想改变map。这样就不能 使用下标运算符来检查一个元素是否存在,在这种情况下,应该使用find:

if (word_count.find("foobar") == word_count.end()
	cout << "foobar is not in the map" << endl;

4、在multimap 或 multiset 中查找元素:最直观的方法 是使用find 和 count

string search_item("Alain de Botton");    // 要查找的作者
auto entries = authors.count(search_item);// 元素的数量
auto iter = authors.find(search_item);	  // 此作者的第一本书
while (entries) {						  // 用一个循环查找此作者的所有著作
	cout << iter->second << endl;
	++iter;								  // 前进到下一本书
	--entries;							  // 记录还剩多少本书
}

遍历一个 multimap 或 multiset 时,保证 可以得到序列中 所有具有给定关键字的元素

5、一种不同的,面向迭代器的 解决方法:可以用 lower_bound 和 upper_bound 来解决此问题。这两个操作都接受一个关键字,返回 一个迭代器

如果 关键字在容器中,lower_bound 返回的迭代器 将指向 第一个具有给定关键字的元素,而 upper_bound 返回的迭代器 则指向最后一个匹配给定关键字的元素 之后的位置
如果元素 不在multimap中,则lower_bound 和 upper_bound 会返回相等的迭代器 —— 指向 一个不影响排序的关键字 插入位置

因此,用相同的关键字 调用 lower_bound 和 upper_bound 会得到 一个迭代器范围,表示 所有具有该关键字的元素的范围

如果我们查找的元素具有 容器中最大的关键字,则此关键字的upper_bound 返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower_bound 返回的 也是尾后迭代器

// authors 和 search_item 的定义,与前面的程序一样
// beg 和 end 表示对应此作者的元素的范围
for (auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item);
	beg != end; ++beg)
	cout << beg->second << endl; // 打印每个题目

如果容器中 没有这样的元素,beg 将指向 第一个关键字大于 search_item 的元素,有可能是 尾后迭代器

如果 没有元素与给定关键字匹配,则lower_bound 和 upper_bound 会返回相等的迭代器 —— 都指向 给定关键字的插入点,能保持 容器中元素顺序的插入位置

6、equal_range函数:不必 再调用upper_bound 和 lower_bound,直接调用 equal_range 即可。此函数 接受一个关键字,返回一个迭代器 pair。若 关键字存在,则 第一个迭代器 指向第一个 与关键字匹配的元素,第二个迭代器 指向 最后一个匹配元素之后的位置。若 未找到匹配元素,则 两个迭代器 都指向关键字 可以插入的位置

// authors 和 search_item 的定义,与前面的程序一样
// pos保存 迭代器对,表示 与关键字匹配的元素范围
for (auto pos = authors.equal_range(search_item);
	 pos.first != pos.second; ++pos.first)
	cout << pos.first->second << endl; // 打印每个题目

没有 用局部变量beg 和 end 来保存元素范围,而是使用了equal_range返回的pair。此pair的first成员 保存的迭代器 与lower_bound返回的迭代器是一样的
second 保存的迭代器 与 upper_bound的返回值 是一样的

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <set>

using namespace std;

int main()
{
	multimap<string, string> m = { {"123", "1"}, {"123", "2"}, {"12", "13"}, {"123", "12"} };
	// 要改变数据结构:map<string, multiset<string>> 这样作者(map)和作品(multiset)都是有序的
	map<string, multiset<string>> m2;
	for (auto& p : m) {				  // 可以加引用
		m2[p.first].insert(p.second); // map通过下标添加元素,multiset通过insert添加元素
	}
	for (const auto& p : m2) {
		for (const auto& s : p.second) {
			cout << p.first << " " << s << endl;
		}
	}
	return 0;
}

3.6 一个单词转换的map

这个程序的功能是这样的:
给定一个string,将它转换为另一个string。程序的输入是两个文件。第一个文件保存的是一些规则,用来转换第二个文件中的文本
每条规则由两部分组成:一个 可能出现在输入文件中的单词和一个用来替换它的短语。表达的含义是,每当 第一个单词出现在输入中时,我们 就将它替换为对应的短语。第二个输入文件 包含要转换的文本

函数trans_word 管理整个过程。它接受两个 ifstream参数:第一个参数 应绑定到单词转换文件,第二个参数 应绑定到我们要转换的文本文件
函数buildMap 会读取转换规则文件,并创建一个map,用于保存每个单词 到其转换内容的映射
函数trans 接受一个string,如果存在转换规则,返回转换后的内容

*string.substr(1) 是一个字符串的方法调用,用于从字符串的第二个字符(索引从0开始)开始提取子字符串。具体而言,substr函数 接受一个参数,即要 提取的子字符串的起始位置(索引)。在这个程序中,参数为1,表示 从索引为1的位置开始提取子字符串,即 从字符串的第二个字符开始,跳过前面的空格

ifs >> s && getline(ifs, s2)  // 读取第一个单词 存入key中,行中剩余内容 存入value
// while循环使用一个bool变量isFirst来确定是否打印一个空格,只有第一个单词 前面不加空格

// 让s在map里面找,注意逻辑
// 检查s2除了空格 是不是真的有转换后的值(所以是1)

istringstream iss(line); 	  // 使用stringstream读取行中单词(把每个单词分开)
while (iss >> word)      	  // 使用stringstream读取行中单词(分开了还要有读入的部分)
#include <fstream>
#include <iostream>
#include <string>
#include <sstream>

using namespace std;

unordered_map<string, string> buildMap(ifstream& ifs) {
	unordered_map<string, string> um;
	string s, s2;
	// 读取第一个单词 存入key中,行中剩余内容 存入value
	while (ifs >> s && getline(ifs, s2)) {
		if (s2.size() > 1) {       // 检查s2除了空格 是不是真的有转换后的值(所以是1)
			um[s] = s2.substr(1);  // *
		}
		else
			throw runtime_error("no data for " + s);
	}
	return um;
}

void trans(string& s, unordered_map<string, string> &um) {
	// 让s在map里面找,注意逻辑
	auto it = um.find(s); 
	if (it != um.cend()) // find返回值跟顺序容器没区别,insert不一样
		s = it->second;  // map没有second,只有pair有second
}

void trans_word(ifstream& map_file, ifstream& s2) {
	map<string, string> um;
	um = buildMap(map_file);
	string line;
	while (getline(s2, line)) {
		istringstream  iss(line); // 使用stringstream读取行中单词(把每个单词分开)
		string word;
		// while循环使用一个bool变量isFirst来确定是否打印一个空格,只有第一个单词 前面不加空格
		bool isFirst = true;
		while (iss >> word) {     // 使用stringstream读取行中单词(分开了还要有读入的部分)
			if (isFirst)
				isFirst = false;
			else
				cout << " ";
			trans(word, um);
			cout << word;
		}
		cout << endl;
	}
}

int main()
{
	ifstream ifs("map_file.txt");
	ifstream ifs2("11.33_input.txt");
	trans_word(ifs, ifs2);
	return 0;
}

在buildMap中,如果进行如下改写,会有什么效果

trans_map[key] = value.substr(1);
//改为
trans_map.insert({key, value.substr(1)});

在这里没有影响,但是两个插入方式是有区别的:如果关键字出现多次,使用下标会重复赋值,最后保存的是最后一个值;使用insert只插入第一个

4、无序容器

1、新标准定义了 4个无序关联容器。这些容器不是使用 比较运算符 来组织元素,而是 使用一个哈希函数 和 关键字类型的==运算符。在关键字类型的元素 没有明显的序关系 的情况下,无序容器 是非常有用的。在某些应用中,维护元素的序代价 非常高昂,此时 无序容器 也很有用

2、使用无序容器:除了 哈希管理操作 之外,无序容器 还提供了与有序容器相同的操作(find、insert等)。这意味着 我们曾用于 map和set的操作 也能用于 unordered_map 和 unordered_set。类似的,无序容器 也有允许重复关键字的版本
因此,通常 可以用一个无序容器 替换对应的有序容器,反之亦然。但是,由于元素未按 顺序存储,一个使用无序容器的程序的输出(通常)会与使用有序容器的版本不同

// 统计出现次数,但单词不会按字典序排列
unordered_map<string, size_t> word_count;
string word;
while (cin >> word)
	++word_count[word];    		 // 提取并递增word的计数器
for (const auto&w : word_count)  // 对map中的每个元素
	// 打印结果
	cout << w.first << " occurs " << w.second
		 << ((w.second > 1) ? "times" : " time") << endl;

此程序与原程序的 唯一区别是 word_count的类型
对于每个单词,我们将得到相同的计数结果。但单词不太可能按字典序输出

3、管理桶:无序容器 在存储上组织为 一组桶,每个桶 保存零个 或 多个元素。无序容器 使用一个哈希函数 将元素映射到桶
为了访问一个元素,容器 首先计算元素的哈希值,它指出应该搜索哪个桶。容器 将具有一个特定哈希值的所有元素 都保存在相同的桶中。如果容器 允许重复关键字,所有 具有相同关键字的元素 也都会在同一个桶中。因此,无序容器的性能依赖于 哈希函数的质量 和 桶的数量和大小

对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将 每个特定的值 映射到 唯一的桶。但是,将不同关键字的元素 映射到相同的桶也是允许的
当一个桶 保存多个元素时,需要 顺序搜索这些元素 来查找我们想要的那个(开放寻址法:线性探测)。计算一个元素的哈希值 和 在桶中搜索 通常都是很快的操作

附:哈希 处理散列冲突的方法

1)开放寻址法
当一个元素的哈希位置已被占用时,开放寻址法 会尝试在哈希表中寻找另一个空闲位置。这个过程又叫探测。常见的探测方法有:
线性探测:顺序查找下一个空闲位置
二次探测:探测的间隔按二次方增加(1, 4, 9, …)
双重散列:使用第二个哈希函数确定探测序列

2)链地址法
链地址法 在每个哈希表的槽位上 存储一个链表。当多个元素映射到同一个槽位时,这些元素 会被添加到对应槽位的链表中。查找、插入和删除操作 都需要遍历这个链表。链地址法的一个变种是 使用其他数据结构(如红黑树)来提高特定场景下的性能

3)再散列
再散列 是指当哈希表的填充因子(即已存储的元素与总空间的比例)达到一定阈值时,增加 哈希表的容量,并将 所有元素重新散列到 新的哈希表中。这个过程 可以帮助减少碰撞 和 提高查找效率,但会暂时增加计算和内存成本

4)一致性哈希
主要用于 分布式系统中,一致性哈希算法 通过将数据和服务器映射到 同一个环状的哈希空间中来减少 因服务器增减而引起的重哈希操作。当系统的服务器数量变化时,只有 相对少量的数据需要重新分配,从而 提高系统的稳定性和分布均匀性。

实际应用
在实际应用中,选择合适的冲突解决策略取决于数据的特性和应用场景。例如,如果元素分布均匀,开放寻址法可能更高效;而如果哈希表的负载因子较高,链地址法可能表现得更好。在分布式系统中,一致性哈希可以有效地解决节点的动态增减问题

4、无序容器提供了一组管理桶的函数,这些成员函数 允许我们查询容器的状态 以及 在必要时 强制容器进行重组

无序容器管理操作
桶接口

函数解释
c.bucket_count()正在使用的桶的数目
c.max_bucket_count()容器能容纳的最多的桶的数量
c.bucket_size(n)第n个桶中有多少个元素
c.bucket(k)关键字为k的元素在哪个桶中

桶迭代

迭代器 / 函数解释
local_iterator可以用来访问 桶中元素的迭代器类型
const_local_iterator桶迭代器的const版本
c.begin(n), c.end(n)桶n的 首元素迭代器 和 尾后迭代器
c.cbegin(n), c.cend(n)与 前两个函数类似,但返回 const_local_iterator

哈希策略

函数解释
c.load_factor()每个桶的平均元素数量,返回float值
c.max_load_factor()c试图维护的 平均桶大小,返回float值。c会在需要时 添加新的桶,以使得 load_factor <= max_load_factor
c.rehash(n)重组存储,使得 bucket_count >= n 且 bucket_count > size / max_load_factor
c.reserve(n)重组存储,使得c可以保存n个元素 且不必rehash

5、无序容器 对关键字类型的要求:默认情况下,无序容器 使用关键字类型的==运算符 来比较元素,它们 还使用一个hash<key_type>类型的对象 来生成每个元素的哈希值
标准库为内置类型(包括指针)提供了 hash模板。还为 一些标准库类型,包括 string 和 智能指针类型 定义了hash。因此,我们 可以直接定义关键字 是内置类型(包括指针类型)、string 还是 智能指针类型 的无序容器

不能 直接定义关键字类型为自定义类类型 的无序容器。与容器不同,不能直接 使用哈希模板,而必须 提供我们自己的hash模板版本
类似于 为有序容器重载关键字类型的 默认比较操作。为了 能将 Sale_data 用作关键字,我们需要 提供函数 来替代 == 运算符 和 哈希值计算函数

size_t hasher(const Sales_data &sd)
{
	return hash<string>()(sd.isbn());
}
bool eqOp(const Sales_data &lhs, const Sales_data &rhs)
{
	return lhs.isbn() == rhs.isbn();
}

hasher函数 使用一个标准库hash类型对象 来计算ISBN成员的哈希值,该hash类型 建立在string类型之上

使用这些函数 来定义一个 unordered_multiset

using SD_multiset = unordered_multiset<Sales_data,
					decltype(hasher)*, decltype(eqOp)*>; 
// 参数是桶大小、哈希函数指针和相等性判断运算符指针
SD_multiset bookstore(42,hasher, eqOp);

简化 bookstore 的定义,首先为 unordered_multiset 定义了 一个类型别名,此集合的哈希 和 相等性判断操作 与hasher和eqOp函数有着相同的类型。通过 使用这种类型,在定义bookstore时 可以将我们希望它使用的函数的指针 传递给它

我们的类 定义了==运算符,则可以 只重载哈希函数

//使用 FooHash 生成 哈希值; Foo 必须有==运算符
unordered_set<Foo, decltype(FooHash)*> fooSet(10, FooHash);

6、用unordered_map重写 3.6中程序
把map换成 unordered_map 要加unordered_map头文件,除此之外没区别了

#include <fstream>
#include <iostream>
#include <unordered_map>
#include <string>
#include <sstream>

using namespace std;

unordered_map<string, string> buildMap(ifstream& ifs) {
	unordered_map<string, string> um;
	string s, s2;
	while (ifs >> s && getline(ifs, s2)) {
		if (s2.size() > 1) {
			um[s] = s2.substr(1);  // *
		}
		else
			throw runtime_error("no data for " + s);
	}
	return um;
}

void trans(string& s, unordered_map<string, string> &um) {
	auto it = um.find(s); 
	// unordered_map的find用法相对简单(就一种),主要是通过键来查找元素,并返回指向该元素的迭代器或end()迭代器
	if (it != um.cend())
		s = it->second;
}

void trans_word(ifstream& map_file, ifstream& s2) {
	unordered_map<string, string> um;
	um = buildMap(map_file);
	string line;
	while (getline(s2, line)) {
		istringstream  iss(line);  
		string word;
		bool isFirst = true;
		while (iss >> word) {     
			if (isFirst)
				isFirst = false;
			else
				cout << " ";
			trans(word, um);
			cout << word;
		}
		cout << endl;
	}
}

int main()
{
	ifstream ifs("map_file.txt");
	ifstream ifs2("11.33_input.txt");
	trans_word(ifs, ifs2);
	return 0;
}

附:关于find的补充:unordered_map / map

unordered_map的find用法相对简单(就一种),主要是通过键来查找元素,并返回指向该元素的迭代器或end()迭代器

1、unordered_map的find:

iterator find(const Key& key);

参数:key是要查找的键
返回值:如果找到了键为key的元素,find 返回一个指向该元素的迭代器。如果没找到,它返回一个指向 unordered_map容器末尾的迭代器(end()迭代器)

find函数仅接受一个参数,即要查找的键
它是一个非常高效的操作,时间复杂度在平均情况下是常数时间(O(1)),但在最坏情况下是线性时间(O(n)),其中n是容器中元素的数量。最坏情况发生的原因主要是由于哈希冲突
与std::map相比,unordered_map使用的是哈希表实现,而不是红黑树,因此在绝大多数情况下它提供更快的查找速度

2、unordered_map.find(s)find(unordered_map.begin(), unordered_map.end(), s) 有什么区别

unordered_map.find(s)find(unordered_map.begin(), unordered_map.end(), s)在功能上是相似的,它们都用来查找键为s的元素。但是,它们在实现和效率上有重要区别:

1)unordered_map.find(s)
这是std::unordered_map提供的成员函数。
直接利用哈希表的特性进行查找,因此查找效率非常高,平均时间复杂度是O(1)
当找到键为s的元素时,返回一个指向该元素的迭代器;如果未找到,则返回 unordered_map.end()

2)find(unordered_map.begin(), unordered_map.end(), s)
这实际上使用的是std::find算法,它是一个模板函数,定义在<algorithm>头文件中
std::find 对于 unordered_map 来说,并不直接适用,因为 unordered_map存储的是键值对(即std::pair<const Key, T>类型的元素),而不是单个键。如果你尝试用std::find来查找一个键,你需要提供一个完整的键值对作为参数,或者 使用std::find_if配合自定义比较函数
std::find和std::find_if通过遍历容器中的每个元素来查找,这意味着它的平均时间复杂度是O(n),其中n是容器中元素的数量。这比unordered_map.find(s)要低效得多

实际应用
当你想在unordered_map中查找一个键时,应该使用unordered_map.find(s),这样可以充分利用unordered_map的哈希表特性,实现快速查找
使用std::find(或std::find_if)遍历unordered_map是不合适的,除非你有特殊的需求,比如同时基于键和值的复杂条件查找,这时可能需要用到std::find_if加上自定义的比较函数

简而言之,对于unordered_map,find成员函数是查找键的最佳选择,因为它直接利用哈希表结构,提供了最优的查找效率
对于 map 也同理 当需要在std::map中根据键查找元素时,应该使用map.find(s),因为它能够提供更高的查找效率,利用了map的红黑树结构特性
std::find 对map来说并不直接适用,因为std::find期望的是遍历容器 寻找与 给定值相等的元素。而map存储的是键值对(即std::pair<const Key, ValueType>类型的元素),不是单个键。因此,直接使用std::find查找一个map中的键是不可行的,除非你构造一个键值对作为搜索目标
要基于键查找,你可能需要使用std::find_if结合自定义比较函数。即便如此,这种方法通过遍历整个map来查找元素,其时间复杂度是O(n),效率低于map.find(s)的O(log n)

  • 20
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值