[C++ Primer] : 第11章: 关联容器

使用关联容器

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

关联容器支持高效的关键字查找和访问. 有两个主要的关联容器类型: map和set.
map: map中的元素是一些关键字—值(key—value)对, 关键字起到索引的作用, 值则表示与索引相关联的数据.
set: set中每个元素只包含一个关键字, set支持高效的关键字查询操作——检查一个给定的关键字是否在set中.

标准库提供了8个关联容器, 这8个容器间的不同体现在3个方面:

  • 或者是一个set, 或者是一个map;
  • 是否允许重复的关键字;
  • 按顺序保存元素, 或无序保存.

允许重复关键字的容器的名字中都包含单词multi, 不保持关键字按顺序存储的容器的名字都以单词unordered开头. 无序容器使用哈希函数来组织元素. 有序容器可以使用比较运算符组织元素.
类型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, 关键字可以重复出现

注意: 关联容器分为有序的关联容器和无序的关联容器(区别在于有没有unordered前缀). 有序关联容器利用了比较运算符(< > <= >=)来组织元素, 因此其保存顺序是有序的; 无序关联容器则使用哈希函数和 == 运算符来组织元素, 其元素的保存顺序是无序的.

使用map

string line("abc abc def ghk ghk"), word; // line保存的是以空格分割的单词
istringstream iss(line);
map<string, size_t> word_cnt;
while(iss >> word)
    ++word_cnt[word];

for(auto i : word_cnt)
    cout << i.first << " occurs " << i.second
         << (i.second > 1 ? " times" : " time") << endl;

使用set

string line("aa abc bb abc def cc ghk ghk"), word; // line保存的是以空格分割的单词
istringstream iss(line);
map<string, size_t> word_cnt;
set<string> exclude = {"aa", "bb", "cc"}; // 统计时需要排除的单词集合
while(iss >> word)
    if(exclude.find(word) == exclude.end()) // 只统计不在exclude中的单词
        ++word_cnt[word];

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

关联容器概述

关联容器的迭代器都是双向的.
每个关联容器都定义了一个默认构造函数, 它创建一个空的指定类型的容器. 新标准下也可以对关联容器进行值初始化.
当初始化一个map时, 必须提供关键字类型和值类型, 每个关键字-值对包围在花括号中: {key, value}.
map和set中的关键字必须是唯一的, 即对于一个给定的关键字, 只能有一个元素的关键字等于它. 容器multimap和multiset没有此限制, 它们允许多个元素具有相同的关键字.
map和set中的关键字都是const的, 即不能该改变关键字.

对关键字类型的要求
默认情况下, 无序容器使用关键字类型的 == 运算符来比较元素, 它们还是用一个hash<key_type>类型的对象来生成每个元素的哈希值. 标准库为内置类型(包括指针)提供了hash模板, 还为一些标准库类型, 包括string和智能指针类型定义了hash, 因此可以直接定义关键字是内置类型(包括指针类型), string还是智能指针类型的无序容器. 但是我们不能直接定义关键字类型为自定义类型的无序容器. 与容器不同, 不能直接使用哈希模板, 而必须提供自己的hash模板版本.
对于有序容器——map, multimap, set以及multiset, 关键字类型必须定义元素比较的方法. 默认情况下, 标准库使用关键字类型的 < 运算符来比较两个关键字, 因此关键字必须重载 < 运算符.
用来组织一个容器中元素的操作的类型也是该容器类型的一部分, 为了指定使用自定义的操作, 必须在定义关联容器类型时提供此操作的类型. 用尖括号指出要定义那种类型的容器, 自定义的操作类型必须在尖括号中紧跟着元素类型给出.
在尖括号中出现的每个类型, 就仅仅一个类型而已, 当我们创建一个容器(对象时), 才会以构造函数参数的形式提供真正的比较操作(其类型必须与在尖括号中指定的类型相吻合).
如我们不能直接定义一个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. 代码如下所示:

multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

重载比较操作符, 注意使用decltype来获得一个函数指针类型时, 必须加上一个 * 来指出我们要使用一个给定函数类型的指针.
用compareIsbn来初始化bookstore对象, 表示当我们向bookstore添加元素时, 通过调用compareIsbn来为这些元素排序. 可以用compareIsbn代替&compareIsbn作为构造函数的参数, 因为当我们使用一个函数的名字时, 在需要的情况下它会自动转化为一个指针, 当然, 用&compareIsbn的效果是一样的.

pair类型
pair类型定义在utility头文件中.
一个pair保存两个数据成员. pair是一个用来生成特定类型的模板. pair的两个类型名不要求一致. pair的默认构造函数对数据成员进行值初始化.
pair的数据成员是public的, 两个成员分别命名为first和second.

|pair上的操作 |
|------------------------------|---------------------------------------
|pair<T1, T2> p; |p是一个pair, 两个成员类型为T1和T2, 进行值初始化
|pair<T1, T2> p(v1, v2); |两个成员分别用v1和v2初始化
|pair<T1, T2> p = {v1, v2};
|make_pair(v1, v2); |返回一个v1和v2初始化的pair. pair的类型从v1和v2的类型推断出来.
|p.first |返回p的名为first的公有数据成员
|p.second |返回p的名为second的公有数据成员
|p1 relop p2 |关系运算符(<, >, <=, >=)按字典顺序定义. 依次比较first和second的大小
|p1 == p2 |first和second成员分别相等时两个pair相等.
|p1 != p2

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

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

关联容器操作

关联容器额外的类型别名
key_type
mapped_type
value_type

对于set类型, key_type和value_type是一样的, set中保存的值就是关键字.
对于map类型, 元素是关键字—值对, 即每个元素是一个pair对象, 包含一个关键字和一个关联的值. 由于我们不能改变一个元素的关键字, 因此这些pair的关键字部分是const的.
必须记住, 一个map的value_type是一个pair, 可以改变pair的值, 但是不能改变map中关键字成员的值.
关联容器迭代器
当解引用一个关联容器迭代器时, 会得到一个类型为容器的value_type的值的引用, 对map而言是一个pair类型, 其first成员保存const的关键字, second成员保存值.
set的迭代器是const的
虽然set类型同时定义了iterator和const_iterator两种类型, 但两种类型都只允许只读访问set中的元素. 一个set中的关键字也是const的.
遍历关联容器
当使用一个迭代器遍历一个map, multimap, set或multiset时, 迭代器按关键字升序遍历元素.
关联容器和算法
通常不对关联容器使用泛型算法, 关键字是const这一特性意味着不能将关联容器传递个修改或重排容器元素的算法, 因为这类算法通常需要向元素写入值, 而set和map中的关键字是const的.
关联容器可用于只读算法. 使用关联容器定义的专用的find成员会比调用泛型find快得多.

添加元素
关联容器的insert成员向容器添加一个元素或一个元素范围.由于map和set不含重复关键字, 因此插入一个已存在的元素对容器没有任何影响.
向set添加元素

vector<int> ivec{1, 2, 3, 2, 1, 4};
set<int> iset;
iset.insert(ivec.cbegin(), ivec.cend());
iset.insert({3, 4, 5, 2, 3});

向map添加元素
对一个map进行insert操作时, 必须记住元素类型是pair. 插入元素的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最简单的方法是在参数列表中使用花括号初始化.

关联容器的insert操作
c.insert(v)
c.emplace(args)
c.insert(b, e)
c.insert(il)
c.insert(p, v)
c.emplace(p, args)

11.3.3 删除元素
关联容器定义了3个版本的erase. 与顺序容器一样, 可以通过传递给erase一个迭代器或一个迭代器对来删除一个元素或一个元素范围. 除此之外, 关联容器还提供一个额外的erase操作, 它接受一个key_type参数. 此版本删除所有匹配给定关键字的元素(如果存在的话), 并返回实际删除的数量. 对于不允许关键字重复的关联容器, erase返回值为0或1, 若返回0, 则表明要删除的元素不在容器中. 对允许重复关键字的关联容器, 返回值可大于1.

从关联容器删除元素
c.erase(k)
c.erase(p)
c.erase(b, e)

11.3.4 map的下标操作
map和unordered_map容器提供下标运算符和一个对应的at函数. set类型不支持下标, 因为set中没有与关键字相关联的值, 元素本身就是值, 因此获取一个与关键字相关联的值的操作就没有意义.
multimap和unordered_multimap也不支持下标操作, 因为多个值可能与一个关键字相关联.
对map使用下标运算符, 如果关键字不在map中, 则会为它创建一个元素并插入到map中, 关联之将进行值初始化. 如以下代码:

map<string, size_t> word_count;
cord_count["Anna"] = 1;

将会执行如下操作:
1.在word_count中搜索关键字为Anna的元素, 结果没有找到.
2.将一个新的关键字—值对插入到word_count中, 关键字是一个const string, 保存Anna. 值进行值初始化, 此处意味着值为0.
3.提取新插入的元素, 并将值1赋予它.
此处注意: 对于代码cord_count["Anna"] = 1的执行过程, 我的理解是如果关键字未找到, 则执行插入操作, 并将其值进行值初始化, 然后再进行下标操作提取元素, 并执行赋值操作.

map和unordered_map的下标操作
c[k]
c.at(k)

对一个map使用下标操作, 其行为与数组或vector的下标操作很不相同, 使用一个不在容器中的关键字作为下标, 会添加一个具有此关键字的元素到map中.

使用下标操作的返回值
map的下标运算与其他的下标运算的不同点:
1.map的下标运算在关键字不在容器中时会插入一个具有此关键字的元素到map中去, 而其他的则不会.
2.通常情况下, 解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的. 但是对map则不然, 对一个map进行下标操作会获得一个mapped_type对象, 但是当解引用一个map迭代器时会得到一个value_type对象.
map的下标操作与迭代器解引用的返回类型不一样, 这点与其他的下标操作不一样.

访问元素

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

lower_bound和upper_bound不适用于无序容器.
下标和at操作只适用于非const的map和unordered_map.

在一个关联容器中查找元素的操作
c.find(k)
c.count(k)
c.lower_bound(k)
c.upper_bound(k)
c.equal_range(k)

对map使用find操作代替下标操作
关联容器的下标操作有一个严重的副作用, 即如果关键字不在map中, 则下标操作会插入一个具有给定关键字的元素. 而find则不会.

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

在multimap和multiset中查找元素
在multimap和multiset中, 如果有多个元素具有给定的关键字, 则这些元素在容器中会相邻存储.
还可以使用lower_bound和upper_bound来解决此问题, 如果关键字在容器中, lower_bound返回的迭代器将指向第一个具有此关键字的元素, 而upper_bound返回的迭代器则指向最后一个匹配给定关键字元素之后的位置. 如果元素不在multimap中, 则lower_bound和upper_bound返回相等的迭代器——指向一个不影响排序的关键字插入位置. 如果lower_bound和upper_bound返回相同的迭代器, 则给定的关键字不在容器中.
最直接的方式是使用equal_range(), 它返回一个迭代器pair, 若关键字存在, 则指明关键字范围, 若未找到匹配元素, 则两个迭代器都指向关键字可以插入的位置.

multimap<string, int> mp{{"aa", 1}, {"aa", 2}, {"bb", 2}, {"cc", 1}, {"aa", 9}};
// 查找并输出所有关键字为"aa"的元素
for(auto beg = mp.lower_bound("aa"), end = mp.upper_bound("aa"); beg != end; ++beg)
    cout << beg->first << " " << beg->second << endl;
// 等价的操作
for(auto pos = mp.equal_range("aa"); pos.first != pos.second; ++pos.first)
    cout << pos.first->first << " " << pos.first->second << endl;

无序容器

无序容器不是使用比较运算符来组织元素, 而是使用一个哈希函数和关键字类型的 == 运算符. 无序容器的元素没有按顺序存储.
无序容器在存储上组织成一组桶, 每个桶保存0个或多个元素. 无序容器使用一个哈希函数将元素映射到桶. 为了访问一个元素, 容器首先计算元素的哈希值, 它指出应该搜索那个桶. 容器将所有具有一个特定哈希值的所有元素都保存在相同的桶中, 如果容器允许重复关键字, 则所有具有相同关键字的元素都会保存在同一个桶中. 因此无序容器的性能依赖于哈希函数的质量和桶的数量和大小.
理想情况下, 哈希函数能将每个特定的值映射到唯一的桶, 但是, 将不同关键字的元素映射到相同的桶也是允许的. 当一个桶保存多个元素时, 需要顺序搜索这些元素来查找我们想要的那个.

桶接口
c.bucket_count()
c.max_bucket_count()
c.bucket_size(n)
c.bucket(k)
桶迭代
local_iterator
const_local_iterator
c.begin(n), c.end(n)
c.cbegin(n), c.cend(n)
哈希策略
c.local_factor()
c.max_load_factor()
c.rehash(n)
c.reserve(n)

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

size_t hasher(const Sales_data &sd)   //提供自己的hash函数
{
    return hash<string>()(sd.isbn()); //用Sales_data中的isbn来做hash计算
}
bool eqOp(const Sales_data &lhs, const Sales_data &rhs) //提供 == 运算
{
    return lhs.isbn() == rhs.isbn()
}
using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>;  //重载哈希函数和 == 运算符.
SD_multiset bookstore(42, hasher, eqOp); // 定义无序容器

如果Sales_data定义了 == 运算符, 则可以只重载哈希函数.

// 使用FooHash生成哈希值, Foo必须有 == 运算符
unordered_set<Foo, decltype(FooHash)*> fooSet(10, FooHash);
posted @ 2017-09-11 22:39  moon1992  阅读(303)  评论(0编辑  收藏  举报