STL源码学习之序列式容器
总所周知,常用的数据结构不外乎数组、链表、树、堆栈、队列、散列表、集合、映射表等等。根据 “数据在容器中的排列” 特性,这些数据结构分为序列式和关联式两种。
序列式容器
所谓序列式容器,其中所有元素都可序,但未必有序。C++语言本身提供了一个序列式容器array
,STL 另外再提供 vector
,list
,deque
,stack
,queue
,priority_queue
等等序列式容器。其中 stack
和 queue
由于只是将 deque
改头换面而已,技术上被归类为一种适配器。
vector
概述
vector是动态空间,随着元素的加入,他的内部机制会自行扩充元素以容纳新元素。因此,vector的运用对于内存的合理利用和灵活性有很大的帮助,我们再也不必因为害怕空间不足而一开始就要求一个大块头array了,我们可以安心使用vector,吃多少用多少。
vector的数据结构
运用start
, finish
, end_of_storage
三个迭代器,便可轻易地提供首尾标示、大小、容量等等机能。使用的迭代器为 Random Access
。
vector的内存管理:constructor、push_back
所谓动态增加大小,并不是在原有空间之后续接新空间,而是以原大小的两倍另外配置一块比较大空间,然后将原内容拷贝过来,才开始在原内容之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。请务必小心。
list
list的节点
template < class T>
struct __list_node {
typedef void* void_pointer;
void_pointer prev; // 型别为 void*。其实可设为 __list_node<T>*
void_pointer next;
T data;
};
list的迭代器
list不能向vector那样使用普通指针作为迭代器,因为list节点不保证在空间中连续存在。list迭代器必须有能力指向list节点,并完成正确的递增,递减,取值,成员存取等操作。
STL list是双向链表,提供bidirectioinal iterators。list的插入和结合都不会引起迭代器失效,删除操作也只是引起指向删除元素的迭代器失效。
list数据结构
STL list不仅是一个双向链表,而且是一个环状双向链表。所以它只需要一个指针,就可以完整表示整个list。
template <class T, class Alloc = alloc>
class list {
protected:
typedef __list_node<T> list_node;
typedef list_node* link_type;
protected:
link_type node; //只需要一个指针,就可以表示整个环状双向链表
}
deque
vector是单向开口的连续空间,deque是双向开口的连续空间。所谓双向,是可以在头尾两端分别做插入和删除操作。从技术观点,vector也可以在头尾两端操作,但效率奇差。
deque和vector的最大差异,一个是deque允许常数时间在头部进行插入删除,一个是在于deque没有容量观念,因为它是以动态的分段连续组合而成。换句话说,向vector那种空间不足而重新分配一个大的空间,再移动元素释放原空间的操作在deque是不会发生的。也因此,deque没有空间保留功能。
虽然deque也提供random access iterator
,但它的迭代器不是普通指针,其复杂度不可与vector不可以道里计,这当然影响了各个层面。因此,除非必要,我们尽量选择vector而不是deque。对deque的排序,为了高效率,可以将deque先完整复制到一个vector,将vector排序后,再复制回deque。
deque中控器
deque是由一段一段定量连续空间构成,一旦有必要在deque前端或尾端增加空间,便配置一段定量连续空间,串接在deque前端或尾端。deque的最大任务,是维持其整体连续的假象,并提供随机存取的接口。避开了“重新配置,移动,释放”的轮回,deque的代价就是复杂的迭代器。
deque即为分段连续,就必须有中央控制,而为了维持整体连续假象,数据结构的设计和迭代器的前进后退等操作都颇为繁琐,deque的代码实现分量比vector和list都多的多。
deque采用一块map作为主控,这里的map是一小块连续空间,其中每个元素(也就是每块node)都是指针,指向另一块较大的连续空间,称为缓冲区。缓冲区才是deque的存储空间主体,SGI STL允许我们指定缓冲区大小,默认值0表示使用512bytes缓冲区。
deque迭代器
deque是分段连续空间,维持其整体连续假象的任务落在了operator++
,operator–
两个运算子身上。
首先我们思考一下,deque迭代器应该具备什么结构。首先它必须能够指出缓冲区在哪儿,其次必须能够判断自己是否已经处于其所在缓冲区边缘,如果是,一旦前进或后退就必须跳至前一个或下一个缓冲区。为了能够正确跳跃,deque必须随时掌握管控中心map。
中控器,缓冲器,迭代器的相互关系:
假设我们现在产生一个deque,并令其缓冲区大小为32,那么每个缓冲区可容纳32/sizeof(int)=8
个元素。经过某些操作之后,deque拥有20个元素,那么其begin()
,end()
传回来的迭代器如下图所示。这两个迭代器事实上一直保持在deque中,名为start()
,finish()
。
20个元素需要20/8=3个缓冲区,所以map运用了三个节点。迭代器start内的cur当然指向缓冲区中的第一个元素,迭代器finish内的cur当然指向缓冲中的最后元素(下一个位置)。
deque迭代器的加、减、前进和后退操作,在行进过程中可能会需要调用set_node跳一个缓冲区。
deque的数据结构
template<class T, class Alloc=alloc, size_t BufSiz = 0>
class deque{
public:
typedef T value_type;
typedef value_type* pointer;
typedef size_t size_type;
public:
typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
protected:
typedef pointer* map_pointer;
protected:
iterator start;
iterator finish;
map_pointer map;
size_type map_size;
...
}
stack
stack是一种先进后出的数据结构,只有一个出口,允许在最顶端进行新增,移除和获取值,对于其他元素没有任何方法进行存取,换句话说,stack不允许遍历。
以某种既有容器作为底部结构,改变其接口,使其符合“前进后出”的特性,形成stack是很容器的。deque是一个双向开口的结构,若以deque为底部容器封闭其头端接口,就可以形成一个stack。因此,SGI STL便以deque作为缺省情况的stack底部结构,stack的实现因而非常简单。
因为stack是以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌“的性质,所以也称为配接器。因此STL stack往往不称为容器,而称为配接器。
stack定义完整列表
template <class T, class Sequence = deque<T> >
class stack {
friend bool operator== __STL_NULL_TMPL_ARGS(const stack&, const stack&);
friend bool operator< __STL_NULL_TMPL_ARGS(const stack&, const stack&);
public:
typedef typename Sequence::value_type value_type;
typedef typename Sequence::size_type size_type;
typedef typename Sequence::reference reference;
typedef typename Sequencd::const_reference const_reference;
protected:
Sequence c; //底层容器
public:
//以下完全利用Sequence c的操作,完成stack的操作
boll empty() const { return c.empty(); }
size_type size() const { return c.size(); }
reference top() { return c.back(); }
const_reference top() const { return c.back(); }
void push(const value_type& x) { c.push_back(x); }
void pop() { c.pop_back(); }
};
template < class T, class Sequence>
bool operator==(const stack<T, Sequence>& x, const stack<T, Sequence>& y) {
return x.c == y.c;
}
template <class T, class Sequence>
bool operator<(const stack<T, Sequence>& x, const stack<T, Sequence>& y) {
return x.c < y.c;
}
stack没有迭代器
stack所有元素的进出都必须符合“先进后出”的原则,只有stack顶端的元素才能够被外界存取,stack不提供走访功能,也不提供迭代器。
以list作为stack的底层容器
除了deque以外,list也是双向开口的数据结构。上述源码中的stack的各种函数list也都具备。若以list为底部容器并封装头部接口,一样可以得到stack。
queue
queue是一种先进先出的数据结构,有两个出口。queue允许新增移除,从低端加入,从顶端取元素。但除了低端加入,顶端取出外,没有其他任何方法可以存取queue其他元素。换言之,queue不接受遍历。
以某种既有容器作为底部结构,将其接口改变,使之符合先进先出的特性,形成一个queue,很容易做到。deque是双向开口的数据结构,若以deque为底部结构,封闭其低端出口和前端入口,就可以得到一个queue。因此,SGI STL便以deque作为其缺省底部结构。
由于queue是以底部容器完成其所有工作,而具有“修改某物接口,使之符合另一种风貌”的特性者,称为配接器。因此STL queue往往不被称为容器,而被称为容器配接器。
queue没有迭代器
queue所有元素都必须符合先进先出条件。只有queue顶端元素才能被外界获取。queue不提供遍历功能,也不提供迭代器。
以list作为queue的底层容器
除deque外,list也是双向开口容器。queue使用的底层容器的函数list也都具备。因此,以list为底部结构封装其头端开口,一样可以得到queue。
heap(隐式表述)
heap并不属于STL容器,它是个幕后英雄,扮演priority queue
的角色。priority queue
允许用户以任何次序推入元素,但取出时必须从优先级最高也就是数值最高的数取出。binary max heap
正具有这样的特性,适合作为priority queue
的底部机制。
若果使用list作为priority queue
的底层机制,元素插入可享常数时间,取极值需要进行线性扫描。我们也可以在插入前先进行排序,使元素值总是有序的,但这样一来,取极值和删除操作达到最高效率,元素的插入只有线性。
我们也可以以二叉搜索树作为底部实现,这么一来,插入极值都是O(logn),但是一来二叉搜索树需要有足够的随机性,而来二叉搜索树并不容易实现。priority queue
的效率最好在二叉搜索树和queue之间,binary heap
便是合适的选择。
binary heap
就是一种完全二叉树,除底层叶子节点外,都是满的,而最底层的叶子节点从左至右又不能有空隙。完全二叉树没有节点漏洞
的一个极大的好处在于,我们可以用array来存储节点。假设动用一个小技巧,将array的0元素设置为极大或极小值,那么当完全二叉树某个节点位于array的i处,其左子节点必然在2i处,右子节点必然在2i+1处,父节点必位于i/2处。通过这个简单的位置规则,我们可以用array轻易实现完全二叉树,这种以array表示tree的方法,我们称之为隐式表述法。
这样我们用一个array和一组heap算法就可以实现priority queue,array的缺点是无法动态改变大小,heap需要这个功能,所以我们用vector来替代。
heap分为max-heap
和min-heap
,前者的每个节点的键值大于等于其子节点的键值,后者每个节点的键值小于等于子节点的键值。max-heap
最大值在根节点,min-heap
最小值在根节点,都位于array或者vector的起头处。STL提供max-heap
,今天讨论的也都是max-heap。
heap算法
push_heap算法
为了满足完全二叉树特性,新加入的元素一定放在最底层做叶子节点,并填补从左至右第一个空格,也就是把新元素插入底层vector的end()处。
新元素是否适合当前位置呢?我们执行一个上溯程序,将新节点与父节点比较,比父节点大,就交换,一直上溯,直到不需要交换或者到根节点为止。
以下为push_heap的实现细节。函数接受两个迭代器作为vector的头尾,并且新元素已经插入到容器的最尾端,如果不符合这两个条件,执行结果未可知。
pop_heap算法
pop操作取走max-heap的根节点(其实是放在vector的最后一个元素),为了满足完全二叉树的特性,必须将最下一层最右边的叶子节点拿掉,我们的任务是为这个叶子节点找一个合适的位子。
为了满足max-heap特性,我们进行下溯,将根节点拿掉(形成一个洞)填入上述失去生存空间的叶子节点值,在将它拿来和其两个子节点比较键值,并与较大的那个对调位置,如此一直下放,直到洞的值大于左右子节点或者到叶子节点为止。
pop_heap具体实现。该函数接受两个迭代器作为底层容器的头尾,如果不满足这个条件,结果不可预期。
pop_heap之后,最大值元素只是被放在了容器的最尾端,并未被移走。如果要取其值,可用back(),如果要移走,可用pop_back()。
sort_heap算法
既然每次pop_heap可获得一个键值最大的值,那么持续pop_heap,每次向前缩减一个元素,程序执行完毕,我们就有一个递增的序列。
下面是sort_heap的实现细节,函数接受两个迭代器作为底层容器的头尾两端,如果不满足这个条件,结果未可知。sort_heap之后的heap不再是一个合法的heap了。
template <class RandomAccessIterator>
void sort_heap(RandomAccessIteraor first, RandomAccessIterator last) {
while (last - first > 1)
pop_heap(first, last--);
}
make_heap算法
这个算法就是将一段数据转化为一个heap。
heap没有迭代器
heap所有元素都必须遵循完全二叉树排列规则,所以heap不支持遍历,也不提供迭代器。
priority queue
priority queue是一个拥有权值观念的queue,它允许加入新元素,移除旧元素,审视元素值等操作。由于这是一个queue,只允许低端插入,顶端获取,除此之外无其他办法存取元素。priority queue并非按照元素推入次序排列,而是按照权值排列,权值最高排在最前面。缺省情况下priority queue利用一个max-heap完成,max-heap可以满足priority queue的依权值高低自动递增排序的特性。
priority queue定义完整列表
priority queue缺省以vector为底部容器,再加上heap处理规则。queue为配接器,priority queue也称为配接器。
template <class T, class Sequence = vector<T>, class Compare = less<typename Sequence::value_type> >
class priority_queue {
public:
typedef typename Sequence::value_type value_type;
typedef typename Sequence::size_type size_type;
typedef typename Sequence::reference reference;
typedef typename Sequencd::const_reference const_reference;
protected:
Sequence c; //底层容器
Compare comp; //元素大小比较标准
public:
priority_queue() :c() {}
explicit priority_queue(const Compare& x) :c(), comp(x) {}
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last, const Compare& x) : c(first, last), comp(x) { make_heap(c.begin(), c.end(), comp); }
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last) : c(first, last) { make_heap(c.begin(), c.end(), comp); }
//以下完全利用Sequence c的操作,完成stack的操作
boll empty() const { return c.empty(); }
size_type size() const { return c.size(); }
const_reference top() const { return c.front(); }
void push(const value_type& x) {
__STL_TRY{
c.push_back(x);
push_heap(c.begin(), c.end(), comp);
}
__STL_UNWIND(c.clear());
}
void pop() {
__STL_TRY{
pop_heap(c.begin(), c.end(), comp);
c.pop_back();
}
__STL_UNWIND(c.clear());
}
};
priority queue没有迭代器
priority queue进出都有一定的规则,只有顶端元素才有机会被外界取用,priority queue不提供遍历功能,也不支持迭代器。
slist
STL list是一个双向链表,slist是一个单向链表。slist不在标准规格之内。slist是forward iterator
,list是bidirectional iterator
,因此slist会受到很多限制,但是单向链表耗用空间更小,操作更快,不失为另一种选择。
slist和list一样,插入、移除,接合操作都不会使原有迭代器失效。根据STL的习惯,插入操作都是将元素插入在指定点之前。作为单向链表,slist没有任何办法可以回头定出前一个位置,因此它必须从头找起。换句话说,slist除了在开头附近,其他位置上采用insert和erase操作都不明智。这就是slist相较list的最大缺点,为此,slist提供insert_after()和erase_after()供选择。
基于同样的效率考虑,slist不提供push_back
,只提供push_front
。
slist节点
slist节点和迭代器设计,在结构上比list复杂的多,运用继承关系,因此在型别转换上有复杂的表现。
//单向链表的节点基本结构
struct __slist_node_base {
__slist_node_base* next;
};
//单向链表的节点结构
template <class T>
struct slist_node:public __slist_node_base {
T data;
};
//全局函数:已经某一节点,插入节点于其后
inline __slist_node_base* __slist_make_link(__slist_node_base* prev_node, __slist_node_base* new_node) {
//令new_node的下一个节点为prev的下一个节点
new_node->next = prev_node->next;
prev_node->next = new_node; //令prev的下一个节点为new_node
return new_node;
}
//全局函数:单向链表的大小
inline size_t __slist_size(__slist_node_base* node) {
size_t result = 0;
for (;node!=0;node=node->next)
++result;
return result;
}
slist迭代器
//单向链表的迭代器基本结构
struct __slist_iterator_base {
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef forward_iterator_tag iterator_category; //注意,单向
__slist_node_base* node; //指向节点基本结构
__slist_iterator_base(__slist_node_base* x):node(x) {}
void incr() {node=node->next;} //前进一个节点
bool operator==(const __slist_iterator_base& x) const {
return node==x.node;
}
bool operator!=(const __slist_iterator_base& x) const {
return node!=x.node;
}
};
//单向链表的迭代器结构
template <class T, class Ref, class Ptr>
struct __slist_iterator : public __slist_iterator_base {
typedef __slist_iterator<T, T&, T*> iterator;
typedef __slist_iterator<T, const T&, const T*> const_iterator;
typedef __slist_iterator<T, Ref, Ptr> self;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef __slist_node<T> list_node;
__slist_iterator(list_node* x) : __slist_iterator_base(x) {}
//调用slist<T>::end()时会造成__slist_iterator(0),于是调用上述函数
__slist_iterator():__slist_iterator_base(0) {}
__slist_iterator(const iterator& x):__slist_iterator_base(x.node) {}
reference operator*() const {return ((list_node*)node)->data;}
pointer operator->() const {return &(operator*());}
self operator++() {
incr();
return *this;
}
self operator++(int) {
self tmp = *this;
incr();
return tmp;
}
}
注意,比较两个slist迭代器是否相等,由于我们没有重载operator==,所以会调用__slist_iterator_base::operator==,根据其定义,我们知道是由__slist_node_base* node是否相等而定的。