《STL源码分析》第四章序列式容器学习总结
序列式容器
所谓序列式容器,其中的元素都可序,但未必有序。C++本身提供了一个序列式容器array,STL另外再提供了vector, list, deque, stack, queue, priority-queue等。其中stack和queue就是将deque改头换面,技术上被归类为配接器(adapter)。
1. vector
vector和array十分相似。唯一的差别就是vector对于空间的运用更加的灵活。array是静态空间,一旦配置了就不能改变大小。vector是动态空间。因此,不用担心因为空间不足而一开始要求一个大块头array了。
(1) vector的迭代器
vector是维护一个连续线性空间,所以不论元素型别为何,普通指针都可以作为vector的迭代器而满足所有必要条件。普通的指针就具备vector的那些功能。例如随机存取。所以,vector提供的是Random Access Iterators。
template<class T,class Alloc = myalloc>
class vector
{
public:
typedef T value_type;
typedef T* pointer;
typedef T* iterator; //vector的迭代器只是一个普通指针
typedef T& reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef const T* const_iterator;
typedef simple_alloc<value_type, Alloc> data_allocator;
private:
iterator start_; //目前空间的头
iterator end_; //目前空间的尾
iterator end_of_storage; //目前可用空间的尾
...
}
(2) vector数据结构
vector的数据结构也是比较简单,就是上边的代码,三个迭代器分别表示目前空间的头,目前空间的尾以及目前可用空间的尾。因为vector的容量是比实际大小要大的。一旦容量等于实际大小,再增加元素整个vector就会重新配置。配置的原则是扩充至原来的2倍。如果2倍容量仍然不足,就会扩张至足够大的容量。
这种动态增长并不是在原空间之后接续新的空间,而是以原来大小的两倍另外配置一块较大的空间,然后将原来的内容拷过来,然后开始在原来的内容上构造新元素。并释放原来的空间。所以说,vector一旦空间重新配置,迭代器就会失效。
2. list
list的好处是每次插入或者删除一个元素的时候,就配置或者释放一个元素空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或者删除,李斯特永远是常数时间。
(1) list的结点
list是一个双向链表。
//双向链表
template<class T>
struct list_node
{
typedef list_node<T>* void_pointer;
void_pointer prev;
void_pointer next;
T data;
};
(2) list迭代器
迭代器,list不能像vector那样用普通指针做迭代器,因为节点不保证在储存空间连续存在。list迭代器必须有能力指向list节点,并且可以正确的递增 递减 取值 成员存取等。和vector的另一个区别就是插入操作和接合操作不会造成迭代器失效。
因为list是一个双向链表,所以list提供的是Bidirectional Iterator。
template<class T>
struct list_iterator : public iterator<bidirectional_iterator_tag, T>
{
typedef list_iterator<T> self;
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef list_node<T>* link_type;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
link_type node; //迭代器内部的指针,指向list_node 节点
...
}
3. deque
deque是一种双向开口的连续性空间。所谓双向开口,意思就是头尾两端可以分别做元素的插入和删除操作。vector虽然也可以头尾两端做操作,但是,对头部做操作的时候效率特别差,不能接受。还有一点和vector不同,deque没有所谓容量。因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并连接起来。
(1) deque的中控器
deque的连续空间实际上是由一段以段的定量连续空间构成的。一旦有必要在deque的前端或者尾端增加新的空间,便配置以段的定量空间串接在整个deque的头部或者尾部。deque的最大任务就是维护整体连续的假象。
deque采用一块所谓的map,这里的map实际上就是一小块连续的空间。其中每个元素都是一个指针,指向另一块比较大的连续空间,成为缓冲区。缓冲区是deque储存数据的主题。
(2) deque的迭代器
要维持整体的连续,就要靠迭代器的operator++和operator–身上。迭代器应该能判断当前是否实在缓冲区边缘,如果是的话,下次移动就要移动到下一个或者上一个缓冲区。
template<class T,class Ref,class Ptr,size_t Bufsize>
struct deque_iterator : public iterator<random_access_iterator_tag,T>
{
typedef deque_iterator<T, T&, T*, Bufsize> iterator;
typedef deque_iterator<T, const T&, const T*, Bufsize> const_iterator;
static size_t buf_size() { return _deque_buf_size(Bufsize, sizeof(T)); }
typedef T value_type;
typedef T* value_pointer;
typedef Ptr pointer;
typedef Ref reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef T** map_pointer; //中控器 指向指针的指针
typedef deque_iterator self;
//保持与容器的连接
T* cur;
T* first;
T* last;
map_pointer node; //指向在那个缓冲区
...
}
(3) deque数据结构
deque除了维护一个指向map的指针,还要维护两个迭代器,分别是指向第一个缓冲区的第一个元素和最后一个缓冲区最后一个元素后边的一个位置(STL左闭右开)。此外,还要记住目前map的大小,一旦map大小不足,就要重新分配一个map.重新分配也是配置更大的,拷贝原来的,释放原来的。
template<class T,class Alloc = myalloc,size_t Bufsize = 0>
class deque
{
public:
typedef T value_type;
typedef value_type* pointer;
typedef size_t size_type;
typedef T& reference;
typedef ptrdiff_t difference_type;
typedef deque_iterator<T, T&, T*, Bufsize> iterator;
typedef pointer* map_pointer;
typedef simple_alloc<value_type, Alloc> data_allocator;
typedef simple_alloc<pointer, Alloc> map_allocator;
iterator start; //第一个节点
iterator finish; //第二个节点
map_pointer map; //指向map map 是一块连续空间 其每个元素都是一个指针,指向一个节点(缓冲区)
size_type map_size; //map里面有多少个指针
...
}
4. heap
heap其实是priority_queue的助手。priority_queue默认是最大堆。它允许用户以任何次序将元素推入容器中,但是取出来的时候一定是最大的在最上边。他使用完全二叉树实现的。那么这个完全二叉树又是使用vector实现的。
对于一个数组的 i 处。其左节点一定位于 2i 处,右节点一定位于 2i+1 处,父节点位于 i/2 处。
heap主要有四个算法。
(1)push_heap
因为是完全二叉树,所以将新加进来的元素一定要放在最下一层的叶节点而且是从左到右的第一个空格位置。也就是底层vcector的最后面,也就是end处。但是这个位置不一定适合这个元素,所以要上溯。和父节点比较大小,如果比父节点大就换位置,一直比较到合适的位置或者到根节点。
template<class RandomIter, class Distance, class T>
void _push_heap(RandomIter first, Distance holeindex, Distance topindex, T value)
{
Distance parent = (holeindex - 1) / 2; //找出父节点
while (holeindex > topindex && *(first + parent) < value)
{
//当这个空洞还没有到顶端,而且现在的值大于父节点 也就是不符合最大堆的顺序
*(first + holeindex) = *(first + parent); //让空洞的位置是父节点,就是将父节点放下来
holeindex = parent; //调整空洞位置 将空洞位置换上去
parent = (holeindex - 1) / 2; //新洞的父节点
} //持续到顶端 或者满足最大堆
*(first + holeindex) = value; //令洞值为新值,完成插入操作
}
(2) pop_heap
删除就是将根的值放在vector的最后,然后在调整整个树。因为根没了要重新找一个。就是一次比较左右节点,大的就上去。一直比较直到合适。但是,因为最后一个位置被删除的根节点占了,所以,原来的最后位置那个点就要重新调整位置。也就是上溯,找到他合适的位置。
其实被删除的那个元素是没有真正的删除的,他是在vector的最后的那个位置,想要真正删除要从vector中删除。
template<class RandomIter,class Distance,class T>
void _adjust_heap(RandomIter first, Distance holeindex, Distance len, T value)
{
Distance topindex = holeindex;
Distance secondChild = 2 * holeindex + 2; //洞节点的右节点
while (secondChild < len)
{
//比较洞节点的左右两个值 找出最大的
if (*(first + secondChild) < *(first + (secondChild - 1)))
{
secondChild--;
//在令目前的洞值为最大的那个点 洞值下移到较大的那个点处 因为根结点要大
*(first + holeindex) = *(first + secondChild);
holeindex = secondChild;
//找出新的右节点
secondChild = 2 * (secondChild + 1);
}
}
if (secondChild == len)
{
//如果没有右节点
//那就让左节点是洞值
*(first + holeindex) = *(first + (secondChild - 1));
holeindex = secondChild - 1;
}
//在把之前的尾节点的值插入正确的位置
_push_heap(first,holeindex,topindex,value);
}
(3) sort_heap
每次pop_heap()之后都将大的元素放到了最后边 那么将所有的元素都pop_heap()一边就会有一个递增序列。
template<class RandomIter>
void sort_heap(RandomIter first,RandomIter last)
{
while (last - first > 1)
{
pop_heap(first, last--);
}
}
(4)make_heap
将一段现有的数据转换为heap。
template<class RandomIter,class T,class Distance>
void _make_heap(RandomIter first, RandomIter last, T*, Distance*)
{
if (last - first < 2)
{
return; //长度是0或者1不用重新排
}
Distance len = last - first;
//找出第一个需要重新拍的子树头部,因为任何叶子节点不需要执行perlocate down 所以有以下计算
Distance holeindex = (len - 2) / 2;
while (ture)
{
//重排以holeindex为首的子树
_adjust_heap(first, holeindex, len, T(*(first + parent)));
if (holeindex == 0) return;
holeindex--;
}
}
5. priority_queue
优先队列就是利用上面的heap完成的。heap是以vector为底层完成的。
priority_queue<T,vector,less> 大顶堆
priority_queue<T,vector,greater> 小顶堆
template<class T,class Sequence = vector<T>,class Compare = std::less<typename Sequence::value_type>>
class priority_queue
{
typedef typename Sequence::value_type value_type;
typedef typename Sequence::size_type size_type;
typedef typename Sequence::reference reference;
typedef typename Sequence::const_reference const_reference;
protected:
Sequence c; //底层容器
Compare comp; //权值比较标准
...
}
6. stack
栈先进后出。它只有一个出口。它可以新增,移除,取得最顶端元素。但是,除了最顶端外,没有可以其他方法可以存取stack的其他元素。也就是stack不允许遍历。
stack可以用deque和list作为底层
deque和list作为底层的话就是封闭头部开口
stack没有迭代器。
7. queue
queue就是先进先出
他有两个口,最低端加入元素,最顶端取出元素。所以,也不允许遍历,也没有迭代器
他是以deque作为底层。封闭低端的出口和前端的入口即可。
list也是可以作为底层的。