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

被折叠的 条评论
为什么被折叠?



