序列式容器
1. vector
1. 初始化
以vector<int> a(2, 9)
为例,调用fill_initialize(size_type n, const T& value)
来分配空间、放入元素、更新迭代器。
// 填充并初始化
void fill_initialize(size_type n, const T& value){
start = allocate_and_fill(n, value);
finish = start + n;
end_of_storage = finish;
}
// 配置而后填充
iterator allocate_and_fill(size_type n, const T& x){
iterator result = data_allocator::allocate(n);
uninitialized_fill_n(result, n, x);
return result;
}
在fill_initialize()
中,首先调用适配器(无论是一级还是二级)获取n个空间,然后取得指向这块内存首地址的指针,最后放入指定值并返回首地址的指针。
2. push_back()
如果有剩余的空间,则在finish
后构造一个新的内存并填入x,然后finish
向后移一位。否则,调用insert_aux
在最后配置新的空间,并赋值。insert_aux(iterator position, const T& x)
细节如下:
-
后面还有空间:
- 将最后一个元素后移一位,然后
finish++
。 - 将
[position, finish-2)
的元素向后移一位,这里的finish-2
的元素即为最开始的最后一个元素,刚才已经后移了。 - 将 x 赋予
position
这个地址。
- 将最后一个元素后移一位,然后
-
后面没有空间了:
size()
翻倍,如果原本为 0 ,则置为 1,设置为len
。- 新建一个
len
这么大的空间, - 将
pos
及之前的拷贝到新的空间,然后构造空间插入x,再将pos
之后的拷贝进去。 - 释放原空间
- 将迭代器更新为指向新的空间。
3. pop_back()
finish
向前移一位,然后销毁finish
所在的那个空间。(finish
指向的是最后一个元素的后一个)
4. erase()
iterator erase(iterator first, iterator last)
- 将
[last,finish)
的元素移到以first
开始的地方,然后返回最后一个迭代器i
。 - 将
[i,finish)
的空间销毁。 - 更新
finish
并返回first
。
5. insert()
void <vector T, Alloc>::insert(iterator position, size_type n, const T& x)
- 首先保证插入元素个数不为0。
- 如果备用空间足够:
- 记录当前
finish
这个迭代器,计算插入点之后有多少个元素elems_after
。 - 如果插入点之后的元素个数大于新增元素个数,那么当前
position
位置的元素会被移动到当前finish
之前。- 将
[finish-n,finish)
的元素移动到以finish
开始的位置(移动了n个),更新finish
。 - 将
[position,old_finish-n)
的元素移动(copy_backard
)到以old_finish
开始的位置。 - 填入 n 个 x。
- 将
- 如果插入点之后的元素个数小于等于新增元素个数,那么从当前
position
位置到当前finish
之间都会被填入 x,finish
之后也会被填入n-elems_after
个 x。- 在
finish
这填入n-elems_after
个 x,并更新finish
。 - 把
[position,old_finish)
之间的元素移动到new_finish
之后。 - 在
[position,old_finish)
之间填入n 个 x。
- 在
- 记录当前
- 如果备用空间不足:
- 需要一个更大的新的内存空间,其长度为:
old_size + max(old_size, n)
。 - 构造这块空间,并更新
start
和finish
。 - 做如下尝试:将
[old_start, position)
的元素拷贝过来,填入 n 个 x,再将[position, old_finish)
的元素拷贝过来。这里使用的是uninitialized_copy()
和uninitialized_fill_n()
。 - 如果有异常,就需要实现
commit or rollback
- 析构从
[start, finish)
的所有对象 - 释放从
start
开始的len
个空间。 - 抛出异常。
- 析构从
- 销毁原有对象,释放空间。
- 更新
start
,finish
,end_of_storge
。
- 需要一个更大的新的内存空间,其长度为:
2. list
1. push_back()
调用insert(end(), x)
来进行链表的插入,insert()
就是一个双向链表的插入。
- 构造一个新的节点
tmp
。 tmp->prev=pos.node->prev, tmp->next=pos.node
。pos.node->prev->next=tmp, pos.node->prev=tmp
。- 返回
tmp
。
2. push_front()
调用insert(begin(), x)
。
3. erase()
- 标记
pos
的前一个节点和后一个节点。 - 连接这两个节点
- 销毁原
pos
的节点并回收内存 - 返回后一个节点
4. clear()
遍历每一个节点,销毁他。然后只留下最原始的node
,让node
的前后指针都指向自己。
5. remove()
遍历 list 每一个节点,如果节点值为指定值,就调用erase
删除它。
template <class T, class Alloc>
void list<T, Alloc>::remove(const T& value){
iterator first = begin();
iterator last = end();
while(first != last){
iterator next = first;
++next;
if(*first == value)
erase(first);
first = next;
// first被删除后,更新为后一个迭代器,以进行接下来的迭代。
}
}
6. unique()
unique()
用于移除 连续且相同 的的元素,只保留一个。从头到尾遍历每一个节点:
- 之后的值只要与其相同,就删掉。
- 不一样,遍历下一个节点。
template <class T, class Alloc>
void list<T, Alloc>::unique(){
iterator first = begin();
iterator last = end();
if(first == last)
return;
iterator next = first;
while(++next != last){ // 可保证每一轮 next都是 first下一个
if(*first == *next)
erase(next);
else
first = next;
next = first; // next 如果被删掉,这里重新设置,将两者对其,在while中对next更新
}
}
7. transfer()
transfer()
可以对 List 中连续范围的元素进行迁移。就是将这一范围的链表扣下来,并将两端与指定位置与其前一个节点连起来,然后将被挖掉那一段节点的两端连起来就可以了。transfer()
是实现splice()
、merge()
、sort()
、reverse()
的基础。
8. splice()
主要就是通过transfer()
实现的。将一段 list 插入另一段 list 的某个位置
9. merge()
merge()
可以将两个链表合并,但前提是两个链表必须是有序的。
- 挨个比较两个链表的第一个节点,直到有一个链表被遍历结束,如果
*first2 < *first1
,则将first2
迁移到first1
前,否则,first1
后移。 - 如果
first2 != last2
,说明第二个链表剩的都比第一个大,直接把第二个链表剩余的部分放到第一个链表后面。
10. reverse()
- 如果链表为空或只有一个元素,就啥也不做。
- 从第二个节点开始遍历,依次将其放到(
transfer
)第一个节点之前,直到最后一个节点。(因为begin()
指向的永远是尾端空白节点node
的下一个节点,所以在这个过程中,它会不断地更新)。
11. sort()
STL的sort()
只接受RandomAccessIterator
,所以List
必须使用自己的sort()
。
- 如果链表为空或只有一个元素,就啥也不做。
- 新建两个链表
carry
、counter[64]
来存放一些中介数据。 - 只要当前链表还有节点
- 链表的第一个节点迁移到
carry
的最开始。 - 遍历
counter
的前fill
层,只要有东西,就依次合并排序。 - 把最后的结果放到
counter[i]
中,如果i==fill
存储到了最后一层,fill++
。
- 链表的第一个节点迁移到
- 再把
counter
由低到高逐层合并,最终结果保存在counter[fill-1]
中。 - 把这个结果交换给
List
。(merge
只对顺序list操作,合并完依然有序)
3. deque
vector
是单向开口的连续线性空间,deque
则是一种双向开口的连续线性空间。deque
和 vector
的最大差异,一在于deque
允许于常数时间内对起头端进行元素的插入或移除操作,二在于deque
没有所谓容量 (capacity
)观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。对deque
进行的排序操作,为了最高效率,可将deque
先完整复制到一个vector
身上,将vector
排序后(利用 STL sort 算法),再复制回deque
。
deque
系由一段一段的定量连续空间构成。一旦有必要在 deque
的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque
的头端或尾端。deque
采用一块所谓的map(注意,不是STL的 map
容器)作为主控。这里所谓map
是一小块连续空间,其中每个元素(此处称为一个节点,node
)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。map
其实是一个T**
,指向每一个缓冲区。
中控器从中间开始设置start
和finish
这两个迭代器,随着元素的加入,逐渐向两侧拓展,每个迭代器含有四个元素:cur
, first
, last
, node
,分别指向所指的缓冲区的头、尾、当前填充元素的位置、这个缓冲区所归属的中控器。注意!start.cur
指向这个deque
的第一个元素,finish.cur
指向这个deque
的最后一个元素的后一个位置。
下面是 deque
迭代器的几个关键行为。由于迭代器内对各种指针运算都进行了重载操作,所以各种指针运算如加、减、前进、后退都不能直观视之。其中最关键的就是:一旦行进时遇到缓冲区边缘,要特别当心,视前进或后退而定,可能需要调用set_node()
跳一个缓冲区。
void set_node(map_pointer new_node){
node = new_node;
first = *new_node;
last = first + difference_type(buffer_size());
}
deque
的重载运算子的主要重载就是加入了对缓冲区边界的考虑,以下举例其中几个。
difference_type operator-(const self& x) const{
return difference_type(buffer_size()) * (node - x.node -1) + (cur - first) + (x.last + x.cur);
}
self& operator++(){
++cur;
if(cur == last){
set_node(node+1); // 这个满了,那么移到下一个缓冲区的第一个。
cur = first;
}
return *this;
}
self& operator++(int){
self tmp = *this;
++*this;
return tmp;
}
1. fill_initialize()
- 利用
create_map_and_nodes()
构建结构- 计算需要的节点数
- 设置一个
map
要管理多少个节点,最少8个,最多为所需节点数+2. - 分配空间
- 设置
nstart
,nfinish
在中控器的中间,使前后的扩充空间一样大。 - 为中控器的每个节点设置缓冲区
- 设置
start
,finish
,x.cur
的具体内容。
- 利用
uninitialized_fill()
填充所有节点。
2. push_back/front()
- 如果这个缓冲区还有两个及以上的空间,就在
finish.cur
上构造元素,更新finish.cur
。 - 如果只有一个,就只够放一个元素,
finish.cur
需要放到下一个缓冲区了(finish.cur
指向最后一个元素的下一个位置),调用push_back_aux()
- 复制插入值
- 如果空间不足,则需要换一个更大的中控器(
reserve_map_at_back()
) - 新建一个缓冲区(
finish.node
的下一个) - 将
finish
设置到下一个缓冲区,更新finish.cur
为下一个缓冲区的第一个。
push_front()
原理与push_back()
一致,但有个区别使只有在第一缓冲区没有空间的时候才在前面新建一个缓冲区,这是因为为了符合前闭后开的原则,start.cur
指向的是第一个节点,而finish.cur
指向最后一个节点的下一个节点。
3. reserve_map_at_back/front()
让我们回头看看一个悬而未解的问题:什么时候map需要重新整治?这个问题的判断由reserve_map_at_back()
和reserve_map_at_front()
进行,实际操作则由reallocate_map()
执行:
查看中控器前/后端的节点空间是否足够,不足则说明map
已经有一端已经到边界了。调用reallocate_map()
更新map
的组织结构。
-
如果新的节点数还不到
map
容量的一半,这说明不需要换map
,只需要把start
,finish
向中间挪一下。计算新的start
应该在的位置。new_start = map + (map_size - new_num_nodes) / 2 + (add_at_front ? nodes_to_add : 0);
- 如果
new_start < start
,说明map
应该向左移,那么顺序将所有节点复制到从new_start
开始的地方 - 否则,说明
map
应该向右移,那么倒序将所有节点复制到从new_start+old_num_nodes
结尾的地方
- 如果
-
如果超过一半了,则应该更换一个更大的
map
- 申请一个新的更大空间的
map
,空间大小为:map_size + max(map_size, node_to_add) + 2
。 - 再用同样的方法选择
new_start
的位置 - 把原来的
map
拷贝过来,并释放原map
- 更新
map
的初始地址和大小
- 申请一个新的更大空间的
-
重新设置
start
和finish
。
4. pop_back / front()
- 如果最后一个缓冲区不是只有
finish.cur
。调整finish.cur
的位置,并将最后一个元素析构。 - 如果只有
finish.cur
,调用pop_back_aux()
。- 释放最后一个缓冲区
deallocate_node(x.first)
。 - 调整
finish
至前一个缓冲区。 finish.cur
指向前一个缓冲区的最后一个元素,并将该元素析构destroy(x.cur)
。
- 释放最后一个缓冲区
这里只说pop_back()
,pop_front()
原理相同。只是判断条件为第一个元素是否在第一个缓冲区的last-1
的位置。
5. clear()
- 对除了头尾缓冲区之外的所有的缓冲区进行元素的析构和缓冲区的释放。
- 如果还剩两个缓冲区,将这两个缓冲区的元素析构,并释放尾缓冲区
- 如果还剩一个缓冲区,将这个缓冲区的元素析构,不用释放
6. erase()
看清除点前后哪边元素少
- 前面少,就倒序拷贝前面的元素(向后移一位),然后
pop_front()
。 - 后面少,就正序拷贝后面的元素(向前移一位),然后
pop_back()
。
如果删除的节点较多,还要释放冗余的缓冲区,并更新start
,finish
,最后返回被删除的指定部分的第一个的迭代器。
7. insert()
- 插入在最前面,调用
push_front()
,返回start
。 - 插入在最后面,调用
push_back()
,然后返回--finish
这个迭代器。 - 在中间,就调用
insert_aux()
- 如果插入点前面元素少,在最前端插入一个与第一个元素同值的元素,把第二个到
pos-1
的元素整体向前用copy
移一位。 - 如果插入点后面元素少,在最后端插入一个与最后一个元素同值的元素,把
pos
到倒数二个的元素整体向后用copy_backward
移一位。 - 在指定位置插入指定值,返回
pos
。
- 如果插入点前面元素少,在最前端插入一个与第一个元素同值的元素,把第二个到
4. stack
以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特性,形成一个stack
,是很容易做到的deque
是双向开口的数据结构,若以deque
为底部结构并封闭其头端开口,便轻而易举地形成了一个stack
。因此,SGI STL
便以deque
作为缺省情况下的stack
底部结构。由于stack
系以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter
(配接器),因此,STL stack
往往不被归类为container
(容器),而被归类为container adapter
。
stack
所有元素的进出都必须符合“先进后出”的条件,只有stack
顶端的元素,才有机会被外界取用。stack
不提供走访功能,也不提供迭代器。
5. queue
queue
是一种先进先出的数据结构,除了最底部可以加入,最顶端可以取出之外,没有别的办法可以存取其中的元素,所以queue
也不允许遍历行为。queue
也是一种配置器,没有迭代器。也用的deque.
6. heap
堆是一个存储在vector
中的完全二叉树,如果以大、小根堆的方式存储,则可以作为优先队列的底层机制,在大根堆中,如果将 0 号位置的元素保留(设为±∞
),则留(或设为无限大值或无限小值),那当complete binary tree
中的某个节点位于vector
的i
处时,其左子节点必位于vector
的2i
处,其右子节点必位于vector
的2i+1
处,其父节点必位于“i/2
”处。通过这么简单的位置规则,vector
可以轻易实现出complete binary tree
。这种以vector
表述tree
的方式,我们称为隐式表述法(implicit representation
) 。
以下算法的实现都是基于大根堆。
1. push_heap
- 先将指定元素通过
push_back()
放于vector
的最尾端。 - 找到当前节点(最后一个元素)的父节点
- 上溯
- 只要当前节点没有被移动到堆的最顶端且其父节点的值比它小:
- 当前节点的值设为父结点的值
- 向上一层更新当前节点,重置父节点
- 将第三步结束的哪个节点的值设为最开始插入的那个节点的值(当前节点本来的值已经在之前被移动到下面了)
- 只要当前节点没有被移动到堆的最顶端且其父节点的值比它小:
2. pop_heap()
- 将第一个元素与最后一个交换,然后
--last
。这时候最大值就被放到vector
的最尾端,vector[1]
处则是一个很小的值。标记为vector[holeIndex]
。 - 下溯:
- 依次比较
vector[holeIndex]
与其左右子节点的值,并与其较大值交换。这里要注意vector[holeIndex]
的子节点的个数可能为1和2,就要区别一下,每次交换都要更新holeIndex
。 - 当
vector[holeIndex]
没有子节点的时候,说明其到了合适的那层,将vector[holeIndex]
设为初值(当时移动到vector[1]
的那个值)。
- 依次比较
3. sort_heap()
既然每次pop_heap
可获得heap
中键值最大的元素,如果持续对整个heap
做pop_heap
操作,每次将操作范围从后向前缩减一个元素(因为pop_heap
会把键值最大的元素放在底部容器的最尾端),当整个程序执行完毕时,我们便有了一个递增序列。
4. make_heap()
- 长度为0 或 1则不用重新排列。
- 计算第一个需要下溯的父节点(也就是最后一个父节点)的位置
Distance parent = (last - first - 2) / 2
。 - 从后向前对每一个父节点进行一次下溯。
heap
的所有元素都必须遵循特别的(complete binary tree
)排列规则,所以heap
不提供遍历功能,也不提供迭代器。
7. priority_queue()
顾名思义,priority_queue
是一个拥有权值观念的queue
,它允许加入新元素、移除旧元素、审视元素值等功能。由于这是一个queue
,所以只允许在底端加人元素,并从顶端取出元素,除此之外别无其它存取元素的途径。priority-queue
带有权值观念,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。权值最高者,排在最前面。
之前有提到堆可以作为优先队列的底层机制,所以具体实现是依赖heap
实现的
1. push()
- 指定值放入容器的最后
- 利用
push_heap()
调整顺序
2. pop()
- 利用
pop_heap()
调整顺序 - 利用
pop_back()
得到极值。
priority_queue
的所有元素,进出都有一定的规则,只有queue
顶端的元素(权值最高者),才有机会被外界取用。priority_queue
不提供遍历功能,也不提供迭代器。