STL中_Rb_tree的探索

我们知道STL中我们常用的setmultisetmapmultimap都是基于红黑树。本文介绍了它们的在STL中的底层数据结构_Rb_tree的直接用法与部分函数。难点主要是_Rb_tree的各个参数的确定。

特别注意在如下代码的Selector类用于从Node中选出用于排序的key值,这个仿函数必须返回const int&而不能是int,否则less<int>::operator(const int&, const int&)会抛出segmentation fault。一开始写成int,看了很多源码才发现是这个原因,一定要注意。(Update on 2020-5-6:通过对源码阅读,终于彻底明白为何出现写int会出现问题,见文末)

接下来是样例代码,里面都有注释了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <iomanip>

// 原则上不要直接引用这个头文件,这里只是为了测试
#include <bits/stl_tree.h>

using namespace std;

struct Node {
int first, second;
Node(int _first, int _second) : first(_first), second(_second){};

friend ostream& operator<<(ostream& outs, const Node& node) {
outs << '{' << node.first << ',' << node.second << '}';
return outs;
}
};

template <class T>
struct Selector {
// MUST return const int&, not int.
// if return int, segmentation fault will occur.
// I have spent much time because of this.
const int& operator()(const T& obj) const {
return obj.first;
}
};

int main() {
// _Rb_tree: red-black tree in STL.
using tree_type = _Rb_tree<int, Node, Selector<Node>, less<int>>;
using iterator_type = tree_type::iterator;
using result_pair_type = pair<tree_type::iterator, bool>;
tree_type tree;

// 插入元素Node(1, 2)
result_pair_type res = tree._M_insert_unique(Node(1, 2));
cout << "insert address = " << res.first._M_node << endl;
cout << "insert result = " << boolalpha << res.second << endl; // true

iterator_type it = tree.begin();
cout << "begin address = " << it._M_node << endl;

it = tree.find(1);
cout << "address = " << it._M_node << ", value = " << *it << endl;

// 再插入元素Node(1, 2)但是因为调用的是insert_unique
// 它不会添加重复值,所以插入会被拒绝
res = tree._M_insert_unique(Node(1, 2));
cout << "insert result = " << boolalpha << res.second << endl; // false

// 再插入元素Node(1, 2)但这次调用insert_equal
// multiset和multimap就是利用这个函数来插入重复值
// 也就是这个函数允许重复值,所以插入成功
tree._M_insert_equal(Node(1, 3));
cout << "size = " << tree.size() << endl; // 大小就变为2

pair<iterator_type, iterator_type> result = tree.equal_range(1);
for (iterator_type ite = result.first; ite != result.second; ++ite) {
cout << "address = " << ite._M_node << ", value = " << *ite << endl;
}

return 0;
}

程序的输出为(内存地址不定):

1
2
3
4
5
6
7
8
insert address = 0xf91be0
insert result = true
begin address = 0xf91be0
address = 0xf91be0, value = {1,2}
insert result = false
size = 2
address = 0xf91be0, value = {1,2}
address = 0xf91c10, value = {1,3}

Update on 2020-5-6

当将Selector::operator()的返回值写为int时,执行时会在main()中的it = tree.find(1)时抛出Segmentation Fault。以下给出调用过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// main.cpp, main()
int main() {
...
it = tree.find(1);
...
}

// stl_tree.h, std::_Rb_tree::find(const _Key&)
template<typename _Key, typename _Val, typename _KeyOfValue,
typename _Compare, typename _Alloc>
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
find(const _Key& __k) {
iterator __j = _M_lower_bound(_M_begin(), _M_end(), __k);
return (__j == end() || _M_impl._M_key_compare(__k,
_S_key(__j._M_node))) ? end() : __j;
}

// stl_tree.h, std::_Rb_tree::_M_lower_bound(_Link_type, _Link_type, const _Key&)
template<typename _Key, typename _Val, typename _KeyOfValue,
typename _Compare, typename _Alloc>
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
_M_lower_bound(_Link_type __x, _Link_type __y, const _Key& __k) {
while (__x != 0)
if (!_M_impl._M_key_compare(_S_key(__x), __k))
__y = __x, __x = _S_left(__x);
else
__x = _S_right(__x);
return iterator(__y);
}

在此处的 _M_impl._M_key_compare 也就是比较仿函数对象,在这里即less<int>对象。异常就是在这个函数里抛出的,可见问题出现在 _S_key(__x) 中。

1
2
3
4
// stl_tree.h, std::_Rb_tree::_S_key(_Const_Base_ptr)
static const _Key& _S_key(_Const_Base_ptr __x) {
return _KeyOfValue()(_S_value(__x));
}

这里的 _KeyOfValue() 就是我们给的 Selector 了。所以在这里如果将 operator() 的返回值写为 int ,则这里将其以 const _Key& 返回,就是返回局部对象的引用,后面又在 less<int>::operator() 里面使用了这个引用(这个引用实际上指向一个已过期的栈上数据: _KeyOfValue() 的返回值),这个行为导致了Segmentation Fault

C++11 新用法

基于哈希的 map 和 set

简述

基于哈希的 mapset ,它们分别叫做 unordered_map, unordered_set 。数据分布越平均,性能相较 mapset 来说提升就更大。但由于它们基于哈希,所以并不像 mapset 一样能自动排序;它们都是无序的。

我做了一个测试:随机生成 $10^7$ 个 int 范围内的整数(平均分布),然后将其分别插入 mapunordered_map,再完整的做一次查询,查看时间和内存上的消耗。

测试结果

结构 总耗时 插入耗时 查询耗时 内存
map 18,041 MS 10,299 MS 7,742 MS 230.7 MB
unordered_map 7,138 MS 5,426 MS 1,712 MS 212.0 MB

当数据分布平均时,从时间上看,两者的性能差距约为 $7138 / 18041 \approx 40\%$

高效随机数据生成-random与chrono

请注意random库与chrono库均是C++11。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <chrono>
#include <iostream>
#include <random>

using namespace std;
typedef long long LL;
typedef unsigned long long ULL;

// 生成 [a, b] 范围内的整数
// 其中 INT_MIN <= a <= b <= INT_MAX
int rand(int a, int b) {
static mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
return uniform_int_distribution<int>(a, b)(rng);
}

// 生成 [a, b] 范围内的整数
// 其中 0 <= a <= b <= UNSIGNED_INT_MAX
unsigned rand(unsigned a, unsigned b) {
static mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
return uniform_int_distribution<unsigned>(a, b)(rng);
}

// 生成 [a, b] 范围内的整数
// 其中 LONG_LONG_MIN <= a <= b <= LONG_LONG_MAX
LL rand(LL a, LL b) {
static mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
return uniform_int_distribution<LL>(a, b)(rng);
}

// 生成 [a, b] 范围内的整数
// 其中 0 <= a <= b <= UNSIGNED_LONG_LONG_MAX
ULL rand(ULL a, ULL b) {
static mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
return uniform_int_distribution<ULL>(a, b)(rng);
}

int main() {
const LL MAX = 1e10;
cout << rand(-MAX, MAX);
return 0;
}

bitset

用处

一含有特定二进制位数 N 的容器,相当于 bool 数组,但压缩内存至每一个二进制位。

申明

bitset<N> bset,其中 N 为字面常量,表示 bitset 中的二进制位个数。如 bitset<10> bset;

常用方法

bitset(unsigned long val)val 构造一个对应的 bitset (至 C++11 前)

bitset(unsigned long long val)val 构造一个对应的 bitset (自 C++11 始)

bitset(string val)val 构造一个对应的 bitset

bool operator[] 获取特定二进制位的值

bool test(pos) 获取特定二进制位的值,但有越界检查:如果越界,抛出 std::out_of_range 异常。

multimap与multiset

multimap

介绍

基本与 map 相同,但除去了 operator[], 因为其特点是相同 key 的值在 multimap 中可以多个存在。需要强调的,与 map 有明显不同的常用方法:

常用方法

iterator insert(value) 插入键值对,返回插入后该元素的迭代器

size_type erase(key) 删除所有关键字与 key 相等的键值对,返回删除的元素个数

void erase(iterator pos) 删除对应位置元素

size_type count(key) 统计关键字为 key 的元素个数

iterator find(key) 查找关键字与 key 相等的键值对,返回对应位置的迭代器。如果不存在则返回 end()如果有多个关键字与 key 相等的键值对,返回的迭代器指向哪一个不定。

pair<iterator, iterator> equal_range(key) 查找关键字与 key 相等的键值对,返回迭代器对,描述与 key 相等的键值对的区间 [first, last)

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <map>

using namespace std;

typedef multimap<int, char>::iterator iterType;

int main() {
multimap<int, char> mp;
mp.insert(make_pair(1, 'A'));
mp.insert(make_pair(2, 'B'));
mp.insert(make_pair(2, 'C'));
mp.insert(make_pair(2, 'D'));
mp.insert(make_pair(4, 'E'));
mp.insert(make_pair(3, 'F'));
pair<iterType, iterType> range = mp.equal_range(2);
for (iterType it = range.first; it != range.second; ++it) {
cout << it->first << ' ' << it->second << '\n';
}
return 0;
}
1
2
3
4
5
output

2 B
2 C
2 D

multiset

介绍

与上述 multimap 的用法基本相同,只是每个元素只有一个值 key,而没有对应的 value

二叉堆基础与优先队列

堆是一种数据结构,而priority_queue (优先队列)是一个基于二叉堆的STL容器。

堆有很多种类,但以下提及的堆均为二叉堆。

以下是一个例子。将新元素20插入二叉堆:




所以对一个二叉堆,有3种基本操作:

  1. 插入:将 val 插入堆。

  2. 取堆顶:查看堆顶元素值。(即整个堆中最大或最小值)

  3. 出堆:将堆顶元素出堆。

FJUTOJ-3682-LRU算法的实现2

传送门

此题让我们实现一个LRU的模板类。本题较简便且高效的写法是维护一个std::list和一个std::unordered_map

std::list 与 std::unordered_map 中存放的内容

std::list中存放各key,类型为K。链表中各键码存放的顺序是按照访问顺序存放的

std::unordered_map中以key为第一维,第二维为一个pair,其firstsecond分别为:

first: 该key对应的value。

second:该key在std::list中的迭代器方便访问。

为方便,下面用“链表”来指代std::list,用“哈希表”来指代std::unordered_map

各操作实现

insert操作:用哈希表判断该键是否已经存在。若存在,先在链表中删除该key,然后再新加一个该key到链表尾部,并更新在哈希表中的value和链表的迭代器。若不存在,则直接加至链表尾部,并在哈希表中插入该key,伴随着对应的value和链表迭代器。

get操作:直接从哈希表中获得其value即可。代码实现未检测该key是否存在,严谨来说应该加上异常处理。

contains操作:直接在哈希表中查询是否存在该key即可。

vis操作:用哈希表判断该键是否存在。若不存在,则本操作无效。否则,将该键从链表中删除,然后再将其加至链表尾部,并更新哈希表中对应链表迭代器。

pop操作:判断是否整个容器已经为空。若为空,则本操作无效。否则,将链表头部元素从链表中删除,并在哈希表中删除对应键值信息。

remove操作:用哈希表判断该键是否存在。若不存在,则本操作无效。否则,将该键从链表中删除,并在哈希表中删除对应键值信息。

empty操作:哈希表或链表判空即可。

size操作:取哈希表或链表大小即可。

clear操作:清空哈希表和链表即可。

时间复杂度

各操作基于对链表和哈希表的修改。期望复杂度均为$O(1)$。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×