0. 序
学习STL源码做的笔记,主线是侯捷老师的《STL源码剖析》。
本文中部分图片来自侯捷老师的PPT。
1. 红黑树 - 数据结构
红黑树(RB-Tree):是一种平衡的二叉搜索树。有利于search和insert
红黑树提供“迭代器”和“遍历”操作。
顺序就是中序遍历的顺序。
注:虽然提供了迭代器,但是我们不应该通过迭代器修改元素的值。
STL中的红黑树提供了两种insert操作:inser_unique和insert_equal,前者表示节点的key在整棵树上独一无二,不能重复,否则插入失败,后者表示节点的key可以重复。
int main()
{
// GNU 4.9之后的红黑树为_Rb_tree
_Rb_tree<int, int, _Identity<int>, less<int>> itree;
cout << itree.size() << endl; // 0
itree._M_insert_unique(3);
itree._M_insert_unique(8);
itree._M_insert_unique(5);
itree._M_insert_unique(9);
itree._M_insert_unique(13);
itree._M_insert_unique(5); // 这句是无效的插入,因为使用的插入方式是unique
cout << itree.empty() << endl; // 0
cout << itree.size() << endl; // 5
cout << itree.count(5) << endl; // 1
itree._M_insert_equal(5);
itree._M_insert_equal(5);
cout << itree.size() << endl; // 7
cout << itree.count(5) << endl; // 3
return 0;
}
2. set与multiset
set与multiset是以红黑树为底层结构的,在使用红黑树时,它的key和value是同一个东西(没有value中的data部分),红黑树在初始化时,给keyOfValue
参数使用了_Identity
。
因为set与multiset使用的是红黑树,所以它也有迭代器,但是用户无法通过迭代器修改元素,原因在于:它内部红黑树的迭代器被const修饰了。GNU 4.9的源代码如下:
- set元素不能重复,因为它红黑树的插入方式使用的是insert_unique
- multiset元素不能重复,因为它红黑树的插入方式使用的是insert_equal
3. map与multimap
与上边的set、multiset类似。
map与multimap是以红黑树为底层结构的,在使用红黑树时,但,它的key和value不是同一个东西(红黑树中的value是一个整体,包括key和data,data就是“键-值”中的值)。
因为map与multimap使用的是红黑树,所以它也有迭代器,我们无法通过它的迭代器修改key,但是可以修改data,原因在于:用户创建map类对象时,传入的是key和data的数据类型,而map和multimap类在构建内部的红黑树时,先将key和data打包成一个pair
,打包的时候,对key加了const限制,而data没有任何限制。所以,尽管它没有对自己内部红黑树的迭代器进行限制,但是达到了目的,技巧在于此。
但是它的迭代器被const修饰了,无法通过迭代器修改元素。GNU 4.9的源代码如下:
- map元素不能重复,因为它红黑树的插入方式使用的是insert_unique
- multimap元素不能重复,因为它红黑树的插入方式使用的是insert_equal
map可以使用中括号
[]
进行将指定的key
所对应的data
取出来,如果这个key
不存在,那么map会直接创建这个key
!这是标准中的规定。,源码如下:
先用lower_bound去找key
出现的第一个位置,然后判断存不存在,如果不存在的话,就insert
。可见,通过中括号的访问方式,因为要先进行一个lower_bound
,所以要稍微耗时一点。
对set与map的小结
set与map(包括各自的multi版本)调用自己内部红黑树的关键区别在于:
- set中的红黑树
key
就是value
,而map中value
包含key
。 - set中不能通过迭代器修改元素,是对红黑树迭代器进行了const;而map中是在构建红黑树所需的
value
时,对key进行了const限制。
set与multiset的关键区别在于:
- 内部红黑树的插入方式不同,普通版本使用的是
insert_unique
,而multi版本使用的是insert_equal
。
map与multimap的关键区别在于:
- 内部红黑树的插入方式不同,普通版本使用的是
insert_unique
,而multi版本使用的是insert_equal
。 - map可以使用中括号
[ ]
直接进行查询
和插入
操作,但是,通过中括号的访问方式,因为要执行一个lower_bound
操作,所以时间上不占优势。
4. hashtable-数据结构
hashtable(哈希表) 也叫 散列表、哈希表。
哈希表,最开始的想法是:
开 N 个bucket(篮子),每个篮子下挂一个链表,要哈希的每个元素都对N取余为 r,那么该元素就挂在下标是 r 的篮子下的链表上。可见,这种方式当碰撞较多的时候(碰撞:多个元素对N取余结果相同),链表的查找速度会很慢。
哈希表,改进后的想法:
先开 N 个bucket(篮子),如果当前已经被哈希的元素比 N 大了(经验上是觉得这个时候可能某个篮子下的链表就已经过长了),那么就扩充篮子的数量,扩充方式为:在 2*N 附近找一个质数作为新的篮子数量,然后对所有已经被哈希的元素重新进行放置(rehashing),这样以来,每个篮子下的链表都会变短,便于查找。
注意,尽管各家实现的哈希表可能不同,但是他们的bucket数量一定都比元素多,差别可能就是在篮子扩充时不太一样。
上述过程见下边图片 :
哈希表的迭代器有点类似deque的迭代器,需要能够进行跳跃,对外伪装成连续。
每个对象,如何得到它在哈希表中对应的数字(hashCode)呢?方法不一,只要不重复即可,STL中也对一些基础的数据类型做了hashCode方法,比如对于数字(int,long,char等)就直接让它本身作为hashCode,对于C中的字符串也有相应的方法(比如,hash<const char*>、hash<char *>
)。
但是,ST中没有提供针对c++中string的hashCode方法(也就是说,没有hash<std::string>
)。
5. unordered系列容器
在c++11以后,底层使用哈希表(hashTable)实现的容器有4个:
- unordered_set
- unordered_multiset
- unordered_map
- unordered_multimap