关联型容器

关联型容器

一、概述

关联性容器支持高效的关键字查找和访问。其中元素是按关键字来保存和访问的,主要关联型容器类型是map和set。map中的一些元素是pair<key, value>,即键值对,关键字起到索引的作用,值则表示与索引关联的数据。set中每个元素只包含一个关键字;set支持高效的关键字查询操作——检查一个给定关键字是否在set中。字典是使用map很好的例子;

标准库提供8个关联容器,如图所示。

image

不同之处体现在三个维度:

1.或者是map,或者是set

2.或者是有序,或者是无序

3.或者是允许重复,或者不允许重复

map和multimap定义在头文件map中;

set和multiset定义在set文件中;

无序容器定义在unordered_map和unordered_set中

二、定义关联容器

定义map时,必须之名关键字类型和值类型,而定义set时,只需指明关键字类型;

定义空容器:map<string, size_t> word;

列表初始化(C++11):

map<string, size_t> m;
map<string, size_t> n{{"tubin", 1}, {"tufang", 2}, {"tianlan", 3}};
map<string, size_t> p(n.begin(), n.end());
for (const auto & w : p) {
    cout << w.first << " " << w.second << endl;
}

当初始化map时,必须提供关键字类型和值类型,用{}花括号扩起来;

三、关键字类型的要求

3.1 关键字类型

关联型容器对关键字类型有些限制。对于有序容器,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。

可以提供自定义的操作代替关键字的<运算符,只是该操作必须在关键字类型上定义一个严格弱序。(<=)

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

3.2使用关键字类型的比较函数

为了指定使用自定义的操作,必须在定义关联容器时提供比较操作的类型。自定义操作类型必须在尖括号中紧跟着元素类型给出。

方法1

//自定义比较函数
bool compare_size(const string &a, const string &b) {
    return a.size() < b.size();
}
//指定比较函数
map<string, size_t, decltype(compare_size)*> word(compare_size);

decltype指出自定义操作的类型,但当它作用于函数时得到的并非是函数指针,而是函数类型,因此必须加上*
再用compare_size来显示初始化word对象。此时将会调用map的explicit带参构造。

方法2

//定义
template <typename T>
struct Longer : public binary_function<T, T, bool> {
    bool operator() (const T& x, const T& y) const {
        return x.size() > y.size();
    }
};

//使用
map<string, size_t, Longer<string>> word;

看过STL源码之后,可以通过自定义一个函数,继承自标准库的模版,就可以得到可配接(adaptable)的函数对象了。

3.3 简单应用

可以定义vector::iterator 到 int 的map,但是不能定义list::iterator 到 int 的map.

因为vector中迭代器即为原生指针,支持<运算符,但list的迭代器不支持<运算符,因为不是连续存储的,比较大小没有意义;

四、pair类型

pair标准库类型,定义在utility中。一个pair保存两个数据成员。类似容器,pair是一个用来省城特定类型的模版,创建pair时必须提供两个类型名,两者不要求一样。

创建pair

//使用默认构造
pair<string, string> p1;
pair<string, size_t> p2;
pair<string, vector<int> p3;
//提供初始化器
pair<string, string> author{"my", "mht"};
//make_pair();
pair<string, string> p = make_pair("al", "bd");
//显示构造
p3 = pair<string, vector<int>>();

使用pair

pair的数据成员是public的,两个成员分别命名为first和second。

用对象访问:cout << author.first << author.second;

用指针访问:cout << w->first << w->second;

五、简单使用关联容器

ex1:单词计数程序 map

//自定义判断符号
map<string, size_t, Longer<string>> word_count;
string record;
while (cin >> record) {
    ++word_count[record];
}
for ( const auto & w : word_count) {
    cout << w.first << ": " << w.second
    << (w.second > 1 ? "times": "time") << endl;
}

ex2:忽略常见单词的计数程序 map set

/用set保存想忽略的单词,在统计前判断
map<string, size_t> word_count;
set<string> exclude = {"the", "The", "and", "And", "a", "A", "or", "Or"};

string word;
while (cin >> word) {
    //不在exclude中
    if (exclude.find(word) == exclude.end()) {
        ++word_count[word];
    }
}

for (const auto& w : word_count) {
    cout << w.first << ": " << w.second
            << (w.second > 1 ? " times" : " time")
            << endl;
}

六、关联容器操作

key_type 容器的关键字类型
mapped_type 每个关键字关联的类型,只适用于map
value_type 对于set,与key_type 相同;对于map,是pair

6.1 关联容器迭代器

迭代器
解引用一个迭代器得到一个容器的value_type值的引用;
其中,set的迭代器是const的,value_type即为key_type,是const的
对于map,value_type的first成员是key_type, 也是const的。
容器和算法
我们通常不对关联容器使用泛型算法。关键字是const这一特性意味着不能使用mutable 算法;只应用于制度的算法;但这类型的算法都要搜索序列,不能进行快速查找。因此对于关联性容器使用泛型搜索算法,几乎总是个坏主意。应该使用关联性容器专用的成员。(见后文)
实际应用
在实际应用中,如果要使用算法,要么当作一个源序列,要么当一个目的位置。

6.2 添加元素

四种方法添加元素

map<string, size_t> m;
m.insert({"tubin", 1});
m.insert(make_pair("tufang", 2));
m.insert(pair<string, size_t>("lanlan", 3));
m.insert(map<string, size_t>::value_type("tiantian", 4));

返回值
1.对于不包含重复关键字的容器,添加元素返回一个pair<iterator, bool>,bool标识插入成功与否。
2.对于包含重复关键字的容器,添加元素一定能成功,因此返回的是指向新元素的iterator

重写单词计数程序

map<string, size_t> word_count;
string word;
while (cin >> word) {
    auto ret = word_count.insert({word, 1});
    if (!ret.second) {
        ++ret.first->second;//解析如下
    }
}

解析:
ret保存返回值pair<iterator, bool>
ret.first 为iterator,指向value_type<key_type, mapped_type>
ret.first->second 为值mapped_type
++ret.first->second 递增此值

6.3 删除元素

标准库定义函数如下:
image

erase(k):返回实际删除的元素的数量;
对于不重复容器,erase返回的值总是0(不存在) or 1(存在);
对于重复容器,erase返回的值可能大于1;

6.4 map的下标操作

特定容器提供
map和unordered_map提供下标[ ]运算符和对应at函数;
set不提供,因为没有相关联的值;
multi-不提供;因为下标索引到的结果可能有多个;

特性
下标操作接受一个索引(关键字),获取与之相关联的值;但主要特性是,如果没有该索引,则创建一个元素并插入到map中,关联值将进行值初始化;
因此,只能对非const版本提供下标操作

at操作
访问关键字为k的元素,带有参数检查;如果k不存在,则抛出异常;

返回值
通常情况下,解引用一个迭代器返回类型与下标运算返回类型一致;但对于map则不然
对map,下标操作返回一个mapped_type对象; 解引用迭代器,得到value_type(pair)对象.

总结
下标和at操作只指针对于非const的map和unordered_map

6.5 访问元素

标准库定义如下查找元素的操作
image

对于不允许重复容器,count和find没区别;
对于允许重复容器,count还要统计个数;

因此,对于count,find,[]
如果指向知道给定关键字是否在map中,而不想改变map,则用find;否则用[]
如果不想计数,则用find;否则用count

在multimap,multiset中查找元素的三种方法
ex:在给定一个从作者到著作题目的映射,打印一个特定作者的所有著作,有三种方法

1 count + find

multimap<string, string> m{{"my", "tb"}, {"my", "tm"}, {"my", "zfb"}, {"mht", "QQ"}, {"mht", "wx"}};
string item = "my";

auto entries = m.count(item);   //统计个数,如果没有返回0
auto iter = m.find(item);       //找到第一个,如果没有返回end()

while (entries) {
    cout << iter->second << endl;
    ++iter;
    --entries;
}

2 lower_bound + upper_bound
lower_bound:返回迭代器指向第一个具有给定关键字的元素;
upper_bound:返回的迭代器指向最后一个匹配给定关键字的元素之后的位置;
如果关键字不存在,则lower_bound和upper_bound会返回相等的迭代器;(有可能都是尾后迭代器)

for (auto b = m.lower_bound(item), e = m.upper_bound(item);
    b != e; ++b) {
    cout << b->second << endl;
}

3 equal_range
equal_range返回的是pair,其中两个成员分别是调用的lower_bound和upper_bound得到的迭代器。因此,此方法和上面方法没有本质的区别;唯一的区别是:不需要定义局部变量来保存元素范围;

for (auto p = m.equal_range(item); p.first != p.second; ++p.first) {
    cout << p.first->second << endl;
}

7 无序容器

新标准定义了4个无序容器。这些容器不是用比较运算符来组织元素,而是是哟过一个哈希函数和关键字类型的==运算符。在关键字无明显的序的关系时,无序容器是非常有用的。

7.1使用无序容器

无序容器的使用和对应的有序容器的使用完全相同。唯一的区别就是内容是无序的,不能指望输出的结果是有序的。
通常,可以用一个无序容器替换对应的有序容器,反之亦然。

7.2 管理桶(bucket)

无序容器在存储上组织为一组桶,每个桶保存0或多个元素。(SGI STL源码中哈希表采用分离链接法实现)。
对于相同的参数,哈希函数必须总是产生相同的鸡诶过。

7.3 对关键字类型的要求

1 默认情况哎,无序容器使用关键字类型==运算符来比较元素,他们还使用一个hash类型的对象来生成每一个元素的哈希值。
2 标准库为内置类型(包括指针类型),string和智能指针类型定义了hash模版,因此我们可以直接定义相对应的无序容器。
3 但是我们不能直接定义关键字类型为自定义类型的无序容器。除非我们提供自己的hash模版版本。

7.4 自定义类型容器

1 自定义
类似于自定义map关键字类型比较函数一样,我们也自定义函数代替==运算符合哈希函数

//自定义函数
size_t hasher(const A& s) {
    return hash<string>()(s.name());
}

bool equal_A(const A& a1, const A& a2) {
    return a1.name() == a2.name();
}

//使用这些函数来定义一个unordered_set
unordered_set<A, decltype(hasher)*, decltype(equal_A)*> usa1(43, hasher, equal_A);

//简化
//类型定义
using SD_Set = unordered_set<A, decltype(hasher)*, decltype(equal_A)*>;

//定义unordered_set
SD_Set s(43, hasher, equal_A);

2 模版特例化
对于内置类型直接定义:unordered_set<int> us;

为了让自定义数据类型也能用默认方式,必须定义hash模版的一个特例版本;
一个特例化hash类必须定义:
1.一个重载的调用运算符,接受一个容器关键字类型的对象,返回size_t
2.两个类型成员,result_type和argument_type,分别为调用运算符()的返回类型和参数类型
3.默认构造和拷贝赋值运算符(可隐式定义)

唯一复杂的地方就是需要在原模版定义所在的命名空间中特例化。具体定义如下:

//假设A为自定义类型
class A {
public:
    string name() const { return name_; }
private:
    int size_;
    string name_;
};

//为A类型特例化hash模版
namespace std {//打开命名空间
    template<>//表示正在定义一个特例化版本,模版参数为A
    struct hash<A>
    {
        typedef size_t result_type;
        typedef A argument_type;
        size_t operator()(const A& s) const;
    };

    size_t hash<A>::operator()(const A &s) const {
        return hash<string>()(s.name());
    }
} //关闭命名空间;注意,此处无分号;

//定义A类型的无序容器
unordered_set<A> usa;

你可能感兴趣的:(map,set,容器,模版特例化)