stl container usage tips

24
STL Container Usage Tips Yongquan 2015-5

Upload: -

Post on 20-Jan-2017

16 views

Category:

Documents


4 download

TRANSCRIPT

Page 1: STL Container Usage Tips

STL Container Usage Tips

Yongquan2015-5

Page 2: STL Container Usage Tips
Page 3: STL Container Usage Tips

std::vector object model

关于 vector这种容器,需要澄清的几个概念或者认识:1. 对于 vector来说,一个元素是否属于某个给定的 vector,并不是以该元素的值为评判标准的,而是以它的地址为标准。这就是为什么 vector没有 find函数的原因;2. 迭代器失效的真正含义有这样几种情况: (1) 它指向的对象被彻底销毁了,它成了野指针; (2) 它虽然不是野指针,但是它指向的对象已经不是原来的那个对象了; (3) 它虽然不是野指针,但是它指向的对象已经不属于原来的那个容器了,这个就是归属问题;3. 在调用 insert、 erase、 assign、 resize、 swap等函数之后都可能使先前获得的迭代器、指针、引用失效。例如,它们都可能使先前获得的迭代器 end()失效,即不再指向现在的 end()。特别是当发生内存重分配的情况下以及调用 swap()函数以后,所有已经获得的迭代器等都将失效;4. 具体地,凡是增加元素的操作都可能使先前获得的迭代器等失效,但是站在我们开发人员的角度,我们并不清楚哪次增加会导致内存重分配,所以为保险起见还是重新获取迭代器为妙。但是,删除元素操作不会导致容量缩减,因此不需要内存重分配,先前获得的迭代器、指针、引用等可能并不真的失效,不过它们未必仍然指向原来的元素对象(如果是删除末尾元素,则原来指向它的迭代器失效)。

T1 T2 T3 T4 T5 T6

start

fi ni sh

end_of _storage

std: : vector<T>

3

Page 4: STL Container Usage Tips

std::vector如果需要在遍历 vector的过程中删除特定的元素,那么下面的代码有没有问题?std::vector<int> V; // 假设有一个 vector容器………… // 添加很多元素std::vector<int>::iterator itFirst = V.begin();std::vector<int>::iterator const itLast = V.end();while (itFirst != itLast){ if (*itFirst == 100) itFirst = V.erase(itFirst); // 返回逻辑上的下一个元素位置 else ++itFirst;}

4

有问题!

Page 5: STL Container Usage Tips

std::vector这样呢?std::vector<int> V; // 假设有一个 vector容器………… // 添加很多元素for (std::vector<int>::iterator itFirst = V.begin(); itFirst != V.end(); ) // 每次循环都重新获取 end(){ if (*itFirst == 100) V.erase(itFirst++); // 先指向下一个元素位置,然后删除当前元素 else ++itFirst;}

5

有问题!

Page 6: STL Container Usage Tips

std::vector正确的方法:std::vector<int> V; // 假设有一个 vector容器………… // 添加很多元素for (std::vector<int>::iterator itFirst = V.begin(); itFirst != V.end(); ) // 每次循环都重新获取 end(){ if (*itFirst == 100) itFirst = V.erase(itFirst); // 返回逻辑上的下一个元素位置 else ++itFirst;}

6

正确!

Page 7: STL Container Usage Tips

std::vector5. vector是序列式容器 (sequenced container)或顺序容器。所谓序列式容器,从概念上讲就是说元素的逻辑顺序由它们的相对位置关系确定而不是由值的大小关系来确定,所以向其中添加元素和删除元素时都必须指出具体操作位置(或者说,不能单纯以值来定位元素,必须用下标或者地址来定位元素。操作位置和地址具体指迭代器)。正因为如此,所以在序列式容器中说 "上一个元素 "和 "下一个元素 "是合理的,有明确的含义;6. 关于函数 erase()的返回类型。 vector的 erase()函数为什么要返回“下一个元素”的 iterator?其一是它确实能够 (如第 5条所述 );其二是它返回“下一个元素”的位置并不费事,复杂度是 O(1)的;其三是能带来方便性;7. begin() 和 end()也是最常用的函数,所以 STL标准要求它们的时间复杂度是

O(1); rbegin()/rend()也一样;8. iterator++/--的时间复杂度也是 O(1)的; reverse_iterator也一样;9. 在非尾端的 insert和 erase操作都会导致若干元素的拷贝动作 (调用拷贝构造函数 ),因此效率比较差 (O(n))。如果需要频繁地在容器的开头或中间进行 insert和 erase等操作,就应该避免使用 vector,应该使用哪种容器?10. Iterator的归属问题和范围问题,示例如下:

7

Page 8: STL Container Usage Tips

typedef std::vector<int> IntVector;IntVector A(10, 5), B(5, 10);IntVector::iterator ix = A.begin();B.erase(ix); // ix并不属于 B!B.insert(ix, 20); // ix并不属于 B!IntVector::iterator it = B.end();it += 2;B.erase(it); // it不在有效范围内!B.insert(it, 20); // it不在有效范围内!int orphan = 30;IntVector::iterator io(&orphan);B.erase(io); // io不属于 B,更不在有效范围内!B.insert(io, 20); // io不属于 B,更不在有效范围内!这个问题编译器无能为力,只能在 runtime时进行检查。 SafeSTL以及较新的MSVC+

+ 提供的 STL都具备这个能力,并且只在 Debug 版本中有效。 Release build时自动去掉检查代码。

std::vector5 5 5 5 5 5 5 5 5 5A:

10B: 10 10 10 10

it

ix

8

Page 9: STL Container Usage Tips

std::list object model

关于 list这种顺序容器,它很多方面都和 vector是一样的。不同的方面有:1. List在任何位置的插入、删除元素动作的复杂度是 O(1)的;2. List的增加元素的操作不会使任何迭代器失效;删除元素的操作除了使指向被删除元素的迭代器、指针、引用失效外,其他元素的迭代器等都不会失效;特别地, end() 始终不会失效,而且只要 list对象没有销毁它的 end()就是不变的,因此我们在循环处理 list中的元素时就不需要反复重新计算 end();

l i st<T> l st1;

_head ●

si ze

al l ocator存储分配器

空白●

●data

●data

●data

●. . .. . .

l st1. end() l st1. begi n() l i st : : Node

l i st容器中的有效元素

●next

●prev

Heap

可静态创建也可动态创建

9

有些实现版本将空白结点直接包含在 list对象内部,并且没有 data域,如 SGI的实现;而有的是用一个指针指向空白结点,如MSVC++的实现。这都是实现细节,并不影响 list的接口语义和性能要求。

Page 10: STL Container Usage Tips

std::list在遍历 list的过程中删除特定的元素,下面的做法都是可以的:std::list<int> L; // 假设有一个 list容器………… // 添加很多元素std::list<int>::iterator itFirst = L.begin(); // 只需一次std::list<int>::iterator const itLast = L.end(); // 只需一次while (itFirst != itLast){ if (*itFirst == 100) itFirst = L.erase(itFirst); // 返回逻辑上的下一个元素位置 else ++itFirst;}

10

正确!

Page 11: STL Container Usage Tips

std::liststd::list<int> L; // 假设有一个 list容器………… // 添加很多元素std::list<int>::iterator itFirst = L.begin();std::list<int>::iterator const itLast = L.end();while (itFirst != itLast){ if (*itFirst == 100) L.erase(itFirst++); else ++itFirst;}

11

正确!

Page 12: STL Container Usage Tips

std::liststd::list<int> L; // 假设有一个 list容器………… // 添加很多元素for (std::list<int>::iterator itFirst = L.begin(); itFirst != V.end(); ){ if (*itFirst == 100) L.erase(itFirst++); else ++itFirst;}

12

正确!

Page 13: STL Container Usage Tips

std::liststd::list<int> L; // 假设有一个 list容器………… // 添加很多元素L.remove(100);

3. List实现了一个双向环状链表;4. Header 结点的作用:方便了 begin()/end()/rbegin()/rend() 的实现,并且保证了它们的复杂度为 O(1);5. List 专 门 提 供的函数: sort(), reverse(), merge(), unique(), remove_if(), remove(),

splice();

13

正确!

Page 14: STL Container Usage Tips

std::set/std::map/rb_tree object model

S. end()

al l ocator

si zepredi cati on

set<i nt> S;

20

header ●

15

25

root

S. begi n() 10

23

_Color

_L _P _R

<NodeBase>

bool _Color

NodeBase *_L

NodeBase *_P

NodeBase *_R

<Tree Node>

_ValueType _V

_Color

_L _P _R5

_Color

_L _P _R30

14

Page 15: STL Container Usage Tips

关于 set/map这种关联式容器,需要澄清的几个概念或者认识:1. Set其实对应的是数学上的集合概念,而我们知道集合的元素是无所谓顺序的。例如任意一个整数集合 S={5, 20, 30, -2, 6, -100},元素是无序的,你不能说元素 20 排在元素 30的前面,也不能说元素 -100 排在元素 6的后面。因此, erase()函数的标准形式是返回

void,而不是所谓的“指向下一个元素的 iterator”;2. map就是一张‘ n 行 2列’的表格,或者说是 pair<key, value>的一个集合,它的 pair同样没有先后顺序;3. 基于二叉平衡搜索树的实现,按照用户指定的比较函数对元素排序 (比如 std::less<T>或者 std::greater<T>),这纯粹是为了提高查找性能,要不然树的空间开销如此之大又是何必呢?计算当前节点的下一个节点即 iterator++ 的时间复杂度并不是 O(1) 的,而是

O(log2N)的(即接近于二叉树的深度,这一点与顺序容器不同); iterator--也是一样。4. 此处,‘下一个’‘上一个’均是指元素自动排序后的逻辑顺序,不是在 Tree上的位置顺序 (实际上, Tree中的结点也没有什么所谓的位置顺序 ),这是实现需要,与 set/map的概念并不矛盾;5. Set/map并非一定要用 rb-tree实现,其实也可以用 list实现;6. 只要 set/map对象还在,其 end()就是不变的, begin()虽然会变但是复杂度总是 O(1)的。因此,就像 list那样,在遍历时也不需要反复计算 end();

std::set/std::map

15

Page 16: STL Container Usage Tips

std::set/std::map基本的复杂度计算

Page 17: STL Container Usage Tips

在遍历 set的过程中删除特定的元素:std::set<int> S; // 假设有一个 set容器………… // 添加很多元素std::set<int>::iterator itFirst = S.begin(); // 只需一次std::set<int>::iterator const itLast = S.end(); // 只需一次while (itFirst != itLast){ if (*itFirst == 100) S.erase(itFirst++); // 返回排序逻辑上的下一个元素位置 else ++itFirst;}

std::set/std::map

17

正确!

Page 18: STL Container Usage Tips

std::set/std::map关于容器嵌套:STL容器的实现基于元素对象的 deep-copy 语义和自身的 deep-copy 语义,因此在拷贝、赋值、传递、排序、合并、拆分时会有很大的 overhead。所以,当元素类型是大对象时,应该避免在容器中直接存放大对象,改为存放它们的智能指针 (auto_ptr<>除外 ),特别是需要嵌套容器的时候。例如:std::map<std::string/*name*/, std::list<BigObject> > M; // 假设有一个嵌套容器std::list<BigObject> L;BigObject a, b, c, d;L.push_back(a); // 添加很多元素L.push_back(b);L.push_back(c);L.push_back(d);M[“ 张三” ] = L;

BigObject A[10];L.assign(A, A+10);M[“ 李四” ] = L;……

M[“ 张三” ].sort();

18

Page 19: STL Container Usage Tips

std::set/std::map建议的做法:typedef std::list<BigObject> BigObjectList;typedef boost::shared_ptr<BigObjectList> BigObjectListSmartPtr;

std::map<std::string/*name*/, BigObjectListSmartPtr> M;BigObjectListSmartPtr pL(new BigObjectList);BigObject a, b, c, d;pL->push_back(a); // 添加很多元素pL->push_back(b);pL->push_back(c);pL->push_back(d);M[“ 张三” ] = pL;

BigObject A[10];BigObjectListSmartPtr pT(new BigObjectList);pT->assign(A, A+10);M[“ 李四” ] = pT;……

M[“ 张三” ]->sort();

19

Page 20: STL Container Usage Tips

Hashtable/unordered_set/unordered_map object model

1. SGI STL hashtable的实现使用开链法来解决 hash函数的结果冲突问题;2. Hashtable没有 reverse_iterator,因此不支持反向遍历;3. begin()的复杂度是 O(N); end()为 O(1); iterator++的复杂度接近 O(1);4. Hashtable对元素不进行自动排序,因此 hashset/hashmap在 C++最新标准中被命名为 unordered_set/unordered_map;

20

Page 21: STL Container Usage Tips

5. Hash函数的质量好坏直接影响到 find()/insert()/erase()函数的性能 ;6. 好的 Hash函数能使得 hashtable的 find()/insert()/erase()等函数的性能接近

O(1),也就是说任何一个单链表的长度都接近 1,而且在 buckets中的分布比较均匀; 最坏的 Hash函数使得任何 key的 hash值都相等即落到同一个 bucket 里,因此使整个hashtable 退化为一个单链表;

7. 为了避免任何一个单链表过长, hashtable实现采取了自动扩张策略,即当 hashtable中包含的元素总数即将超过当前 bucket的数量时,就会重建 buckets vector( 仅需重建 vector即可 )。因此最坏情况下,任何一个单链表的长度都不会超过当前 bucket的数量 ;8. Hashtable 初始化时,取下表中不小于用户给定的 size的最小素数作为 buckets的实际大小 (不同的实现使用不同的素数集合 ): static const unsigned long __stl_prime_list[28] = { 53ul, 97ul, 193ul, 389ul, 769ul, 1543ul, 3079ul, 6151ul, 12289ul, 24593ul, 49157ul, 98317ul, 196613ul, 393241ul, 786433ul, 1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul, 50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul, 1610612741ul, 3221225473ul, 4294967291ul };9. 当 Hashtable需要重建 buckets vector时,有些实现直接按照 2 倍于原来的 size的策略来扩展 (如 MSVC++),而 SGI实现按照上表取下一个素数为新的 size。

Hashtable/unordered_set/unordered_map

Page 22: STL Container Usage Tips

10. 当重建 buckets vector后,由于‘模 (%) 运算’的 base 变大了,所以必须对所有元素重新 hash并计算新的 bucket 编号,这样的话原来的单链表就会被打散重新排列;11. 在元素是大对象以及相同规模的元素数量并且规模达到一定数量级的情况下比较

hashtable、 std::vector和 binary_tree的性能 ( 仅针对最常用的三类操作 ):

12. Example: ndm_db_conn.hh, _SQL_STMT_MAP_

Hashtable/unordered_set/unordered_map

operations hashtable std::vector binary_treeinsert(V) 如果不需要重建 buckets,则接近

O(1);如果需要扩展和重建 buckets,则所有元素对象需要重新 hash并连接到新的 bucket 上,但是没有拷贝每个元素对象的开销。所以,这种情况下的开销仍然比 vector要好很多

如果不需要重建 vector,那么只有在末尾插入 (push_back)时不需要搬移元素对象 (拷贝元素 );如果需要重建 vector,那么搬移元素的开销巨大;

平 均 为O(log2N),但是有时需要重新平衡树结构,因此会伴随子树旋转和红黑结点转换等复杂操作

find(key) 平均接近 O(1); 不直接提供 find ,但平均O(N);

O(log2N)

erase(key) 平均接近 O(1); 只有在末尾删除元素时不需要搬移后面的元素;

同 insert()操作

Page 23: STL Container Usage Tips

Quiz 相同元素规模的情况下,上述各个容器类型中,哪种的空间开销最大?其次是谁? 如果不考虑 std::map/std::set 实现中的排序要求,它们是否可以用 hash

table来实现? 在结构上, std::set<T> rb_tree<T>, 那么 std::map<key, value>

rb_tree< ??? > ? 同样道理, hash_set<T> hashtable<T>, hash_map<key, value>

hashtable< ??? > ? 对于给定的 hash_map/hash_set(hash函数已经给定 ),具有相同 hash值的不同 key或不同元素一定会落入同一个 bucket 里。那么具有不同 hash值的不同 key或不同元素是否一定落入不同的 bucket 里? 对于 hash_multiset/hash_multimap ,当 insert() 操作导致 buckets 需要重建时,原来具有相同 value/key的那些元素在新的 hashtable 里面是否仍然位于同一个 bucket 里面并且挨在一起? [不一定,因为 mod的 base 变了 ] 使用开链法实现 Hashtable时,为什么不使用双向链表?

23

Page 24: STL Container Usage Tips

Q&A

24