100、深入理解无序关联容器

深入理解无序关联容器

1. 成员函数模板与重载解析

某些成员函数模板(如 find count contains lower_bound upper_bound equal_range )只有在 Compare::is_transparent 有效且表示一个类型时,才会参与重载解析。

对于关联容器的推导指南,如果满足以下任何一个条件,则不参与重载解析:
- 具有 InputIterator 模板参数,且为该参数推导出的类型不符合输入迭代器的要求。
- 具有 Allocator 模板参数,且为该参数推导出的类型不符合分配器的要求。
- 具有 Compare 模板参数,且为该参数推导出的类型符合分配器的要求。

2. 关联容器的异常安全保证

  • clear() 函数不会抛出异常。
  • erase(k) 函数除非容器的 Compare 对象抛出异常,否则不会抛出异常。
  • 在插入单个元素的 insert emplace 函数中,如果任何操作抛出异常,插入操作将无效。
  • swap 函数除非容器的 Compare 对象的 swap 操作抛出异常,否则不会抛出异常。

3. 无序关联容器概述

无序关联容器提供了基于键快速检索数据的能力。大多数操作的最坏情况复杂度是线性的,但平均情况要快得多。标准库提供了四种无序关联容器: unordered_set unordered_map unordered_multiset unordered_multimap

这些容器符合容器的要求,但 a == b a != b 的语义与其他容器类型不同。每个无序关联容器由键 Key 、满足 Cpp17Hash 要求的哈希函数对象类型 Hash 和诱导键值等价关系的二元谓词 Pred 参数化。 unordered_map unordered_multimap 还将任意映射类型 T 与键关联起来。

3.1 哈希函数和键相等谓词

容器的 Hash 类型对象称为哈希函数, Pred 类型对象称为键相等谓词。如果两个键 k1 k2 满足键相等谓词 pred(k1, k2) 有效且返回 true ,则它们被认为是等价的。在这种情况下,哈希函数必须为它们返回相同的值。

3.2 唯一键和等价键

无序关联容器支持唯一键(每个键最多包含一个元素)或等价键。 unordered_set unordered_map 支持唯一键, unordered_multiset unordered_multimap 支持等价键。在支持等价键的容器中,等价键的元素在迭代顺序中相邻。

3.3 值类型和迭代器

对于 unordered_set unordered_multiset ,值类型与键类型相同;对于 unordered_map unordered_multimap ,值类型是 pair<const Key, T> 。在键类型和值类型相同的无序关联容器中, iterator const_iterator 都是常量迭代器。

3.4 桶的组织

无序关联容器的元素组织成桶,具有相同哈希码的键出现在同一个桶中。随着元素的添加,桶的数量会自动增加,以保持每个桶的平均元素数量低于一个界限。重新哈希操作会使迭代器失效,改变元素的顺序和桶的分配,但不会使指向元素的指针或引用失效。

4. 无序关联容器的要求

以下是无序关联容器的一些常见要求和操作:

表达式 返回类型 断言/注释 复杂度 前置/后置条件
X::key_type Hash::transparent_key_equal (如果有效);否则为 Pred 编译时 - -
X::mapped_type (仅 unordered_map unordered_multimap T 编译时 - -
X::value_type (仅 unordered_set unordered_multiset Key 要求: value_type 可从 X 中擦除 编译时 -
X::value_type (仅 unordered_map unordered_multimap pair<const Key, T> 要求: value_type 可从 X 中擦除 编译时 -
X::hasher Hash Hash 应为一元函数对象类型, hf(k) 类型为 size_t 编译时 -
X::key_equal Hash::transparent_key_equal (如果有效);否则为 Pred 要求: Pred 可复制构造,是二元谓词且为等价关系 编译时 -
X::local_iterator 迭代器类型 可用于遍历单个桶,但不能跨桶遍历 编译时 -
X::const_local_iterator 迭代器类型 可用于遍历单个桶,但不能跨桶遍历 编译时 -
X::node_type 节点句柄类模板的特化 公共嵌套类型与 X 中对应类型相同 编译时 见 21.2.4
X(n, hf, eq) X 构造一个至少有 n 个桶的空容器,使用 hf 作为哈希函数, eq 作为键相等谓词 O(n) -
X(n, hf) X 要求: key_equal 可默认构造。构造一个至少有 n 个桶的空容器,使用 hf 作为哈希函数, key_equal() 作为键相等谓词 O(n) -
X(n) X 要求: hasher key_equal 可默认构造。构造一个至少有 n 个桶的空容器,使用 hasher() 作为哈希函数, key_equal() 作为键相等谓词 O(n) -
X() X 要求: hasher key_equal 可默认构造。构造一个具有未指定数量桶的空容器,使用 hasher() 作为哈希函数, key_equal() 作为键相等谓词 常量 -
X(i, j, n, hf, eq) X 要求: value_type 可从 *i 构造到 X 中。构造一个至少有 n 个桶的空容器,使用 hf 作为哈希函数, eq 作为键相等谓词,并插入 [i, j) 范围内的元素 平均情况 O(N) N distance(i, j) ),最坏情况 O(N^2) -

4.1 插入操作

插入操作包括 emplace insert 系列函数,不同的插入函数有不同的返回类型和行为:

graph TD;
    A[插入操作] --> B[emplace];
    A --> C[insert];
    B --> B1[a_uniq.emplace];
    B --> B2[a_eq.emplace];
    B --> B3[a.emplace_hint];
    C --> C1[a_uniq.insert];
    C --> C2[a_eq.insert];
    C --> C3[a.insert(p, t)];
    C --> C4[a.insert(i, j)];
    C --> C5[a.insert(il)];
    C --> C6[a_uniq.insert(nh)];
    C --> C7[a_eq.insert(nh)];
    C --> C8[a.insert(q, nh)];

4.2 提取和合并操作

  • extract(k) :移除容器中键等价于 k 的元素,并返回一个拥有该元素的 node_type (如果找到),否则返回一个空的 node_type
  • extract(q) :移除迭代器 q 指向的元素,并返回一个拥有该元素的 node_type
  • merge(a2) :尝试提取 a2 中的每个元素,并使用 a 的哈希函数和键相等谓词将其插入到 a 中。

4.3 查找和计数操作

查找和计数操作可用于快速定位和统计元素:
- find(k) :返回指向键等价于 k 的元素的迭代器,如果不存在则返回 end()
- count(k) :返回键等价于 k 的元素的数量。
- contains(k) :等价于 find(k) != end()
- equal_range(k) :返回包含所有键等价于 k 的元素的范围。

4.4 桶相关操作

桶相关操作可用于管理和查询桶的信息:
- bucket_count() :返回容器包含的桶的数量。
- max_bucket_count() :返回容器可能包含的桶的数量上限。
- bucket(k) :返回键等价于 k 的元素所在桶的索引。
- bucket_size(n) :返回第 n 个桶中的元素数量。

4.5 负载因子操作

负载因子相关操作可用于控制容器的负载:
- load_factor() :返回每个桶的平均元素数量。
- max_load_factor() :返回容器尝试保持的最大负载因子。
- max_load_factor(z) :可能会更改容器的最大负载因子,使用 z 作为提示。
- rehash(n) :确保桶的数量满足一定条件。
- reserve(n) :等价于 rehash(ceil(n / max_load_factor()))

5. 无序容器的比较

两个无序容器 a b 相等的条件是 a.size() == b.size() ,并且对于 a 中每个等价键组 [Ea1, Ea2) ,在 b 中存在一个等价键组 [Eb1, Eb2) ,使得 is_permutation(Ea1, Ea2, Eb1, Eb2) 返回 true 。比较操作的复杂度在不同情况下有所不同。

5.1 迭代器和有效性

无序关联容器的迭代器类型至少是前向迭代器。插入和 emplace 操作不会影响容器元素引用的有效性,但可能会使所有迭代器失效。擦除操作只会使被擦除元素的迭代器和引用失效,并保留未擦除元素的相对顺序。

插入和 emplace 操作在满足 (N+n) <= z * B 条件时不会影响迭代器的有效性,其中 N 是插入操作前容器中的元素数量, n 是插入的元素数量, B 是容器的桶数量, z 是容器的最大负载因子。提取操作只会使被移除元素的迭代器失效,并保留未擦除元素的相对顺序,指向被移除元素的指针和引用仍然有效。

6. 插入操作详解

6.1 emplace 系列

  • a_uniq.emplace(args)
    • 要求 value_type 必须能从 args 构造到容器 X 中。
    • 效果 :当且仅当容器中不存在与构造的 value_type 对象 t 键等价的元素时,插入 t 。返回的 pair 中, bool 部分表示插入是否成功, iterator 部分指向与 t 键等价的元素。
    • 复杂度 :平均情况为 $O(1)$,最坏情况为 $O(a_uniq.size())$。
  • a_eq.emplace(args)
    • 要求 value_type 必须能从 args 构造到容器 X 中。
    • 效果 :插入构造的 value_type 对象 t ,并返回指向新插入元素的迭代器。
    • 复杂度 :平均情况为 $O(1)$,最坏情况为 $O(a_eq.size())$。
  • a.emplace_hint(p, args)
    • 要求 value_type 必须能从 args 构造到容器 X 中。
    • 效果 :等价于 a.emplace(std::forward<Args>(args)...) 。返回指向与新插入元素键等价的元素的迭代器, const_iterator p 是搜索起始位置的提示,实现可以忽略该提示。
    • 复杂度 :平均情况为 $O(1)$,最坏情况为 $O(a.size())$。

6.2 insert 系列

操作 要求 效果 复杂度
a_uniq.insert(t) t 为非 const 右值, value_type 需可移动插入到 X 中;否则需可复制插入到 X 中。 当且仅当容器中不存在与 t 键等价的元素时,插入 t 。返回的 pair 中, bool 部分表示插入是否成功, iterator 部分指向与 t 键等价的元素。 平均情况为 $O(1)$,最坏情况为 $O(a_uniq.size())$
a_eq.insert(t) t 为非 const 右值, value_type 需可移动插入到 X 中;否则需可复制插入到 X 中。 插入 t ,并返回指向新插入元素的迭代器。 平均情况为 $O(1)$,最坏情况为 $O(a_eq.size())$
a.insert(p, t) t 为非 const 右值, value_type 需可移动插入到 X 中;否则需可复制插入到 X 中。 等价于 a.insert(t) 。返回指向与 t 键等价的元素的迭代器, iterator p 是搜索起始位置的提示,实现可以忽略该提示。 平均情况为 $O(1)$,最坏情况为 $O(a.size())$
a.insert(i, j) value_type 必须能从 *i 构造到 X 中,且 i j 不是容器 a 中的迭代器。 等价于对 [i, j) 中的每个元素执行 a.insert(t) 平均情况为 $O(N)$( N distance(i, j) ),最坏情况为 $O(N(a.size() + 1))$
a.insert(il) - 等价于 a.insert(il.begin(), il.end()) a.insert(il.begin(), il.end())
a_uniq.insert(nh) nh 为空或 a_uniq.get_allocator() == nh.get_allocator() nh 为空,无效果;否则,当且仅当容器中不存在与 nh.key() 键等价的元素时,插入 nh 拥有的元素。 平均情况为 $O(1)$,最坏情况为 $O(a_uniq.size())$
a_eq.insert(nh) nh 为空或 a_eq.get_allocator() == nh.get_allocator() nh 为空,无效果并返回 a_eq.end() ;否则,插入 nh 拥有的元素并返回指向新插入元素的迭代器。 平均情况为 $O(1)$,最坏情况为 $O(a_eq.size())$
a.insert(q, nh) nh 为空或 a.get_allocator() == nh.get_allocator() nh 为空,无效果并返回 a.end() ;否则,在唯一键容器中,当且仅当不存在与 nh.key() 键等价的元素时插入;在等价键容器中,总是插入。返回指向与 nh.key() 键等价的元素的迭代器, iterator q 是搜索起始位置的提示,实现可以忽略该提示。 平均情况为 $O(1)$,最坏情况为 $O(a.size())$

7. 提取和合并操作详解

7.1 提取操作

  • a.extract(k)
    • 效果 :移除容器中键等价于 k 的元素。若找到,返回一个拥有该元素的 node_type ;否则返回一个空的 node_type
    • 复杂度 :平均情况为 $O(1)$,最坏情况为 $O(a.size())$。
  • a.extract(q)
    • 效果 :移除迭代器 q 指向的元素,并返回一个拥有该元素的 node_type
    • 复杂度 :平均情况为 $O(1)$,最坏情况为 $O(a.size())$。

7.2 合并操作

  • a.merge(a2)
    • 要求 a.get_allocator() == a2.get_allocator()
    • 效果 :尝试提取 a2 中的每个元素,并使用 a 的哈希函数和键相等谓词将其插入到 a 中。在唯一键容器中,若 a 中存在与 a2 中元素键等价的元素,则该元素不会从 a2 中提取。
    • 复杂度 :平均情况为 $O(N)$( N a2.size() ),最坏情况为 $O(N * a.size() + N)$。
graph TD;
    A[操作] --> B[提取操作];
    A --> C[合并操作];
    B --> B1[a.extract(k)];
    B --> B2[a.extract(q)];
    C --> C1[a.merge(a2)];

8. 查找和计数操作详解

操作 效果 复杂度
b.find(k) 返回指向键等价于 k 的元素的迭代器,若不存在则返回 b.end() 平均情况为 $O(1)$,最坏情况为 $O(b.size())$
a_tran.find(ke) 返回指向键等价于 ke 的元素的迭代器,若不存在则返回 a_tran.end() 平均情况为 $O(1)$,最坏情况为 $O(a_tran.size())$
b.count(k) 返回键等价于 k 的元素的数量。 平均情况为 $O(b.count(k))$,最坏情况为 $O(b.size())$
a_tran.count(ke) 返回键等价于 ke 的元素的数量。 平均情况为 $O(a_tran.count(ke))$,最坏情况为 $O(a_tran.size())$
b.contains(k) 等价于 b.find(k) != b.end() 平均情况为 $O(1)$,最坏情况为 $O(b.size())$
a_tran.contains(ke) 等价于 a_tran.find(ke) != a_tran.end() 平均情况为 $O(1)$,最坏情况为 $O(a_tran.size())$
b.equal_range(k) 返回包含所有键等价于 k 的元素的范围,若不存在则返回 make_pair(b.end(), b.end()) 平均情况为 $O(b.count(k))$,最坏情况为 $O(b.size())$
a_tran.equal_range(ke) 返回包含所有键等价于 ke 的元素的范围,若不存在则返回 make_pair(a_tran.end(), a_tran.end()) 平均情况为 $O(a_tran.count(ke))$,最坏情况为 $O(a_tran.size())$

9. 桶和负载因子操作详解

9.1 桶相关操作

操作 效果 复杂度
b.bucket_count() 返回容器包含的桶的数量。 常量
b.max_bucket_count() 返回容器可能包含的桶的数量上限。 常量
b.bucket(k) 返回键等价于 k 的元素所在桶的索引(要求 b.bucket_count() > 0 )。 常量
b.bucket_size(n) 返回第 n 个桶中的元素数量(要求 n [0, b.bucket_count()) 范围内)。 $O(b.bucket_size(n))$
b.begin(n) 返回指向第 n 个桶中第一个元素的迭代器(要求 n [0, b.bucket_count()) 范围内)。 常量
b.end(n) 返回第 n 个桶的尾后迭代器(要求 n [0, b.bucket_count()) 范围内)。 常量
b.cbegin(n) 返回指向第 n 个桶中第一个元素的常量迭代器(要求 n [0, b.bucket_count()) 范围内)。 常量
b.cend(n) 返回第 n 个桶的尾后常量迭代器(要求 n [0, b.bucket_count()) 范围内)。 常量

9.2 负载因子操作

操作 效果 复杂度
b.load_factor() 返回每个桶的平均元素数量。 常量
b.max_load_factor() 返回容器尝试保持的最大负载因子。 常量
a.max_load_factor(z) 可能会更改容器的最大负载因子,使用 z 作为提示(要求 z 为正数)。 常量
a.rehash(n) 确保 a.bucket_count() >= a.size() / a.max_load_factor() a.bucket_count() >= n 平均情况为线性于 a.size() ,最坏情况为二次方
a.reserve(n) 等价于 a.rehash(ceil(n / a.max_load_factor())) 平均情况为线性于 a.size() ,最坏情况为二次方

10. 总结

无序关联容器在数据检索方面具有高效性,尤其是在平均情况下。不同的操作有不同的复杂度和要求,在使用时需要根据具体场景进行选择。插入和提取操作可能会影响迭代器和引用的有效性,需要特别注意。同时,桶和负载因子的管理对于容器的性能也至关重要,可以通过相关操作进行调整。在进行容器比较时,要满足特定的条件,并且复杂度也因容器类型而异。通过深入理解这些特性,可以更好地利用无序关联容器来解决实际问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值