您的位置:首页 > 编程语言 > C语言/C++

C++基础的不能再基础的学习笔记——关联容器

2018-02-14 21:49 627 查看

关联容器

之前我们已经学习了顺序容器:

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模板。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息