11 关联容器

0. 概述

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

  • 关联容器支持高效的关键字查找和访问操作。2个主要的关联容器(associative-container)类型是 map 和 set。

    • map 中的元素是一些键值对(key-value):*关键字起索引作用,值表示与索引相关联的数据。
    • set 中每个元素只包含一个关键字,支持高效的关键字查询操作:检查一个给定关键字是否在 set 中。
  • 标准库提供了8个关联容器,它们之间的不同体现在三个方面:

    • 是 map 还是 set 类型。
    • 是否允许保存重复的关键字。
    • 是否按顺序保存元素。
  • 允许重复保存关键字的容器名字都包含单词 multi无序保存元素的容器名字都以单词 unordered 开头。

  • 关联容器类型

    • map 和 multimap 类型定义在头文件 map 中;
    • set 和 multiset 类型定义在头文件 set 中;
    • 无序容器定义在头文件 unordered_map 和 unordered_set 中。
  • map的关键字都是const,不能修改

  • 在一个map中,元素是关键字-值对。其中的每一个元素是一个pair对象,包含一个关键字和一个关联的值。也就是说,map中可以有很多元素,其每一个元素就是一个pair对象。

  • 不能修改 set 或 multiset 容器中元素的值。因为元素被修改后,容器并不会自动重新调整顺序,于是容器的有序性就会被破坏,再在其上进行查找等操作就会得到错误的结果。因此,如果要修改 set 或 multiset 容器中某个元素的值,正确的做法是先删除该元素,再插入新元素

  • 同理,也不能修改 map 和 multimap 容器中元素的关键字。

1. 使用关联容器

  • 类似顺序容器,关联容器也是模板。

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

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

使用map

  • 定义map时,必须指定关键字和值的类型
  • 从 map 中提取一个元素时,会得到一个pair类型的对象。
  • pair 是一个模板类型,保存两个名为 first 和 second 的公有数据成员
  • map 所使用的 pair 用 first 成员保存关键字,用 second 成员保存对应的值
// 统计每个单词在输入中出现的次数
map<string, size_t> word_count;     // string到size_t的空map
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;

使用set

  • 定义set时只需指明关键字类型
  • set 类型的 find 成员返回一个迭代器。如果给定关键字在 set 中,则迭代器指向该关键字,否则返回的是尾后迭代器end
// 统计输入中每个单词出现的次数
map<string, size_t> word_count;
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];

2. 关联容器概述

  • 关联容器(有序的和无序的)能够支持表9.2中的普通容器操作

  • 关联容器不支持顺序容器的位置相关的操作,例如push_front或push_back。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。
  • 关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。
  • 除了与顺序容器相同的操作之外,关联容器还支持一些顺序容器不支持的操作(表11.7)和类型别名(表11.3)。此外,无序容器还提供了一些用来调整哈希性能的操作。
  • 关联容器的迭代器都是双向的。

2.1 定义关联容器

初始化map或set

    • 定义 map 时,必须指定关键字类型和值类型;定义 set 时,只需指定关键字类型,因为 set 中没有值。
  • 初始化 map 时,提供的每个键值对用花括号 {} 包围:{key, value}。
  • 每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。
  • 我们也可以将关联容器初始化为另一个同类型容器的拷贝,或是从一个值范围来初始化关联容器,只要这些值可以转化为容器中所需类型就可以。在新标准下,我们也可以对关联容器进行值初始化:
map<string, size_t> word_count;   // 空容器
// 列表初始化
set<string> exclude = { "the", "but", "and" };
// 三个元素; authors将姓映射为名
map<string, string> authors =
{
    {"Joyce", "James"},
    {"Austen", "Jane"},
    {"Dickens", "Charles"}
};
  • 与以往一样,初始化器必须能转换为容器中元素的类型。
    • 对于set,元素类型就是关键字类型:一个关键字就是一个元素
    • 对于map,元素类型就是pair类型。我们将每个关键字-值对包围在花括号中{key,value}来指出它们一起构成了map中的一个元素:一个键值对就是一个元素。如anthors初始化时包括三个元素。在每个花括号中,关键字是第一个元素,值是第二个。

初始化multimap或multiset

  • map或set中的关键字必须是唯一的,即,对于一个给定的关键字,只能有一个元素的关键字等于它。
  • multimap和multiset没有此限制,它们都允许多个元素具有相同的关键字。如一个特定的单词可具有多个与之关联的词义
/* 具有唯一关键字的容器与允许重复关键字的容器之间的区别*/
// 定义一个有20个元素的vector,保存0到9每个整数的两个拷贝
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++i) {
	ivec.push_back(i);
	ivec.push_back(i); // 每个数重复保存一次
}
// iset包含来自ivec的不重复的元素; miset包含所有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
cout << miset.size() << endl;	// 打印出20
  • map 和 set 中的关键字必须唯一,multimap 和 multiset 没有此限制。

2.2 关键字类型的要求

  • 对于有序容器(map、multimap、set 和 multiset),关键字类型必须定义元素比较的方法

  • 默认情况下,标准库使用关键字类型的 < 运算符来进行比较操作

  • 传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求。

  • 也可以自定义关键字类型的比较操作:

    • 所提供的操作必须在关键字类型上定义一个严格弱序。可以将严格弱序看作“小于等于”,虽然实际定义的操作可能是一个复杂的函数。
    • 无论我们怎样定义比较函数,它必须具备如下基本性质:
      • 两个关键字不能同时“小于等于”对方:如果k1“小于等于”k2,那么k2绝不能“小于等于”k1.
      • 如果k1“小于等于”k2,且k2“小于等于”k3,那么k1必须“小于等于”k3
      • 如果存在两个关键字,任何一个都不“小于等于”另一个,那么我们称这两个关键字是“等价”的。如果k1“等价与”k2,且k2“等价于”k3,那么k1必须“等价于”k3.
      • 如果两个关键字是等价的(即,任何一个都不“小于等于”另一个),那么容器将它们视作相等来处理。当用作map关键字时,只能有一个元素与这个关键字关联,我们可以用两者中任意一个来访问对应的值。

使用关键字的比较函数

  • 传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求。

  • 在实际编程中,重要的是,如果一个类型定义了“行为正常”的 < 运算符,则它可以用作关键字类型。

  • 用来组织容器元素的操作的类型也是该容器类型的一部分。如果需要使用自定义的比较操作,则必须在定义关联容器类型时提供此操作的类型操作类型在尖括号中紧跟着元素类型给出

/*
例如,我们不能直接定义一个Sales_data的multiset,因为Sales_data没有<运算符。
但是,可以用定义的compareIsbn函数来定义一个multiset。
此函数在Sales_data对象的ISBN成员上定义了一个严格弱序。
函数compareIsbn应该像下面这样定义:
*/
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
    return lhs.isbn() < rhs.isbn();
}

/*
为了使用自己定义的操作,在定义multiset时我们必须提供两个类型。
关键字类型Sales_data,以及比较操作类型(应该是一种函数指针类型),可以指向compareIsbn。
当定义此容器类型的对象时,需要提供想要使用的操作的指针。
在本例中,我们提供一个指向compareIsbn的指针:
*/
// bookstore中多条记录可以有相同的ISBN
// bookstore中的元素以ISBN的顺序进行排列
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

2.3 pair类型:map的每个元素都是pair类型,first成员为关键字,second成员为值

  • pair 定义在头文件 utility 中。
  • 一个 pair 可以保存两个数据成员,分别命名为 first 和 second。
  • 类似容器,pair是一个用来生成特定类型的模板。当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型。两个类型不要求一样:
pair<string, string> anon;        // 保存两个string
pair<string, size_t> word_count;  // 保存一个string和一个size_t
pair<string, vector<int>> line;   // 保存string和vector<int>
/*
pair 的默认构造函数对数据成员进行值初始化。
因此,anon是一个包含两个空string的pair
line保存一个空string和一个空vector
word_count中的size_t 成员值为0
string成员被初始化为空vector。
*/
  • pair 的默认构造函数对数据成员进行值初始化。

  • 我们也可以为每个成员提供初始化器:

//创建了一个名为author的pair,两个成员被初始化为"James"和"Joyce"
pair<string,string> author{"James","Joyce"};
  • pair 支持的操作:

  • 在C++11中,如果函数需要返回 pair,可以对返回值进行列表初始化(花括号包围)。

pair<string, int> process(vector<string> &v)
{
    // 处理v
    if (!v.empty())
        // 列表初始化
        return { v.back(), v.back().size() };
    else
        // 隐式构造返回值
        return pair<string, int>();
}
  • 我们还可以用make_pair来生成pair对象,pair的两个类型来自于make_pair的参数:
if(!v.empty())
    return make_pair(v.back(),v.back().size());
  • 在较早的C++版本中,不允许用花括号包围的初始化器来返回pair这种类型的对象,必须显示构造返回值:
if(!v.empty())
    return pair<string,int>(v.back(),v.back().size());

3. 关联容器操作

  • 关联容器定义了类型别名来表示容器关键字和值的类型:

  • 对于 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<string,int>::key_type
  • 另外,只有 map 类型(unordered_map、unordered_multimap、multimap、map)才定义了 mapped_type

3.1 关联容器迭代器

  • 解引用关联容器迭代器时,会得到一个类型为容器的 value_type 的引用。
    • 对 map 而言,value_type 是 pair 类型,其 first 成员保存 const 的关键字,second 成员保存值。
// 获得指向word_count中一个元素的迭代器
auto map_it = word_count.begin();
// *map_it是指向一个pair<const string, size_t>对象的引用
cout << map_it->first;          // 打印此元素的关键字
cout << " " << map_it->second;  // 打印此元素的值
map_it->first = "new key";      // 错误:关键字是const的
++map_it->second;               // 正确·我们可以通过迭代器改变元素
  • 注意:必须记住,一个map的value_type是一个pair,我们可以改变pair的值,但不能改变关键字成员的值。
  • 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;    // 正确:可以读关键字
}

遍历关联容器

  • map 和 set 都支持 begin 和 end 操作。使用迭代器遍历 map、multimap、set 或 multiset 时,迭代器按关键字升序遍历元素
// 获得一个指向首元素的迭代器
auto map_it = word_count.cbegin();
// 比较当前迭代器和尾后迭代器
while (map_it != word_count.cend()) {
// 解引用迭代器,打印关键字-值对
	cout << map_it->first << " occurs "
		<< map_it->second << " times" << endl;
	++map_it; // 递均迭代器, 移动到下一个元素
}
// 输出是按字典序排列的
  • 关联容器和泛型算法:通常不对关联容器使用泛型算法
    • 关键字const这一特性意味着不能将关联容器传递给修改或重排序容器元素的算法,因为这类算法需要向元素写入值,而set类型是的元素是const的,map中元素是pair,其第一个成员是const的。
    • 关联容器可用于只读取元素的算法。但是,很多这类型算法都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行查找,因此对其使用泛型搜索算法几乎总是个坏主意。例如,关联容器定义了一个名为find的成员,它通过一个给定的关键字直接获取元素。我们可以用泛型find算法来查找一个元素,但此算法会进行顺序搜索。使用关键字容器定义的专用的find成员会比调用泛型find快得多
    • 在实际编程中,如果我们真要对一个关联容器使用算法,要么是将它当作一个源序列,要么当作一个目的位置。例如,可以用泛型copy算法将元素从一个关联容器拷贝到另一个关联容器。通过使用inserter,我们可以将关联容器当作一个目的位置来调用另一个算法。

3.2 添加元素:insert、emplace

  • 使用 insert 成员可以向关联容器中添加元素。
  • 由于map和set(以及对应的无序类型)包含不重复的关键字,因此向 map 和 set 中添加已存在的元素对容器没有影响(即不会真的添加)
  • 关联容器的 insert 操作:
    • insert有两个版本,分别接受一对迭代器,或是一个初始化列表
  • 向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个元素
set2.insert({1,3,5,7,1,3,5,7}); //set2现在有8个元素
  • 向map添加元素
  • 对一个map进行insert操作时,必须记住元素类型是pair。
  • 通常情况下,对于想要添加到 map 中的数据,并没有一个现成的 pair 对象。可以直接在 insert 的参数列表中创建 pair。
    • 在新标准下,创建一个pair最简单的方法是在参数列表中使用花括号初始化。
    • 也可以调用make_pair或显式构造pair。
// 向word_count插入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));
  • insert 或 emplace 的返回值依赖于容器类型和参数:以单词计算程序为例
    • 对于不包含重复关键字的容器,添加单一元素的 insert 和 emplace 版本返回一个 pair,表示操作是否成功。
      • pair 的 first 成员是一个迭代器,指向具有给定关键字的元素;
      • second 成员是一个 bool 值。
        • 如果关键字已在容器中,则 insert 直接返回,bool 值为 false
        • 如果关键字不存在,元素会被添加至容器中,bool 值为 true
    • 对于允许包含重复关键字的容器,添加单一元素的 insert 和 emplace 版本返回指向新元素的迭代器。
//统计每个单词在插入中出现次数的一种繁琐的写法
map<string,size_t> word_count; //从string到size_t 的map
string word;
whle(cin>>word)
{
    //插入一个元素,关键字等于word,值为1
    //若word已经在word_count中,insert什么也不做
    auto ret=word_count.insert({word,1});
    if(!ret.second)
        ++ret.first->second;
    }
/*
对于每个word,我们尝试将其插入到容器中,对应的值为1,
若word已在map中,则什么都不做,特别是与word相关联的计数器的值不变。
若word还未在map中,则此string对象被添加到map中,且其计数器的值被置为1.

if语句检查返回值的波bool部分,若为false,则表明插入操作未发生,
在此情况下,word已存在与word_count中,因此必须递增此元素所关联的计数器
*/
  • 向multiset或multimap添加元素——以作者和他所著书籍为例
/*
我们的单词计算程序依赖于这样一个事实:一个给定的关键字只能出现一次。
这样,任意给定的单词只有一个关联的计数器,我们有时希望能添加具有相同关键字的多个元素。
例如,可能想建立作者到他的所著书籍题目的映射。
在此情况下,每个作者可能有多个条目,因此我们应该使用multimap而不是map。
由于一个multi容器中关键字不必唯一,在这些类型上调用insert总会插入一个元素:
*/
multimap<string,string> authors;
//插入一个元素,关键字为Barth,John
authors.insert({"Barth,John"," Sot-weet Factor"});
//正确:添加第二个元素,关键字也是Barth,John
authors.insert({"Barth,John","Lost in the Funhouse"});
  • 对允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素的迭代器,这里无须返回一个bool值,因为insert总是向这类容器中加入一个新元素

3.3 删除元素:erase

  • 关联容器的删除操作:三个版本
  • 与顺序容器一样,我们可以通过传递erase一个迭代器或一个迭代器对来删除一个元素或一个元素范围
  • 与顺序容器不同,关联容器提供了一个额外的 erase 操作。它接受一个 key_type 参数,删除所有匹配给定关键字的元素(如果存在),返回实际删除的元素数量
/删除一个关键字,返回删除的元素数量
if(word_count.erase(removal_word))
    cout<<"ok:"<<removal_word<<" removed\n";
else
    cout<<" oops:"<<removal_word<<" not fount!\n";
  • 对于不包含重复关键字的容器,erase 的返回值总是1或0。若返回值为0,则表示想要删除的元素并不在容器中。
  • 对允许重复关键字的容器,删除元素的数量可能大于1:
auto cnt=authors.erase("Barth,John");
// 如果authors是我们创建的multimap,则cnt为2.

3.4 map的下标操作:只适用非const的map和unordered_map,没有关键字时会插入;如果不想插入不要用下标,用find

  • map 和 unordered_map 的下标操作:

  • set类型不支持下标,因为set中没有与关键字相关联的“值”,元素本身就是关键字。

  • 不能对一个multimap和unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。

  • map 下标运算符接受一个关键字,获取与此关键字相关联的值。如果关键字不在容器中,下标运算符会向容器中添加该关键字,并值初始化关联值

map<string,size_t> word_count;
//插入一个关键字为Anna的元素,关联值进行值初始化;然后将1赋予它
word_count["Anna"]=1;
/*
将会执行如下操作:
在word_count中搜索关键字为Anna的元素,未找到
将一个新的关键字插入到word_count中。关键字是一个const string,保存Anna,值进行值初始化。在本例中意味着值为0
提取出新插入的元素,并将值1赋予它。
*/
  • 由于下标运算符可能向容器中添加元素,所以只能对非 const 的 map 使用下标操作
  • 注意:对一个map使用下标操作,其行为与数组或vector上的下标操作很不相同:
    • 使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到map中。
  • 使用下标操作的返回值
    • 与 vector 与 string 等的下标运算符不同,map 的下标运算符返回的类型与解引用 map 选代器得到的类型不同。
    • 对 map 进行下标操作时,返回的是 mapped_type 类型的对象;解引用 map 迭代器时,返回的是 value_type 类型的对象。
    • 与其他下标运算符相同的是,map的下标运算符返回一个左值。由于返回的是一个左值,所以我们既可以读也可以写元素:
cout<<word_count[“Anna"];//用Anna作为下标提取元素;会打印出1
++word_count["Anna"];    //提取元素,将其增1
cout<<word_count["Anna"];  //提取元素并打印它,会打印2

3.5 访问元素:find/count/lower_bound/upper_bound

  • 关联容器的查找操作:提供多种查找一个指定元素的方法

  • 应该使用哪个操作依赖于我们要解决什么问题:

    • 如果我们所关心的只不过是一个特定元素是否已在容器中,可能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
  • 对map使用find代替下标操作
    • 对map和unordered_map类型,下标运算符提供了最简单的提取元素的方法,但有一个严重的副作用:如果关键字还未在map中,下标操作会插入一个具有给定关键字的元素。
    • 但有时,我们只想知道一个给定关键字是否在map中,而不想改变map。这样就不能使用下标运算符来检查一个元素是否存在,因为如果关键字不存在的话,下标运算符会插入一个新元素。在这种情况下,应该使用find:
if(word_count.find("foobar")==word_count.end())
cout<<"foobar is not in the map"<<endl;
  • 如果 multimap 或 multiset 中有多个元素具有相同关键字,则这些元素在容器中会相邻存储。例如,给定一个从作者到著作题目的映射,我们可能想打印一个特定作者的所有著作。可以用三种不同方法来解决这个问题:
  • (1)最直观的方法是使用find和count——find和count配合使用:find返回第一个符合元素,count返回符合元素的数量
string search_item("Alain de Botton");      // author we'll look for
auto entries = authors.count(search_item);  // number of elements
auto iter = authors.find(search_item);      // first entry for this author
// loop through the number of entries there are for this author
while(entries)
{
    cout << iter->second << endl;   // print each title
    ++iter;      // advance to the next title
    --entries;   // keep track of how many we've printed
}
/*
首先调用count确定此作者共有多少本著作,并调用find获得一个迭代器,指向第一个关键字为此作者的元素。
for循环的迭代次数依赖于count的返回值。特别是,如果count返回0,则循环一次也不执行。

当我们遍历一个 multimap 或 multiset 时,保证可以得到序列中所有具有给定关键字的元素
*/
  • (2)一种不同的,面向迭代器的解决方法:调用upper_bound和lower_bound
    • lower_bound 和 upper_bound 操作都接受一个关键字,返回一个迭代器。
      • 如果关键字在容器中,lower_bound 返回的迭代器会指向第一个匹配给定关键字的元素,而 upper_bound 返回的迭代器则指向最后一个匹配元素之后的位置。
      • 如果关键字不在 multimap 中,则 lower_bound 和 upper_bound 会返回相等的迭代器,指向一个不影响排序的关键字插入位置。
      • 因此用相同的关键字调用 lower_bound 和 upper_bound 会得到一个迭代器范围,表示所有具有该关键字的元素范围。
    • lower_bound 返回的迭代器可能指向一个具有给定关键字的元素,但也可能不指向。如果关键字不在容器中,则 lower_bound 会返回关键字的第一个安全插入点(不影响容器中元素顺序的插入位置)
    • lower_bound 和 upper_bound 有可能返回尾后迭代器。如果查找的元素具有容器中最大的关键字,则 upper_bound 返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则 lower_bound 也返回尾后迭代器。
    • 如果 lower_bound 和 upper_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;    // 打印每个题目
  • (3)最直接方法:直接调用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;  // 打印每个题目

4. 无序容器

  • 新标准库定义了4个 无序关联容器(unordered associative container),这些容器使用 哈希函数(hash function) 和关键字类型的 == 运算符组织元素。

  • 在关键字类型的元素没有明显有序关系的情况下,无序容器是非常有用的

  • 虽然理论上哈希技术能获得更好的平均性能,但在实际中想要达到很好的效果还需要进行一些性能测试和调优工作。因此,使用无序容器通常更为简单。

  • 如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器。

  • 除了哈希管理操作之外,无序容器还提供了与有序容器相同的操作(find、insert等)。这意味着我们曾用于map和set的操作也能用于unordered_map和unordered_set。类似的,无序容器也有允许重复关键字的版本

  • 无序容器和对应的有序容器通常可以相互替换。但是由于元素未按顺序存储,使用无序容器的程序输出一般会与有序容器的版本不同。

  • 无序容器在存储上组织为一组桶,每个桶保存零或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。因此无序容器的性能依赖于哈希函数的质量和桶的数量及大小。

  • 管理桶

    • 无序容器在存储上组织为一组桶,每个桶保存零或多个元素。
      • 无序容器使用一个哈希函数将元素映射到桶
      • 为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。
      • 容器将具有一个特定哈希值的所有元素都保存在相同的桶中。
      • 如果容器允许重复关键字,所有具有相同的关键字的元素也都会在同一个桶中。
      • 因此无序容器的性能依赖于哈希函数的质量和桶的数量及大小。
    • 对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。
    • 当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量比较操作。
  • 无序容器提供了一组管理桶的函数。如表所示。这些成员函数允许我们查询容器的状态以及在必要时强制容器进行重组。无序容器管理操作

  • 无序容器对关键字类型的要求

    • 默认情况下,无序容器使用关键字类型的==运算符比较元素,还使用一个 hash<key_type> 类型的对象来生成每个元素的哈希值。
    • 标准库为内置类型(包括指针)提供了hash模板。还为一些标准库类型,包括string和智能指针类型定义了hash。因此,我们可以直接定义关键字是内置类型(包括指针类型)、string还是智能指针的无序容器。
    • 但是,我们不能直接定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供我们自己的hash模板版本。
    • 我们不能使用默认的hash,而是使用另一种方法,类似于为有序容器重载关键字类型的默认比较操作。为了能将Sales_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类型之上。类似的,eqOp函数通过比较ISBN号来比较两个Sales_data
我们使用这些函数来定义一个unordered_multiset
*/
		
using SD_multiset=unordered_multiset<Sales_data,decltypr(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);
posted @ 2021-05-31 13:21  夏目的猫咪老师  阅读(81)  评论(0编辑  收藏  举报