《STL源码剖析》阅读摘要
第一章
介绍一些STL中特定的组态和设定
第二章 空间配置器(allocator)
第一级、第二级空间配置器
STL设计的空间配置器 std::alloc
三个基本处理工具:uninitialized_copy、uninitialized_fill、uninitialized_fill_n
第三章 迭代器与traits编程技法
迭代器粘合了 算法与容器
这一部分大量使用了偏特化,例如,当调用一个泛型算法时,根据输入类型,使用模板函数+模板类(参数类型推断机制,返回一个临时对象)推断输入的迭代器的类型(这非常重要)。然后以类型(临时对象)为参数去调用被封装的函数,这个时候根据参数的类型的不同,会匹配不同的特化版本,保证最优效率。
3.6节介绍了如何推断输入类型的完整代码,用到了“萃取器”traits(模板类),对迭代器对象、指针、指向const的指针有多个偏特化版本。
举个例子具体怎么做:
假设我们用struct定义了好几种迭代器类型,例如
struct A {}; struct B :public A {}; struct C :public A {}; ` //等等,都是空的,没成员。
定义了
traits
,它将不同迭代器的类型重新统一命名:template <class Iterator> struct iterator_traits { typedef typename Iterator::iterator_category iterator_category; };
然后定义某种迭代器, 例如:
struct __XXX__iterator { typedef C iterator_category; //定义这个迭代器属于C类 };
这样,在某个封装函数种用到迭代器时,先使用traits类去推断(取得)迭代器类型,以类型为参数调用被封装的函数。
template<class Iterator> void fun(Iterator& iter) { typedef typename iterator_traits<Iterator>::iterator_category category; fun_(iter, category()); //A(), B()或者C() } template<class A_iter> void fun_(A_iter& i, A) { //函数多了一个tag,A类的某个对象 std::cout << "I am A" << std::endl; } template<class B_iter> void fun_(B_iter& i, B) { std::cout << "I am B" << std::endl; } template<class C_iter> void fun_(C_iter& i, C) { std::cout << "I am C" << std::endl; } //测试函数 int main() { __XXX__iterator X_iter; fun(X_iter); //输出 "I am C" } //决定迭代器类型的函数 template <class Iterator> inline typename iterator_traits<Iterator>::iterator_category iterator_category(const Iterator&){ typedef typename iterator_traits<Iterator>::iterator_category category; return category(); //返回一个临时对象,在这里其实就是C(),空的没成员 }
第四章 序列容器
array, vector, heap, list, deque(双端队列), stack, queue
1.vector
讲解如何控制大小,空间配置策略,重新配置使得数据移动
vector的迭代器为 普通指针, vector空间不足时用空间配置器扩容2倍、不够就扩容需要的大小n
vector扩容是在另外一片空间,然后将原来的内容拷贝过来,然后再构造新的内容。
vector 删除erase(iter) 返回的是 删除位置的迭代器
插入insert的时候根据插入位置会有不同的策略,插入位置为指示节点的前方(插入元素a,b,指示点p);
2.list
list时一个环状双向链表,不能使用普通指针为迭代器
template<class T>
struct __list_node{
typedef void* void_pointer;
void_pointer prev; //void* 其实可为__list_node<T>*
void_pointer prev;
T data;
}
根据前一章的内容,需要为迭代器设计iterator_category(迭代器类型),value_type,pointer, reference等等
typedef __list_node<T>* link_type;
link_type node; //指向节点的指针
由于是一个环状的链表,所以class list只用一个node指针就可以表示整个环
list的insert在指定位置插入,erase删除指定位置,返回删除结点的后一个
操作:push_front, push_back, erase, pop_front, pop_back, clear, remove, unique, splice, merge, reverse, sort
unique:移除连续相同元素
transfer(iter p, iter first, iter last) 把[first, last)中的元素接到p之前的位置(顶替p的位置)
splice(将所指元素移到指定),merge(合并两个有序链表,归并),reverse(反转),sort(快排,找指定位置)都通过transfer来执行,其中sort是list特有的sort。
3.deque
deque是双向开口的线性空间,(vector单向开口)。可以在头尾插入和删除,且都是常数级操作。
deque是用动态的分段连续空间合成的,在空间不足时,可以随时增加一段新的空间并连接起来,不会出现vector中需要重新配置、复制旧元素的情况,因此也不需要预留(capacity)。deque支持随机访问,为了同时实现这些功能,它的迭代器并不是普通指针,整体实现也相当复杂。
中控器
deque使用一块map(连续空间,不是容器map)作为主控,其中每个元素都是指针,指向另一块连续的线性空间,称为缓冲区(这才是存储元素的地方)。deque通过中控器表现出表面上的连续,并提供随机访问接口。
template <class T, class Alloc == alloc, size_t Bufsize = 0>
class deque
{
public:
typedef T value_type;
typedef value_type* pointer;
protected:
typedef pointer* map_pointer;
map_pointer map; //T**,指向中控器map,连续空间;元素为指针(节点),指向缓冲区
size_type map_size; //map容纳的 指针数量
}
迭代器
STL自定义迭代器的类型,迭代器保存了T* cur(迭代器指向的元素),T* first(缓冲头), T* last(缓冲尾)
以及一个指向中控器map的指针。在操作碰触边界时,要利用这些指针来跳转缓冲区(set_node)。
deque数据结构
duque维护了两个迭代器,一个指向头(start), 一个指向尾(finish),还保存了中控器map的大小。
protected:
iterator start;
iterator finish;
deque在构造时候,保有一个空的缓冲区(无元素),然后再调用函数、根据元素数量来扩展。在扩展时,会将有数据缓冲区指针保持在最中央,使得两头可以扩充的缓冲区一样大(调整map_size,保证至少一个)。然后为现用的节点配置缓冲区,这些缓冲区就是deque的可用空间(最后一个可能留有余裕)。
添加元素,添加操作分为push_front、push_back。缓冲区有多的就直接添加,没有就要配置新的缓冲区,在配置缓冲区前,如果中控map节点不够就要重新配置map(reserve_map_at_front(),reserve_map_at_back())。
发生了节点不足的情况时,分头节点不足和尾节点不足两种情况执行,首先检查map整体上是不是还有充足的节点空间(例如,尾部插入太多,头部为空),如果是这种情况,就调整节点的位置到map中央;如果不是,就配置新空间,给新map使用,将原节点指针拷贝过来,然后,更新map_pointer map, map_size
,最后两种情况都需要更新start, finish
两个迭代器。
删除元素,pop_back,pop_front,clear,erase。erase删除元素时,重点是要考虑哪端需要移动的元素较少,如果前端元素较少就移动前端,反之移动后端,并对应移动start, finish
。删除时,如果发生了跳转缓冲区,就要释放掉缓冲区,但是,清除完整个deque(clear),还是会保有一个缓冲区不释放(策略,也是初始状态)。
插入元素,insert,同样要考虑哪端元素较少。
4.stack与queue
缺省情况下都使用deque为底部结构,由于都以底部容器完成所有工作,被称为容器配接器。stack(栈)所有元素先进后出,queue(队列)所有元素先进先出,都不提供迭代器、遍历功能。还可以用list作为底部容器。
5.heap
为优先队列(priority queue)服务,考虑到实现难度采用堆,复杂度在队列和二叉排序树之间。
heap的算法:
make_heap(vector.begin(), vector.end()),
push_heap(vector.begin(), vector.end()),
pop_heap(vector.begin(), vector.end()),
sort_heap(vector.begin(), vector.end())
迭代器的范围就是操作范围,先要是堆的顺序才能执行后三个方法,其中要先主动push_back
然后执行push_heap
,执行pop_heap
后要主动pop_back()
。
6.priority_queue
只允许在底端加入元素,并从顶端取出元素(权值较高),利用max-heap自动排序,只有最顶端元素才有机会被外界取用。缺省情况下使用vector为底部容器,是一种容器适配器。
template<class T, class Sequence = vector<T>,
class Compare = less<typename Sequence::value_type>>
class priority_queue{
Sequence c;
const_reference top();
void push(const type_value& x)
{
c.push_back(x);
...
}
void pop()
{
...
c.pop_back();
}
}
7.slist
单向链表,待补充
第五章 关联式容器
所谓关联式容器,设计观念上类似关联式数据库(实际上简单很多):每笔数据都有一个键值(key)和一个实值(value)。
记录几个关联容器通用的重要方法:
lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。equal_range是C++ STL中的一种二分查找的算法,试图在已排序的[first,last)中寻找value,它返回一对迭代器i和j,其中i是在不破坏次序的前提下,value可插入的第一个位置(亦即lower_bound),j则是在不破坏次序的前提下,value可插入的最后一个位置(亦即upper_bound),因此,[i,j)内的每个元素都等同于value,而且[i,j)是[first,last)之中符合此一性质的最大子区间
AVL-Tree
维护平衡的方式:子树高度差不能超过1
若插入节点后影响了平衡:只需要从插入或者删除节点向上找到第一个不平衡的节点,调整它到平衡
两种调整动作:左旋(逆时针旋转),右旋(顺时针旋转)
不平衡类型:
- 左 + 左(左子树外侧):右旋一次
- 左 + 右(左子树内侧):左旋一次到情况1,然后右旋一次
- 右 + 左(右子树内侧):右旋一次到情况4,然后左旋一次
- 右 + 右(右子树外侧):左旋一次
红黑树与4种基于红黑树(RB-Tree)的关联式容器
RB-Tree
- 节点不是红色就是黑色
- 根节点为黑色
- 如果节点为红色,子节点一定为黑色
- 任一节点到null的任何路径,所含黑节点数必须相同
关于STL种红黑树的实现,每个节点有三个指针,除了左右外还有个父,存在一个head节点(也是end()
所对应的节点),刚创建时,head节点的父指0,在插入元素后,指向真正的第一个元素的节点root
,同时root
也指向head,head的左指向最左节点,也就是begin()
需要返回的节点,右指针指向最右节点,也就是rbegin()
需要返回的节点。
更多待补充
set
操作基于RB-Tree,key值即value值
map
操作基于RB-Tree,key值不同,value值可以相同
map的插入操作有一些小细节,使用key值通过下标访问符[]进行操作时,会发生插入操作,调用了一个返回pair<iterator, bool>
的insert
函数,其中第一个是迭代器,第二个bool表示插入成功与否,成功时迭代器指向新元素,失败时也就是元素已经存在时,指向已经存在的元素。
multiset
可重复key值版本,调用RB-Tree的insert_equal
multimap
可重复key值版本,调用RB-Tree的insert_equal
哈希表与4种基于哈希表的容器
hashtable
可被视为字典结构,提供常数时间的基本操作。
实现方法:使用线性容器存储元素,每一个位对应一个元素的key值(索引),并使用hash function
(散列函数)将大数映射为小数,作为key值(索引),举例:
按ASCII编码把字符串编码,“jjhou”——‘j’, ‘j’, ‘h’, ‘o’, ‘u’ 如下:
‘j’ * 128^4 + ‘j’ * 128 ^ 3 + ‘h’ * 128 ^ 2 + ‘o’ * 128 ^ 1 + ‘u’ * 128 ^ 0
106 * 128^4 + 106 * 128 ^ 3 + 104 * 128 ^ 2 + 111 * 128 ^ 1 + 117 * 128 ^ 0 = 28678174709
这个数显然太大了,保存如此大的数组显然不科学。
所以使用散列函数,例如X % TableSize会得到一个范围在0~TabelSize-1之间的数,作为索引。
这样做的问题是会有不同的元素映射到相同位置(发生碰撞),解决办法:线性探测、二次探测、开链
STL采用开链,hashtabel内元素为桶(bucket),意思每个单元可能存储的是多个节点,每个节点存有值和指向下一个节点的指针(像list),整个聚合在一起的(buckets)使用vector
来完成。
实现细节:
- 迭代器内型为forward,意思只能前进,迭代器指向一个节点,前进一个位置即到达下一个节点,如果正巧在list的尾端,则跳到下一个有效的桶(bucket)上,也就是下一个list的头部节点。
- 表格大小一般为质数,STL中预备了28个质数(大致按照翻倍顺序)。
- 插入元素时首先判断hashtabel是否需要重建,标准为新增元素后的总元素个数与当前
bucket vector
的大小比较,如果前者大于后者就重建。重建时要挨个对旧表的元素重哈希计算,挂到新表正确的桶里面。 - 封装了一个
bkt_num()
函数计算元素落到哪个桶,需要获得key值和桶的个数(buckets,vector的大小),哈希函数对字符串类型const char*
进行了特定的处理。 - 删除
clear
删掉了每一个节点,但是vector
并没有释放,复制copy_from
先要clear
清空所有节点,然后复制vector
的每一个bucket
,也就是指向节点的指针,还要对每个bucket
内的节点挨个复制(new node)
。
hash_set 与 hash_multiset
操作基于hashtable,key值即value值(multi为可重复版本)
和用RB-Tree实现的set区别在于元素无序
hash_map 与 hash_multimap
操作基于hashtable,key值不同,value值可以相同(multi为可重复版本)
和用RB-Tree实现的map区别在于元素无序