关联容器
之前我们已经学习了顺序容器:
http://blog.csdn.net/fancynece/article/details/79193881
而关联容器和顺序容器有着根本的不同:
顺序容器中的元素是按元素在容器中的位置顺序保存和访问的,
关联容器中的元素是按关键字来保存和访问的。
关联容器类型 | 元素 |
---|---|
按关键字有序保存元素 | |
map | 键值对 |
set | 关键字 |
multimap | 关键字可重复的map |
multiset | 关键字可重复的set |
无序保存元素 | |
unordered_map | 用哈希函数组织的map |
unordered_set | 用哈希函数组织的set |
unordered_multimap | 用哈希函数组织的map,关键字可重复 |
unordered_multiset | 用哈希函数组织的set,关键字可重复 |
其中,map和multimap定义在头文件”map”中,set和multiset在头文件set中,无序容器定义在头文件”unordered_map”和”unordered_set”中。
一、如何使用关联容器
同顺序容器一样,关联容器也是类模板,因此我们也需要给出元素类型。
1、 map是关键字-值对的集合。例如,可以将一个人的名字作为关键字,电话号码作为值,我们称之为“将名字映射到电话号码”。map通常被称作关联数组,与正常数组类似,不同之处在于我们将位置作为下标来访问数组,而将关键字作为下标来访问map。
map<string, size_t> cnt; //string到size_t类型的映射
string word;
while (cin >> word )
++cnt[word]; //以关键字为下标进行访问,若访问不到则创建新元素
for (const auto &c : cnt)
cout << c.first << "---" << c.second << endl;
2、set是关键字的集合(关键字即值),当只是想知道一个值是否存在时,set是最有用的。
map<string, size_t> cnt;
set<string> exclude = {"the","is","i"};
string word;
while (cin >> word) {
//对不在exclude里的单词计数
if(exclude.find(word) == exclude.end())
++cnt[word];
}
for (const auto &c : cnt)
cout << c.first << "---" << c.second << endl;
二、关联容器概述
关联容器都支持容器公有的操作,但是不支持与位置相关的操作,如push_front
并且关联容器不支持一个元素值和一个数量值进行构造或赋值。
1. 定义和初始化关联容器
定义和初始化关联容器,可以通过以下几种方式:
- 默认构造函数:
set<string> s1;
- 同类型容器拷贝:
set<string> s2(s1);
- 迭代器给定范围:
set<string> s3(s2.begin(), s2.end());
- 列表初始化:
set<string> s4{ "fancy" };
map和set不允许关键字重复,而multimap或multiset允许关键字重复。
vector<int> ve{ 2,1,2,3 };
set<int> s(ve.cbegin(), ve.cend());
multiset<int> ms(ve.cbegin(), ve.cend());
cout << s.size() << endl; //输出3
cout << ms.size() << endl; //输出4
2. 关键字类型的要求
对于有序容器,关键字是有顺序的,因此关键字的类型必须定义比较方法。
对于C++的内置类型而言,用 < 运算符来比较两个关键字。
而对于自定义的类型,我们可以提供自己定义的操作来代替 < 运算符,但是这个操作必须是严格弱序(<)的,也就是必须要满足下面三条规则:
- a严格弱序于b,则b不可能严格弱序于a
- a严格弱序于b,b严格弱序与c,则a严格弱序于c
- a、b不严格弱序于对方,则a、b等价
a < b //a小于b
b < a //a大于b
!(a < b) && !(b < a) //a等价b
3. pair类型
标准库模板类型pair定义在头文件utility中,一个pair保存两个数据成员frist和second。当我们从map中取出一个键值对时,会得到一个pair类型的对象。
不同的是,pair的数据成员是public的,可以通过访问符.进行访问。
pair<type1,type2> p; //默认初始化
pair<type1,type2> p(v1,v2);
pair<type1,type2> p = {v1,v2};
make_pair(v1,v2); //根据v1和v2构造一个pair,一般用作参数时使用
p.first
p.second
p1 关系运算符 p2
p1 == p2
p1 != p2
三、关联容器操作
除了顺序容器所提供的类型外,关联容器还定义了其他类型。
类型名 | |
---|---|
key_type | 表示关键字的类型 |
mapped_type | 表示值的类型(map特有) |
value_type | 对于set,value_type = key_type。 对于map,value_type = pair< const key_type,mapped_type >(需要注意的是,由于关键字的类型不可改变,因此需要const修饰) |
1. 关联容器迭代器
解引用迭代器(关键字不可修改)
当解引用关联容器的迭代器时,我们会得到容器的value_type类型的值的引用。
对于map而言,map的迭代器指向一个pair类型:pair< const key_type,mapped_type >。其中关键字是不可修改的,我们不可通过pair修改关键字。
map<string,int> cnt = {{"fancy",9}};
auto map_it = cnt.begin();
map_it->frist = "new"; //错误,关键字是const的
map_it->second = 10; //正确
对于set而言,虽然set定义了iterator和const_iterator类型,但由于关键字(set值)不可改变,因此两种类型都只提供只读功能。
set<int> iset = {0,1,2,3,4,5}
set<int>::iterator set_it = iset.begin();
if(set_it != iset.end())
*set_it = 6; //错误
2. 添加元素
insert操作 | |
---|---|
c.insert(v) | v是value_type类型的对象 |
c.emplace(args) | args用来构造元素 |
对于map和set,只有在关键字不存在时才会插入,返回指向该关键字的迭代器和一个Bool表示是否插入成功。而对于multi而言,总是会插入数据,返回新插入元素的迭代器。 | |
c.insert(b,e) | b、e为一对迭代器,返回void |
c.insert(list) | 列表插入,返回void |
c.insert(p,v) | p为一个迭代器,从p开始搜索新元素应该存储的位置,返回指向该关键字的迭代器 |
c.emplace(p,args) |
向set中插入元素。
vector<int> ivec = { 2,4,6,8,2,4,6 };
set<int> num;
num.insert(ivec.begin(), ivec.end());
num.insert({ 1,3,5,7,2,4,6 });
向map中插入元素,所插入的元素类型是pair类型。通常对于想要插入的数据,并没有一个现成的pair对象,可以在insert的参数列表中创建pair。
map<string, size_t> nameAge;
nameAge.insert({ "fancy",18 });
nameAge.insert(make_pair("six", 21));
nameAge.insert(pair<string, size_t>("naomi", 3));
nameAge.insert(map<string, size_t>::value_type("clear", 5))
insert函数根据容器类型和参数的不同,返回值也不相同。
对于不包含重复关键字的容器,insert和emplace 返回一个pair。pair.frist是一个迭代器,指向该关键字对应的元素;pair.second是一个bool类型,为false时表示,关键字已存在容器中,为true时表示,不存在并且已成功插入。
3. 删除元素
erase操作 | |
---|---|
c.erase(k) | 删除关键字为k的元素。返回size_type类型的值,指出删除的元素的数量 |
c.erase(p) | 删除迭代器p所指向的元素。返回指向p之后元素的迭代器 |
c.erase(b,e) | 删除迭代器所指范围[b,e),返回e |
if(word_cnt.erase(remove_word)
cout << remove_word << "已删除" << endl;
cout << remove_word << "未找到" << endl;
4. map的下标操作
map和unordered_map提供了下标运算符。
. | |
---|---|
c[k] ; | 返回关键字为k的元素。若k不在c中,添加一个关键字为k的元素,并默认初始化 |
c.at(k) ; | 若k不在c中,抛出异常 |
需要注意的是,顺序容器的下标运算符只提供了访问功能,而map的下标运算符,当容器中无该关键字时,会为它创建一个元素并插入到map中,并进行默认初始化。因此如果只是检验容器中是否有该关键字,不能使用下标运算符。
与顺序容器下标运算符的另一个不同之处是,顺序容器 下标运算符返回的类型与解引用迭代器的类型,是相同的。而map 下标运算符返回mapped_type类型,解引用迭代器返回的是value_type类型。
map<string, int> name;
name["fancy"]; //创建了 <fancy,0> 这个元素
name["fancy"] = 10; //修改关键字为fancy的元素的值
name.at("fancy");
5. 访问元素
访问元素 | |
---|---|
c.find(k); | 返回一个迭代器,指向第一个关键字为k的元素。若k不在容器中,则返回尾后迭代器 |
c.count(k); | 返回关键字等于k的元素的数量 |
c.lower_bound(k); | 返回一个迭代器,指向第一个关键字不小于k的元素 |
c.upper_bound(k); | 返回一个迭代器,指向第一个关键字大于k的元素 |
c.equal_range(k); | 返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,则pair的两个成员都是尾后迭代器 |
其中,无序容器不支持 lower_bound 和 upper_bound。
当我们想要访问map中的元素时,根据所期望结果的不同,可以选择使用下标运算符(关键字不在容器时添加),或者find函数(关键字不在容器时不添加)。
multimap<string, string> book = { {"fancy","xxx"},{"fancy","yyyyy"},{"six","zzz"} };
auto cnt = book.count("fancy"); //获得数目
auto it = book.find("fancy"); //找到第一本书
while(cnt){
cout << it->second << endl;
++it;
--cnt;
}
lower_bound和upper_bound
这两个操作都接受一个关键字,返回一个迭代器。
当关键字在容器中时,lower_bound返回第一个具有该关键字的元素的迭代器,upper_bound返回最后一个具有该关键字的元素的下一个元素的迭代器。
因此,用lower_bound和upper_bound可以得到一个具有该关键字元素的范围。
当关键字不在容器中时,lower_bound和upper_bound会返回相等的迭代器——指向一个不影响排序的关键字插入位置。
因此,我们可以重写上述程序。
multimap<string, string> book = { {"fancy","xxx"},{"fancy","yyyyy"},{"six","zzz"} };
for (auto beg = book.lower_bound("fancy"), end = book.upper_bound("fancy"); beg != end; ++beg)
cout << beg->second << endl;
equal_range函数
equal_range函数返回一个pair类型,即pair< lower_bound,upper_bound >与同时使用lower_bound与upper_bound 所得范围相同。
multimap<string, string> book = { {"fancy","xxx"},{"fancy","yyyyy"},{"six","zzz"} };
for (auto pos = book.equal_range("fancy"); pos.first != pos.second; ++pos.first)
cout << pos.first->second << endl;
一个单词转换的map
接下来,我们以单词转换为例,学习写一个map。
程序的输入是两个文件。
第一个文件保存的是一些规则,每条规则由 A B构成,对于输入文本,出现A时替换成B。
第二个文件是要转换的文本。
我们需要三个函数来实现这个过程。第一个是管理整个过程的wordTransform,第二个是建立转换规则的buildMap,第三个是进行单词转换的transform。
//管理整个过程的函数
void wordTransform(ifstream& map_file, ifstream& input) {
auto trans_map = buildMap(map_file); //规则映射
string text;
while (getline(input, text)) {
istringstream stream(text);
string word;
bool wd = true; //是否打印空格
while (stream >> word) {
if (wd)
wd = false;
else
cout << " ";
cout << transform(word, trans_map);
}
cout << endl;
}
}
//规则映射
map<string, string> buildMap(ifstream& map_file) {
map<string, string> trans;
string key, value;
//第一个单词为关键字,剩余单词为要转换的句子
while (map_file >> key && getline(map_file, value)) {
if (value.size() > 1)
trans[key] = value.substr(1); //跳过空格
else
throw runtime_error("no rule for" + key);
}
return trans;
}
//转换函数
const string& transform(const string& s, const map<string, string>& m) {
auto it = m.find(s);
if (it != m.end())
return it->second;
else
return s;
}
四、 无序容器
无序容器并不是通过比较运算符(<)来组织元素的,而是通过哈希函数和关键字类型的==运算符。
在关键字类型是无序的情况下,用无序容器是很好的选择。
而无序容器和有序容器可以相互替换,但是由于元素并没有按关键字类型顺序存储,使用无序容器的输出通常会与有序容器不同。
1. 管理桶
无序容器在存储上为一组桶。每个桶内存放0个或多个元素,将哈希函数值相同的元素存放在一个桶内。当一个桶有多个元素时,便需要顺序搜索这些元素。
因此,无序容器的性能和哈希函数的质量、桶的大小密切相关。
无序容器提供了一组管理桶的函数,允许我们查询容器的状态以及在必要时强制容器进行重组。
管立桶的函数 | |
---|---|
桶接口 | |
c.bucket_count() | 正在使用的桶的数目 |
c.max_bucket_count() | 容器能容纳的最多的桶的数目 |
c.bucket_size(n) | 第n个桶的元素个数 |
c.bucket(k) | 关键字为K的元素在哪个桶中 |
桶迭代 | |
local_iterator | 访问桶中元素的迭代器 |
const_local_iterator | |
c.begin(n),c.end(n) | 第n个桶的首元素迭代器和尾后迭代器 |
c.cbegin(n),c.cend(n) | |
哈希策略 | |
c.load_factor() | 每个桶的平均元素数量 |
c.max_load_factor() | c会维护桶,使load_factor <= max_load_factor |
c.rehash(n) | 重组存储,使bucket_count >= n && bucket_count > size / max_load_factor |
c.reserve(n) | 重组存储,使c可以保存n个元素且不必rehash |
无序容器使用关键字类型的==运算符比较元素,使用一个hash< key_type >类型的对象生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板,也为string、智能指针定义了hash。
因此,我们可以为内置类型(包括指针)、string、智能指针定义无序容器,但是当我们自定义类型时,必须提供自己的hash模板。