C++基础之关联容器

关联容器

关联容器和顺序容器的本质区别:关联容器是通过键存取和读取元素、顺序容器通过元素在容器中的位置顺序存储和访问元素。因此,关联容器不提供front、push_front、pop_front、back、push_back以及pop_back,此外对于关联容器不能通过容器大小来定义,因为这样的话将无法知道键所对应的值什么。关联容器支持高效的关键字查找和访问,主要是map和set两类。标准库提供了8个关联容器:

map 关联数组:保存键值对,每个元素是pair类型,关键字起到索引的作用,值则表示与索引相关联的数据
set 大小可变的集合,支持通过键实现的快速读取,只有关键字,没有值
multimap 支持同一个键多次出现的 map 类型
multiset 支持同一个键多次出现的 set 类型
unordered_map 使用hash函数组织的map
unordered_set 使用hash函数组织的set
unordered_multimap 使用hash函数组织的map,关键字可重复出现
unordered_multiset 使用hash函数组织的set,关键字可重复出现

一个map或set中关键字必须是唯一的,即一个关键字对应唯一一个值,但是multimap或multiset没有此限制,他们允许多个值对应一个关键字。

有序容器的关键字类型,默认情况下使用<运算符来比较关键字,也可以向算法提供自己定义的操作,但是操作必须在关键字类型上定义一个严格弱序,尖括号中每个类型仅仅表示一个类型,当创建对象时,才会以构造函数参数的形式提供真正的比较操作。

typedef struct Sales{
    int id;
};
//自定义的比较函数
bool compareId(const Sales &s1, const Sales &s2){
    return s1.id < s2.id;
}
//将decltype作用于函数时,返回函数类型,而非函数指针,因此要显示加*来指示它是函数指针;使用compareId来初始化对象表示,使用bookstore添加对象是,使用该函数来排序
multiset<Sales, decltype(compareId)*>bookstore(compareId);

pair类型

pair类型定义在utility头文件中。

pair<T1, T2> p1; 创建一个空的 pair 对象,它的两个元素分别是T1 和 T2 类型,采用值初始化
pair<T1, T2> p1(v1, v2); 创建一个 pair 对象,它的两个元素分别是 T1 和 T2 ,其中 first 成员初始化为 v1,而 second 成员初始化为 v2
pair<T1, T2> p1={v1, v2}; 等价于p1(v1, v2);新增加
make_pair(v1, v2) 以 v1 和 v2 值创建一个新 pair 对象,其元素类型分别是 v1 和 v2 的类型
p1 < p2 两个 pair 对象之间的小于运算,其定义遵循字典次序:如果 p1.first < p2.first 或者 !(p2.first < p1.first) && p1.second < p2.second,则返回
true
p1 == p2 如果两个 pair 对象的 first 和 second 成员依次相等,则这两个对象相等。该运算使用其元素的 == 操作符
p.first 返回 p 中名为 first 的(公有)数据成员
p.second 返回 p 的名为 second 的(公有)数据成员

关联容器还定义了类型:key_type:此容器类型的关键字类型;mapped_type:每个关键字关联的类型,只适用于map;value_type:对于set,与key_type相同,对于map,为pair<const key_type, mapped_type>。

注意:

  • map的value_type是一个pair,我们可以改变pair的值,但不能改变关键字的值。
  • 虽然set定义了iterator和const_iterator类型,但是这两种类型都只能读set中的元素,而不允许改变它的值。

我们通常不对关联容器使用泛型算法。因为,首先关键字是const表示不能将它传递给修改或重排容器元素的算法;虽然关联容器可用于只读算法,但是,这类算法很多要搜索序列,而关联容器中元素不能通过关键字进行快速查找(O(1)),因此,对其使用泛型搜索算法几乎总是坏主意。例如,泛型算法中的find,它会顺序搜索,但是关联容器定义了专用的find成员函数,它的效率会比泛型算法find快很多。

 给容器添加元素

使用insert成员向容器添加元素或元素范围。

vector<int> arr = {0,1,2,3,4};
set<int> st;//空集合
st.insert(arr.begin(),arr.end());//添加一个范围
st.insert(5);//添加一个元素
map<string,int>mp;
mp.insert({"hh",1});
mp["tt"] = 3;

insert返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace返回一个pair,pair的第一个元素是指向给定关键字的迭代器,另一个是bool类型的变量,表明插入成功与否,如果元素已存在,insert什么也不做,返回值bool部分为false。

m.insert(e) e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则插入一个值为 e.second 的新元素;如果该键在 m 中已存在,则保持 m 不变。该函数返回一个 pair 类型对象,包含指向键为 e.first 的元素的 map 迭代器,以及一个 bool 类型的对象,表示是否插入了该元素
m.insert(beg,end) beg 和 end 是标记元素范围的迭代器,其中的元素必须为 m.value_type 类型的键-值对。对于该范围内的所有元素,如果它的键在 m 中不存在,则将该键及其关联的值插入到 m。返回void 类型
m.insert(iter, e) e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则创建新元素,并以迭代器 iter 为起点搜索新元素存储的位置。返回一个迭代器,指向 m 中具有给定键的元素

统计单词出现次数的程序:

map<string, int> word_count;
string word;
while (cin >> word)
{
    auto ret = word_count.insert({ word, 1 });
    if (!ret.second)//如果没插入成功,证明原来已经存在键值,将统计值+1
    {
        ++ret.first->second;// first是一个迭代器,指向插入的键
    }
}

map的下标操作

map和unordered_map提供下标操作和对应的函数at。set不支持下标操作,因为没有与关键字关联的"值",multimap和unordered_multimap也没有下标操作,因为有多个值与一个关键字关联。

使用下标访问 map 与使用下标访问数组或 vector 的行为截然不同:用下标访问不存在的元素将导致在 map 容器中添加一个新元素,它的键即为该下标值。通常来说,下标操作符返回左值。它返回的左值是特定键所关联的值。

有别于 vector 或 string 类型,map 下标操作符返回的类型与对 map 迭代器进行解引用获得的类型不相同。显然,map 迭代器返回 value_type 类型的值——包含 const key_type 和 mapped_type 类型成员的 pair 对象;下标操作符则返回一个 mapped_type 类型的值。

删除元素

  • c.erase(k) 删除m中键为k的元素。返回值为被删除元素的个数,对于map容器而言,其值必然是0或1。
  • c.erase(p) 从m中删除迭代器p所指向的元素。返回值为void类型。
  • c.erase(b,e) 从m中删除一段由一对迭代器范围的元素。返回值为void类型。

访问元素

关联容器提供多种查找指定元素的方法。如果只关心元素是否在容器中,使用find最好。对于不允许重复关键字的容器,find和count没有什么区别;但是对于允许重复关键字的容器,count可以同具有相同关键字的元素的个数。

下面是关联容器中查找元素的操作:(lower_bound和upper_bound不适用于无序容器)

c.find(k) 返回一个迭代器,指向第一个关键字为k的元素,若k不存在,则返回尾后迭代器
c.count(k) 返回关键字为k的元素的数量;不允许重复关键字的容器,返回值永远是0或1
c.lower_bound(k) 返回一个迭代器,指向第一个关键字不小于k的元素
c.upper_bound(k) 返回一个迭代器,指向第一个关键字大于k的元素
c.equal_range(k) 返回一个迭代器pair,表示关键字等于k的范围,若k不存在,pair的两个成员均等于c.end()

由于map中下标操作会将不存在的关键字插入到map中,当我们不需要查找时去插入关键字时,就不能使用它,此时需要使用find函数。

如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。此时我们要找到某个关键字的所有元素时,有下面三种方法:

方法一:通过count和find来找到关键字对应的所有元素

//建立作者及其作品的容器
    while(true){
        string author, work;
        cout << "enter authors name or end the input" << endl;
        if(!getline(cin, author))break;
        cout << "enter authors works" << endl;
        while (cin >> work)authors.insert(make_pair(author, work));
        cin.clear();
    }
    //需要查找的关键字(作者)
    string search_item("Alain de Botton");

    //统计该关键字中元素的个数,即作品数
    auto entries = authors.count(search_item);
    //作者的第一部作品
    auto iter = authors.find(search_item);

    //循环找到所有作品
    while (entries){
        cout << iter->second << endl;//输出每个作品
        ++iter;
        --entries;
    }

方法二:通过lower_bound和upper_bound来找到关键字对应的所有元素

lower_bound返回第一个具有该关键字的元素位置,upper_bound返回最后一个与该关键字匹配的元素位置之后的一个位置。如果没有该关键字,则两个函数返回值相同;当关键字大于容器中的任何一个关键字,则会返回尾后迭代器。

//循环找到所有作品
    for (auto be = authors.lower_bound(search_item), ed = authors.upper_bound(search_item); be != ed; ++be){
        cout << be->second << endl;//输出每个作品
    }

方法三:通过equal_range来找到关键字对应的所有元素

本质上和第二个方法相同。

//循环找到所有作品
    for (auto range = authors.equal_range(search_item); range.first != range.second; ++range.first){
        cout << range.first->second << endl;//输出每个作品
    }

单词转换程序的实例:http://blog.csdn.net/jy_95/article/details/47807789

无序容器

新标准定义了四个无序容器:unordered开头的容器,使用hash函数来构造。

无序容器也支持与有序容器相同的操作,例如insert和find;因此通常可以使用无序容器来替换有序容器。

类    型
X::hasher Hash,一个这样的二元函数对象,即如果hf的类型为Hash,k的类型为Key,则hf(k) 的类型为std::size_t
X::local_iterator 一个类型与X::iterator相同的迭代器,但只能用于一个桶
X::const_local_iterator 一个类型与X::const_iterator相同的迭代器,但只能用于一个桶

管理桶

由于无序关联容器的实现是基于hash函数,那么就需要解决冲突问题,解决冲突的办法比较常用有开放地址法和拉链法。在C++的unordered_map当中,采用的是”桶”的方法,也就是出现冲突的元素都存放在一个桶里。

无序容器存储上组织为一个桶,可以存放零个或多个元素(因此,不同关键字的元素也可能在一个桶中,此时定位到那个桶后,在顺序搜索桶中的元素来找到目标元素),如果允许重复关键字,则关键字相同的元素也存放在一个桶中。因此,无序容器的性能取决于hash函数的质量和桶的数目和大小。

下面是管理桶的一些函数:

操    作 描    述
X(n, hf, eq)

创建一个至少包含n个桶的空容器,并将hf用作哈希函数,将eq用作键值相等谓词。

如果省略了eq,则将key_equal( )用作键值相等谓词;如果也省略了hf,则将hasher( )用作哈希函数

X a(n, hf, eq)

创建一个名为a的空容器,它至少包含n个桶,并将hf用作哈希函数,将eq用作键值相等谓词。

如果省略eq,则将key_equal( )用作键值相等谓词;如果也省略了hf,则将hasher( )用作哈希函数

X(i, j, n, hf, eq)

创建一个至少包含n个桶的空容器,将hf用作哈希函数,将eq用作键值相等谓词,并插入区间[i, j]中的元素。

如果省略了eq,将key_equal( )用作键值相等谓词;如果省略了hf,将hasher( )用作哈希函数;如果省略了n,则包含桶数不确定

X a(i, j, n, hf, eq)

创建一个名为a的的空容器,它至少包含n个桶,将hf用作哈希函数,将eq用作键值相等谓词,并插入区间[i, j]中的元素。

如果省略了eq,将key_equal( )用作键值相等谓词;如果省略了hf,将hasher( )用作哈希函数;如果省略了n,则包含桶数不确定

b.hash_function( ) 返回b使用的哈希函数
b.key_eq( ) 返回创建b时使用的键值相等谓词
桶接口
b.bucket_count( ) 返回b包含的桶数
b.max_bucket_count ( ) 返回一个上限数,它指定了b最多可包含多少个桶
b.bucket(k) 返回键值为k的元素所属桶的索引
b.bucket_size(n) 返回索引为n的桶可包含的元素数
桶迭代
b.begin(n) 返回一个迭代器,它指向索引为n的桶中的第一个元素
b.end(n) 返回一个迭代器,它指向索引为n的桶中的最后一个元素
b.cbegin(n) 返回一个常量迭代器,它指向索引为n的桶中的第一个元素
b.cend(n) 返回一个常量迭代器,它指向索引为n的桶中的最后一个元素
hash策略
b.load_factor() 返回每个桶包含的平均元素数
b.max_load_factor() 返回负载系数的最大可能取值;超过这个值后,容器将增加桶
b.max_load_factor(z) 可能修改最大负载系统,建议将它设置为z
a.rehash(n) 将桶数调整为不小于n,并确保a.bucket_count( )> a.size( ) / a.max_load_factor( )
a.reserve(n) 等价于a.rehash(ceil(n/a.max_load_factor( ))),其中ceil(x)返回不小于x的最小整数

默认情况下,无序容器使用关键字类型的==运算符来比较元素,它们还使用一个hash<key_type>类型对象来生成每个元素的hash值。(标准库为每个内置对象,包括指针提供了hash模板;并且,为一些标准库类型定义了hash,例如string、智能指针)

那么如何使用自定义类型定义无序容器:

size_t hasher(const Sales &sl){
    return hash<int>()(sl.id);//hash<int>是hash函数的函数对象size_t operator()(int _x)
}

bool equalId(const Sales &s1, const Sales &s2){
    return s1.id == s2.id;
}
unordered_multiset<Sales, decltype(hasher)*, decltype(equalId)*>bookstore(10, hasher, equalId);//参数是桶大小、哈希函数指针、相等性判断
原文地址:https://www.cnblogs.com/yeqluofwupheng/p/6865956.html