C++primer十万字笔记 第十一章 关联容器

 关联容器支持高效的关键字查找和访问,两个主要的关联容器是map和set。map中的元素是一些关键字-值(key-value)对:关键字起到索引的作用,值表示与索引相关联的数据。set中每个元素只包含一个关键字;set支持高效的关键字查询操作:检查一个关键字是否在set中,例如在某些文本处理过程中可以使用一个set保存想要忽略的此。字典则是一个很好的map的例子:可以将单词作为关键字,将单词释义作为值。
 标准库提供8个关联容器,如表11.1所示。这8个关联容器的不同体现在3个维度上:每个容器:

  1. 或者是一个set或者是一个map
  2. 或者要求不重复的关键字或者不要求不重复的关键字
  3. 按顺序保存元素或无序保存

匀速重复的容器的关键字都包含单词multi;不保持关键字顺序的容器名字都以unordered开头。因此一个unordered_multi_set是一个允许重复关键词,元素无序保存的集合。无序容器使用哈希函数来组织元素。
 map定义在头文件map中,set定义在头文件set中。无序的版本定义在unordered_map和unordered_set中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xr4xYeNZ-1641812198671)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901114242301.png)]

使用关联容器
map<string,size_t> word_count;	//建立一个string到siez_t的空map
set<string> exclude = {"the","a","an","or"};	//建立一个set,用来记录常用的单词
string word;
while(cin>>word)
    if(exlude.find[word]==exclude.end())	//集合的find成员函数如果没找到会返回一个尾后迭代器
    	++word_count[word];
for (const auto &w: word_count)
    cout<<w.first<<"occurs "<<w.second<<((w.second>1)?"times""time")<<endl;
关联容器

 关联容器支持一些普通的容器操作(如构造函数,迭代器,尺寸,swap等),但不支持顺序容器的操作如push_back和push_front,因为关联容器是按照关键字来存储的这些操作对关联容器没有意义。而且关联容器也不支持构造函数和插入操作这些接受一个元素值和一个数量值的操作。同样关联容器也有自己独特的一些操作是其他的顺序容器所没有的。

定义关联容器

 定义一个map时必须指明关键字类型又指明值类型,而定义一个set时只需要指明关键字类型,因为set中没有值。每个关联容器都定义了一个默认构造函数,它创建一个指定类型的容器。我们也可以将关联容器初始化为另一个同类型容器的拷贝。或是从一个值范围来初始化关联容器只要这些值能够转化为容器所需要的类型即可。

map<string,size_t> word_count;	//空容器
set<string> excluce ={"the","but","and"};
map<string,string> authors={{"Joyce","James"},{"Austen","Jane"}};
//和以往一样,初始化器必须能够转换为容器中元素的类型。对于set,元素类型就是关键字类型。对于map来说就是一个键值对,初始化的一个元素也需要进行内层花括号进行分开的操作。

 一个map或者set的关键字必须是唯一的,即对于一个给定的关键字,只能有一个元素的关键字等于它。容器multimap和multiset没有这个限制,都允许多个元素具有相同的关键字。

关键字类型要求

 关键字对于类型有一些限制,对于无序容器中关键字的要求在后面一点介绍。对于有序容器-map、multimap、set以及multiset的要求关键字类型必须定义元素比较的方法。默认情况下标准库使用关键字类型的<运算符来进行比较两个关键字。在集合类型中关键字类型就是元素类型;在映射类型中关键字类型是元素第一部分的类型。

有序容器的关键子类型

 可以向一个算法提供我们自定义的比较操作,与之类似,也可以提供自己定义的操作来代替关键字上的<运算符。所提供的操作必须在关键字类型上定义一个严格弱序。可以将严格弱序看做是小于等于,虽然实际上定义的操作可能是一个复杂的函数。无论我们如何定义函数,它必须满足如下的性质:

  • 两个关键字不能同时"小于等于对方"
  • 如果key1<=key2,且key2<=key3,那么key1必须<=key3;
  • 如果存在两个关键字(即任何一个都不"小于等于"另外一个),那么我们认为这两个关键字是等价的。当用作map的关键值对时,只能有一个元素能与这两个关键字相连,我们可以用两者的任意一个来访问对应的值。

 用于组织一个容器中元素的操作的类型也是该容器类型的一部分。为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的描述符。如前所述,用<>指定要定义哪种类型的容器,自定义操作类型必须在尖括号中紧跟着元素类型指出。
 在<>中出现的每个类型就仅仅是一个类型而已。当我们创建一个容器时才会以构造函数的参数的形式提供真正的比较操作(其类型必须在<>中与指定的类型相吻合)。

//例如不能定义Sales_data类型的multiset,以内Sales_data没有 <运算符。但是可以自定义一个能够成严格弱序的函数,函数定义如下:
bool compareIsbn(const & lhs,const Sales_data &rhs){
    reutnr lhs.isbn()<rhs.isbn();
}
//然后
multiset<Sales_data,decltype(compareIsbn)*>bookstore(compareIsbn);	//这里我们使用了decltype来指出自定义的操作的类型。当使用decltype函数来获得一个函数指针类型时需要加上一个*来指出我们要使用一个给定函数的指针。用compareIsbn来初始化bookstore对象。这表示当我们向bookstore添加元素时按照ISBN的成员来排序。当然使用&compareIsbn的效果也是一样的。
pair类型

 在介绍pair类型之前,我们需要了解名为pair的标准库类型。其定义在标准库头文件utility中。
 一个pair保存两个成员。类似容器pair是一个用来生成特定类型的模板。当创建一个pair时我们必须提供两个类型名,pair的数据成员将具有对应的类型,两个类型不要求一样:

pair<string,string> anno;	//保存两个string
pair<string,siz_t> word_count;//保存一个string和一个size_t
pair<string,vector<int>>temp;		//保存string和vector<int>
//pair会对每个类型调用默认构造函数当然也可以提供初始化器
pair<string,vector<int>>;		//会产生string和int的默认构造函数
pair<string,vector<int>>temp{"jingjing",{1,2,3}};	//使用大括号列表来构造vector
cout<<temp.first<<temp.second<<endl;				//使用first和secongd来返回数据成员

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lOq6b9Hz-1641812198672)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901142654480.png)]

创建一个pair对象的函数
pair<string,int> process(vector<stirng> &){
    if(!v.empty())
        return {v.back(),v.back().size()};	//使用列表来返回pair对象
    else
        reuturn pair<string,int>();			//隐式的构造返回值
}
关联容器操作
关联容器额外的类型名
key_type:此容器类型的关键字类型
mapped_type:每个关键字关联的类型;只适用于map
value_type:对于set和key_type相同,对于map,为pair<const key_type,mapped_type>
set<string>::value_type v1;		//v1是一个string
set<string>::key_type v2;		//v2是一个string
map<string,int>::value_type v3;	//v3是一个pair<cosnt string ,int>	注意pair的第一个是const类型的
map<string,int>::key_type v4;	//v4是一个string
map<string,int>::mapped_type v5	//v5是一个int
关联容器迭代器

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

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;			//正确我们可以额通过迭代器改变元素		

 虽然set迭代器定义了iterator和const_iterator类型,但两种类型只允许只读访问set的元素。与不能改变一个map元素的关键字一样,一个set的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改。

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关键字是只读的。
    count<<*set_it<<endl;		//正确,set的关键字是const的但是可以读
}

 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;
}
关联容器和算法

 我们通常不低关联容器使用泛型算法。关键字是const这一特性使得不能通过关联容器传递给修改或重排元素的算法。因为这些算法需要向元素写入值,而set类型中的元素是const的,map中的元素是pair并且其中一个是cosnt的。
 关联元素可用于读取元素的算法。但是很多这类都需要搜索序列,由于关联元素的元素不能通过其关键字进行(快速)查找。因此对其使用泛型搜索算法几乎是个坏主意。关联容器有自己的一个find成员函数算法。这比调用泛型find会快很多。
 在实际的编程中,如果我们真要对一个关联容器使用算法,要么是将其当做一个源序列,要么当做一个目的位置。例如可以使用泛型copy算法将元素从一个关联容器拷贝到另一个序列。类似的可以调用一个inserter将一个插入器绑定在一个泛型算法。通过使用inserter可以将关联容器当做一个目的位置来调用另一个算法。

添加元素

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nh3J2SLT-1641812198672)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901152104870.png)]

vector<int> ivec = {2,4,6,8};
set<int> set;
//set容器的insert有两个版本其中一个是接受一对迭代器另一个是接受一个初始化列表。
set.insert(ivec.cbegin(),ivec.cend());	//set中有4个元素
set.insert({1,3,5,7,1,3,5,7});			//现在set有8个元素

 对一个map添加元素时,碧玺记住元素的类型是pair。对想要插入的数据并没有一个现成的pair对象。可以在insert的参数列表中创建一个pair:

word_count.insert({word,1});
word_count.insert(make_pair(word,1));
word_coutn.insert(pairt<string,size_t>(word,1));
word_count.insert(map<string,size_t>::value_type(word,1));

 insert的返回值依赖于容器类型和参数。添加单一元素(单一元素不是这个元素可以是pair类型)的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素,second成员是一个bool值表示插入成功还是已经存在在容器中。如果关键字已经在容器中就什么都不做。且返回值中的bool为false。
 对于multiset或者multimap来说insert直接返回指向插入元素的迭代器。

删除元素

 关联容器定义了三种不同的erase,如下表。与顺序容器一样我们可以通过传递给erase一个迭代器或者一个迭代器对来删除一个元素或者一个元素返回,这个两个版本的erase与顺序容器的操作都非常相似:指定的元素被删除,函数返回void。
 关联容器提供一个额外的erase操作,接受一个key_type参数。此版本可以删除所以后匹配给定关键字的元素,返回实际删除的元素的数量。如果没有这个存在会返回0。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8QSxEA0a-1641812198672)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901155019648.png)]

map的下标操作

 map和unordered_map容器提供了下标运算符和一个对应的at函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FUq9F4VF-1641812198672)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901155133344.png)]

如上表所示set类型不支持set,因为set中没有与关键字相关联的值。元素本身就是关键字。我们不能对一个multimap和一个unordered_multimap使用下标运算符,因为这些容器中可能有多个值与一个关键字相联。
 map的下标操作返回的是mapped_type并且是一个左值,而对迭代器解引用得到的是value_type对象。

map<string,sizie_t> word_count;//空的map
word_count["hello"] = 1;
//将会执行如下的操作
* 在word_count中搜索Anna的元素,但是没有找到
* 将一个新的key-value对插入word_count中。保存Anna值进行初始化,为0
* 将新的值1赋给插这个插入的元素的value
访问元素

 关联容器中提供多种查找一个指定元素的方法。应该使用哪个操作依赖于我们要解决什么样子的问题。如果我们关心是否一个特定元素在容器中find成员函数是一个最佳的选择。对于不允许重复关键字的容器可能使用find还是coutn没什么区别。但是对于有重复的关键字的容器count会做更多的工作(统计个数)。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eMbCJJn9-1641812198672)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901201530339.png)]

 mapd下标操作如果没有这个元素存在会插入一个值,所以最好使用find来找到是否一个元素存在:
if(word_couny.find("foobar") == word_count.end()) cout<<"foor is not in it"<<endl;

 在一个不允许重复关键字的关联容器中查找元素是一件很简单的事,但是对于拥有重复关键字的来说就比较麻烦。如果一个multimap或者multiset有多个元素具有给定关键字,那么这些元素会相邻存储。

//下面的成语用来在一个multimap中查所有属于某个作者的著作
string serach_item("Alanin de Botteon");		//要查找的作者
autoentires = authors.count(search_item);		//元素的数量
auto iter  author.find(search_item);			//此作者的第一本书
while(entries--){								//while循环数量自减		
    cout<<iter++->second<<endl;					//打印每个书名
}
//我们可以使用lower_bound和upper_bound来解决这个问题。这两个操作都接受一个关键词来返回一个迭代器lower_bouind返回第一个具有给定关键字的元素,而upper_bound则返回最后一个匹配给定关键字的元素之后的位置。这两个结合起来的范围就可以确定所有关键字的范围
for(auto beg = author.lower_bound(search_item),end = authors.upper_bound(serach_item);,beg!=end,++beg)
    cout<<beg->second<<endl;
//最简单的方法是equal_range。这个函数接受一个关键字返回一个迭代器pair,过关键词存在则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。如找到匹配元素,则两个迭代器都指向关键字可以插入的位置。
for(auto pos=authors.equal_range(seatrch_item;pos.first!=pos.second;++pos.first)) cout<<pos.first->second<<endl;
一个小程序根据匹配规则来给定一个string来转化为另外一个string
//首先使用读取文件中的规则
//auto buildMap(const string &map_file_name) {
//    ifstream fs(map_file_name);
//    map<string, string> trans;
//    string key, value;
//    cout << "come on" << endl;
//    while (fs >> key >> value) {
//        cout << key << " " << value << endl;
//        trans.insert({key, value});
//    }
//    return trans;
//}
map<string, string> buildMap(const string& map_file_name) {
    map<string, string> trans_map;
    ifstream map_file(map_file_name);
    string key, value;
    while (map_file >> key && getline(map_file, value)) {
        if (value.size() > 1)
            trans_map[key] = value.substr(1);
        else
            throw runtime_error("no rule for " + key);
    }
    for (auto i : trans_map) {
        cout << i.first << " " << i.second << endl;
    }
    return trans_map;
}
// 用来进行转换的代码
const string& transform(const string& s, const map<string, string>& m) {
    auto map_it = m.find(s);
    if (map_it != m.cend())
        return map_it->second;
    else
        return s;
}
//下面是主流程的代码用来调用另外两个的
void word_transform(string filename, decltype(cin)& input = cin) {
    auto trans_map = buildMap(filename);
    string text;
    while (getline(input, text)) {
        cout << text << endl;
        istringstream stream(text);
        string word;
        bool firstword = true;
        while (stream >> word) {
            //            cout<<"word is "<<word<<endl;
            if (firstword)
                firstword = false;
            else
                cout << " ";
            cout << transform(word, trans_map);
        }
        cout << endl;
    }
}

无序容器

 新标准定义了四个无序容器。这些容器不是使用比较运算而是使用哈希函数和关键字类型==。在关键字类型没有明显的顺序的情况下,无序容器是很有用的。在某些应用中维护元素的序的代价非常高此时无序容器也很有用。
 虽然理论上哈希技术能够获得更好的平均性能,但在实际使用中还需要进行一些性能测试和调优的工作。因此使用无序容器通常更为简单。
 除了哈希管理操作外无序容器还提供了与有序容器相同的操作(find、insert、等)。这意味着我们曾用于map和set的操作也能用于unorder_map和unordered_set。类似的无序容器也有允许重复关键字的版本。
 通常可以使用一个无序容器代替有序容器但是由于没有按照顺序输出,一个使用无序容器的输出和有序容器可能会有些区别。

管理桶

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

 对于相同的参数哈希函数产生相同的结果,但是将不同的关键字映射到一个桶也是很有可能的。无序容器提供了一组桶管理操作。这些成员函数允许我们查询容器的状态以及在必要时强制容器进行重组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uh8Lq62J-1641812198673)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210901224951449.png)]

无序容器对于关键字的要求

默认情况下无序容器使用**关键字类型==来比较元素,他们还是用一个hash<key_type>**类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板。还为一些标准库类型包括string以及智能指针定义了hash。因此我们可以直接定义关键字是内置类型(包括指针类型)string和智能指针的无序容器。
 如果想要自定义的类型作为无序容器的关键字,需要自己提供hash模板版本。会在后面介绍如何做到这些(第十六章节)。
 我们不使用默认的hash,二是使用另一种方式,类似于为有序容器重载关键字类型的默认比较操作。为了能将Sales_data用作关键字,我们需要提供函数来代替==运算符和哈希值运算。我们从定义这些词开始。

size_t hasher(const Sales_data &sd){
    return hash<string>()(sd.isbn());			//使用标准库hash类型对象来计算ISBN成员的哈希值,该hash值建立在string类型之上。
}
bool eqOp(const Sales_data &1hs,const Sales_data &rhs){			//eqOp函数通过比较ISBN号来比较两个Sales_data
    return 1hs.isbn()==rhs.isbn();
}
using SD_multiset = unordered_multiset<Sales_data,decltype(hasher)*,decltype(eqOp)*>;	//这里重载了hash函数和相等比较运算。如果类已经定义了==符号,则可以只重载哈希函数。
SD_multiset bookstore(42,hasher,eqOp);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值