关联容器操作
关联容器还定义了如下表所示的类型。这些类型表示容器关键字和值的类型。
关联容器额外的类型别名 |
key_type 此容器类型的关键字类型 mapped_type 每个关键字关联的类型;只适用于map value_type 对于set,与key_type相同 对于map,为pair<const key_type,mapped_type> |
对于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.
关联容器迭代器
当解引用一个关联容器的迭代器时,我们会得到一个类型为容器的valued_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迭代器来读取元素的值,但不能修改:
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操作。与往常一样,我们可以用这些函数获取迭代器,然后用迭代器类遍历容器。例如,我们可以编写一个循环程序来打印单词计数的结果,如下所示:
//获取一个指向首元素的迭代器 auto map_it=word_count.cbegin(); //比较当前迭代器和尾后迭代器 while(map_it!=word_count.end()) { //解引用迭代器,打印关键字-值对 cout<<map_it->first<<" occurs "<<map_it->second<<" times" <<endl; ++map_it; //递增迭代器,移动到下一个元素 }
本程序的输出是按字典序排序的。当使用一个迭代器遍历一个map、multimap、set或multiset时,迭代器按关键字升序遍历元素。
关联容器和算法
我们通常不对关联容器使用泛型算法。关键字const这一特性意味着不能将关联容器传递给修改或重排序容器元素的算法,因为这类算法需要向元素写入值,而set类型是的元素是const的,map中元素是pair,其第一个成员是const的。
关联容器可用于只读取元素的算法。但是,很多这类型算法都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行查找,因此对其使用泛型搜索算法几乎总是个坏主意。例如,关联容器定义了一个名为find的成员,它通过一个给定的关键字直接获取元素。我们可以用泛型find算法来查找一个元素,但此算法会进行顺序搜索。使用关键字容器定义的专用的find成员会比调用泛型find快得多。
在实际编程中,如果我们真有对一个关联容器使用算法,要么是将它当作一个源序列,要么当作一个目的位置。例如,可以用泛型copy算法将元素从一个关联容器拷贝到另一个关联容器。通过使用inserter,我们可以将关联容器当作一个目的位置来调用另一个算法。
添加元素
关联容器insert成员向容器中添加一个元素或一个元素范围。由于map和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个元素
insert有两个版本,分别接受一对迭代器,或是一个初始化列表。这两个版本的行为类似对应的构造函数——对于一个给定的关键字,只有第一个带此关键字的元素才被插入到容器中。
向map添加元素
对一个map进行insert操作时,必须记住元素类型是pair。通常,对于想要插入的数据,并没有一个现成的pair对象。可以在insert的参数列表中创建一个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.inset(map<string,size_t>::value_type(word,1));
如我们所见,在新标准下,创建一个pair最简单的方法是在参数列表中使用花括号初始化。也可以调用make_pair或显式构造pair。最后一个insert调用中的参数:
map<string,size_t>::value_type(s,1);
构造一个恰当的pair类型,并构造该类型的一个新对象,插入到map中。
关联容器的insert操作 |
c.insert(v) v是value_type类型的对象;args用来构造一个元素 c.emplace(args) 对于map和set,只有当元素的关键字不再c中时才插入(或构造)元素。函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是 否成功的bool值 对于multiset和multimap,总会插入(或构造)给定元素,并返回一个指向新元素的迭代器 c.insert(b,e) b和e是迭代器,表示一个c::value_type类型值的范围;il是这种值的花括号列表。函数返回void c.insert(il) 对于map和set,值插入关键字不在c中的元素。对于multimap和multiset,则会插入范围中的元素
c.insert(p,v) 类似insert(v)(或emplace(args)),但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素 c.emplace(p,args) |
检测insert的返回值
insert(或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pari,告诉我们插入操作是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在与容器中。如果关键字已经在容器中,则insert什么事情也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。
作为一个例子,我们用insert重写单词计数程序:
//统计每个单词在插入中出现次数的一种繁琐的写法 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总是向这类容器中加入一个新元素。
删除元素
关联容器定义了三个版本的erase,如下表所示。与顺序容器一样,我们可以通过传递erase一个迭代器或一个迭代器对来删除一个元素或一个元素范围。这两个版本的erase与对应的顺序容器的操作非常相似:指定的元素被删除,函数返回void。
关联容器提供一个额外的erase操作,它接受一个key_type参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除元素的数量。我们可以用此版本在打印结束之前从word_count中删除一个特定的单词:
//删除一个关键字,返回删除的元素数量
if(word_count.erase(removal_word))
cout<<"ok:"<<removal_word<<" removed\n";
else
cout<<" oops:"<<removal_word<<" not fount!\n";
对于保存不重复关键字的容器,erase的返回值总是0或1。若返回值为0,则表明想要的删除的元素并不在容器中
对允许重复关键字的容器,删除元素的数量可能大于1:
auto cnt=authors.erase("Barth,John");
如果authors是我们创建的multimap,则cnt为2.
从关联容器删除元素 |
c.erase(k) 从c中删除每个关键字为k的元素,返回一个size_type值,指出删除的元素的数量 c.erase(p) 从c中删除迭代器p指定的元素,p必须指向c中一个真实元素,不能等于c.end().返回一个指向p之后元素的迭代器。若p指向c中的尾元素,则返回c.end() c.erase(b,e) 删除迭代器对b和e所表示的范围中的元素,返回e |
map的下标操作
map和unordered_map容器提供了下标运算符和一个对应的at函数,如下表所示。set类型不支持下标,因为set中没有与关键字相关联的“值”。元素本身就是关键字,因此“获取与一个关键字相关联的值”的操作就没有意义了。我们不能对一个multimap和unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
类似我们用过的其他下标运算符,map下标运算符接受一个索引(即,一个关键字)。获取与此关键字相关联的值。但是,与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素插入到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中。
map和unordered_map的下标操作 |
c[k] 返回关键字为k的元素;如果k不做c中,添加一个关键字为k的元素,对其进行值初始化 c.at(k) 访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常 |
使用下标操作的返回值
map的下标运算符与我们用过的其他下标运算符的另一个不同之处是其返回类型。通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。但对map则不然,当对一个map进行下标操作时,会获得一个mapped_type对象;但当解引用一个map迭代器时,会得到一个value_type对象。
与其他下标运算符相同的是,map的下标运算符返回一个左值。由于返回的是一个左值,所以我们既可以读也可以写元素:
cout<<word_count[“Anna"];//用Anna作为下标提取元素;会打印出1
++word_count["Anna"]; //提取元素,将其增1
cout<<word_count["Anna"]; //提取元素并打印它,会打印2
注意:与vector和string不同,map 的下标运算符返回的类型与解引用map迭代器得到的类型不同。
如果关键字还未在map中,下标运算符会添加一个新元素,这一特性允许我们编写出异常简洁的程序,例如单词计数程序中的循环。另一方面,有时只是想知道一个元素是否在map中,但在不存在时并不想添加元素。在这种情况下,就不能使用下标运算符。
访问元素
关联容器提供多种查找一个指定元素的方法,如下表所示。应该使用哪个操作依赖于我们要解决什么问题。如果我们所关心的只不过是一个特定元素是否已在容器中,可能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
在一个关联容器中查找元素的操作 |
lower_bound和upper_bound不适合无序容器。 下标和at操作只适用非const的map和unordered_map c.find(k) 返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器 c.count(k) 返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1 c.lower_bount(k) 返回一个迭代器,指向第一个关键字不小于k的元素 c.upper_bount(k) 返回一个迭代器,指向第一个关键字大于k的元素 c.equal_range(k) 返回一个迭代器pair,表示关键字等于k的元素的返回。若k不存在,pair的两个成员均等于c.end() |
对map使用find代替下标操作
对map和unordered_map类型,下标运算符提供了最简单的提前元素的方法。但是,如我们所见,使用下标操作有一个严重的副作用:如果关键字还未在map中,下标操作会插入一个具有给定关键字的元素。这种行为是否正确完全依赖于我们的预期是什么。例如,单词计数程序依赖于这样一个特性:使用一个不存在的关键字作为下标,会插入一个新元素,其关键字为给定关键字,其值为0.也就是说,下标操作的行为符合我们的预期。
但有时,我们只想知道一个给定关键字是否在map中,而不想改变map。这样就不能使用下标运算符来检查一个元素是否存在,因为如果关键字不存在的话,下标运算符会插入一个新元素。在这种情况下,应该使用find:
if(word_count.find("foobar")==word_count.end())
cout<<"foobar is not in the map"<<endl;
在multimap和multiset中查找元素
在一个不允许重复关键字的关联容器中查找一个元素是一件很简单的事情——元素要么在容器中,要么不在。但对于允许重复关键字的容器来说,过程就更为复杂:在容器中可能有很多元素具有给定的关键字。如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中相邻存储。
例如,给定一个从作者到著作题目的映射,我们可能想打印一个特定作者的所有著作。可以用三种不同方法来解决这个问题。最直观的方法是使用find和count:
string search_item("Alain de Botton"); //要查找的作者
auto entries=authors.count(search_item); //元素的数量
auto iter=authors.count(search_item); //此作者的第一本书
while(entries)
{
cout<<iter->second<<endl;
++iter;
--entires;
}
首先调用count确定此作者共有多少本著作,并调用find获得一个迭代器,指向第一个关键字为此作者的元素。for循环的迭代次数依赖于count的返回值。特别是,如果count返回0,则循环一次也不执行。
注意:当我们遍历一个multimap或multiset时,保证可以得到序列中所有具有给定关键字的元素。