《STL源码剖析》——第四章、序列容器
1、容器的概观与分类
所谓序列式容器,其中的元素都可序(ordered)【比如可以使用sort进行排序】,但未必有序(sorted)。C++语言本身提供了一个序列式容器array,STL另外再提供vector,list,deque,stack,queue,priority-queue 等等序列式容器。其中stack和queue由于只是将 deque 头换面而成,技术上被归类为一种配接器(adapter)。
2、vector
vector的数据安排以及操作方式,与array非常相似。两者的唯一差别在于空间的运用的灵活性。
array是静态空间,一旦配置了就不能改变;要换个大(或小)一点的房子,可以,一切琐细得由客户端自己来:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。
vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必因为害怕空间不足而一开始就要求一个大块头array了,我们可以安心使用vector,吃多少用多少。
- 常用的源码:
注意:
- size() & captical:
表示已存储数据的大小,而capital则是内部开辟的空间的大小
所以,在erase、pop_back、clear等删除操作,都不会使得其captial产生变化,只会变的就是size()
- 动态空间配置:
注意,所谓动态增加大小,并不是在原空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。这是程序员易犯的一个错误,务需小心。
- insert():
insert()会根据需要插入元素的位置p以及p后面的元素个数与需要插入元素个数n进行比较,分两种情况进行插入
- emplace_back() VS push_back()减少内存拷贝和移动
struct President
{
President(std::string && p_name, std::string && p_country, int p_year)
: name(std::move(p_name)), country(std::move(p_country)), year(p_year)
{
std::cout << "I am being constructed.\n";
}
President(President&& other)
: name(std::move(other.name)), country(std::move(other.country)), year(other.year)
{
std::cout << "I am being moved.\n";
}
President& operator=(const President& other) = default;
}; int main()
{
std::vector<President> elections;
std::cout << "emplace_back:\n";
elections.emplace_back("Nelson Mandela", "South Africa", ); std::vector<President> reElections;
std::cout << "\npush_back:\n";
reElections.push_back(President("Franklin Delano Roosevelt", "the USA", ));
}
emplace_back:
I am being constructed. push_back:
I am being constructed.
I am being moved.
merge()函数:
merge方式要注意三点:
merge(vec1.begin(),vec1.end(),vec2.begin(),vec2.end(),vec3.begin());
1、vec1,和vec2需要经过排序,merge只能合并排序后的集合,不然会报错。
2、vec3需要指定好大小,不然会报错。
3、merge的时候指定vec3的位置一定要从begin开始,如果指定了end,它会认为没有空间,当然,中间的位置我没有试,回头有空试一下。
3、list
相较于vector的连续线性空间,1ist就显得复杂许多,它的好处是每次插人或删除一个元素,就配置或释放一个元素空间。因此,list 对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插人或元素移除,list永远是常数时间。
由于STL1ist是一个双向链表(double linked-list),迭代器必须具备前移、后移的能力,所以1ist 提供的是Bidirectional lterators。
list 有一个重要性质:插入操作(insert)和接合操作(splice)都不会造成原有的list迭代器失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效。甚至1ist的元素删除操作(erase),也只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。
- SGI list 是一个环状双向链表,它只需要一个指针就可以完整表现整个链表
- list链表的初始结构:
node->next = node;
node->prv = node;
- .transfter() //迁移函数
void transfer(iterator position, iterator first, iterator last);
将list2的first-last之间的元素插入到list1中的position中
- .splice() //衔接函数
void splice( const_iterator pos, list& other ); |
(1) |
|
void splice( const_iterator pos, list&& other ); |
(1) |
(C++11 起) |
void splice( const_iterator pos, list& other, const_iterator it ); |
(2) |
|
void splice( const_iterator pos, list&& other, const_iterator it ); |
(2) |
(C++11 起) |
void splice( const_iterator pos, list& other, const_iterator first, const_iterator last); |
(3) |
|
void splice( const_iterator pos, list&& other, const_iterator first, const_iterator last ); |
(3) |
(C++11 起) |
从一个 list 转移元素给另一个。
不复制或移动元素,仅重指向链表结点的内部指针。若 get_allocator() != other.get_allocator() 则行为未定义。没有迭代器或引用被非法化,指向被移动元素的迭代器保持合法,但现在指代到 *this 中,而非到 other 中。
1) 从 other 转移所有元素到 *this 中。元素被插入到 pos 所指向的元素之前。操作后容器 other 变为空。若 other 与 *this 指代同一对象则行为未定义。
2) 从 other 转移 it 所指向的元素到 *this 。元素被插入到 pos 所指向的元素之前。
3) 从 other 转移范围 [first, last) 中的元素到 *this 。元素被插入到 pos 所指向的元素之前。若 pos 是范围 [first,last) 中的迭代器则行为未定义。
参数
pos |
- |
将插入内容到其前的元素 |
other |
- |
要自之转移内容的另一容器 |
it |
- |
要从 other 转移到 *this 的元素 |
first, last |
- |
要从 other 转移到 *this 的元素范围 |
- .merge()
c1.merge(c2) |
//合并2个有序的链表并使之有序,从新放到c1里,释放c2。 |
c1.merge(c2,comp) |
//合并2个有序的链表并使之按照自定义规则排序之后从新放到c1中,释放c2。 |
c1.splice(c1.beg,c2) |
//将c2连接在c1的beg位置,释放c2 |
c1.splice(c1.beg,c2,c2.beg) |
//将c2的beg位置的元素连接到c1的beg位置,并且在c2中释放掉beg位置的元素 |
c1.splice(c1.beg,c2,c2.beg,c2.end) |
//将c2的[beg,end)位置的元素连接到c1的beg位置并且释放c2的[beg,end)位置的元素 |
归并二个已排序链表为一个。链表应以升序排序。
不复制元素。操作后容器 other 变为空。若 other 与 *this 指代同一对象则函数不做任何事。若 get_allocator() != other.get_allocator() ,则行为未定义。没有引用和迭代器变得非法,除了被移动元素的迭代器现在指代到 *this 中,而非到 other 中,第一版本用 operator< 比较元素,第二版本用给定的比较函数 comp 。
此操作是稳定的:对于二个链表中的等价元素,来自 *this 的元素始终前驱来自 other 的元素,而且 *this 和 other 的等价元素顺序不更改。
参数
other |
- |
要交换的另一容器 |
comp |
- |
比较函数对象(即满足比较 (Compare) 概念的对象),若第一参数小于(即先序于)第二参数则返回 true 。 比较函数的签名应等价于如下: bool cmp(const Type1 &a, const Type2 &b); 虽然签名不必有 const & ,函数也不能修改传递给它的对象,而且必须接受(可为 const 的)类型 Type1 与 Type2的值,无关乎值类别(从而不允许 Type1 & ,亦不允许 Type1 ,除非 Type1 的移动等价于复制 (C++11 起))。 类型 Type1 与 Type2 必须使得 list<T,Allocator>::const_iterator 类型的对象能在解引用后隐式转换到这两个类型。 |
4、deque
deque和vector的最大差异:【vector与deque都可以随机读取】
一在于deque允许于常数时间内对起头端进行元素的插入或移除操作,
二在于deque没有所谓容量(capacity)观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。
- 中控器:
deque是连续空间(至少逻辑上看来如此)。
deque系由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。
deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。避开了“重新配置、复制、释放”的轮回,代价则是复杂的迭代器架构。
deque采用一块所谓的map(注意,不是STL的map容器)作为主控。这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。SGI STL允许我们指定缓冲区大小,默认值0表示将使用512bytes缓冲区。
当map的空间也不够后,会开辟另一个大的map空间
- 迭代器:
deque是分段连续空间。维持其“整体连续”假象的任务,落在了迭代器的operator++和operator--两个运算子身上。
- 数据结构:
- deque 除了维护一个先前说过的指向map的指针外,也维护 start,finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一位置)。此外,它当然也必须记住目前的map大小。因为一旦map所提供的节点不足,就必须重新配置更大的一块map。
- 什么时候map需要重新整治?这个问题的判断由 reserve_map_at_back()和 reserve_map_at_front()进行,实际操作则由reallocate_map()执行;
- 一个deque对象包含四个成员变量,_M_map指向主控器,_M_map_size表示中控器的大小,能够容纳多少个指针,_M_start表示deque的迭代器,所有元素的起始位置,_M_finish表示deque的迭代器,所有元素的终止位置。
- deque的insert()操作:首先判断插入的地方是头或者尾,如果都不是则在调用一个名为insert_aux的辅助函数。此辅助函数通过判断当前的插入位置更靠近头端或者尾端。
- deque的+=操作:首先判断是否在同一级缓冲区区域,如果不在,在确定应该夸几个缓冲区,然后到相应的缓冲区后,再移动
5、stack
- sack定义完整列表
deque是双向开口的数据结构,若以deque为底部结构并封闭其头端开口,便轻而易举地形成了一个stack。因此,SGI STL便以deque作为缺省情况下的stack底部结构,stack的实现因而非常简单,源代码十分简短,
- stack没有迭代器
- 以list作为stack的底层容器
6、queue
- queue定义完整列表
- deque是双向开口的数据结构,若以deque为底部结构并封闭其底端的出口和前端的入口,便轻而易举地形成了一个queue。因此,SGISTL便以deque作为缺省情况下的queue底部结构,queue的实现因而非常简单。
- queue没有迭代器
- 以list作为queue的底层容器
- 优先队列
- priority_queue<Type, Container, Functional>
7、 heap(隐式表述,implicit representation)【堆排序】
- 概念
heap并不归属于STL容器组件,它是个幕后英雄,扮演priority queue
的助手。顾名思义,priority queue允许用户以任何次序将任何元素推入容器内,但取出时一定是从优先权最高(也就是数值最高)的元素开始取。binary max heap正是具有这样的特性,适合作为priority queue的底层机制。
如果使用list作为priority queue的底层机制,元素插入操作可享常数时间。但是要找到list中的极值,却需要对整个list进行线性扫描。我们也可以改变做法,让元素插人前先经过排序这一关,使得list的元素值总是由小到大(或由大到小),但这么一来,收之东隅却失之桑榆:虽然取得极值以及元素删除操作达到最高效率,可元素的插入却只有线性表现。
以binary search tree作为priority queue的底层机制。这么一来,元素的插入和极值的取得就有O(logN)的表现。
但杀鸡用牛刀,未免小题大做,一来binary search tree的输入需要足够的随机性,二来binary search tree并不容易实现。priority queue的复杂度,最好介于queue和binary search tree 之间,才算适得其所。binary heap便是这种条件下的适当候选人。
binary heap就是一种 complete binary tree(完全二叉树)2,也就是说,整棵binary tree除了最底层的叶节点(s)之外,是填满的,而最底层的叶节点(s)由左至右又不得有空隙。【即是一棵完全搜索二叉树】
- heap算法
- push_heap
实现的是堆排序中的插入操作
//向上调整
void upAdjust(int L, int R)
{
int i = R, j = (i - ) / ;//i为欲调整结点,j为其父亲
while (j >= L)
{
if (v[j] < v[i])//父节点小了,那么就将孩子节点调上来
{
swap(v[i], v[j]);
i = j;
j = (i - ) / ;//继续向上遍历
}
else//无需调整
break;
}
}
void insert(int x)
{
v[n] = x;//将新加入的值放置在数组的最后,切记保证数组空间充足
upAdjust(, n);//向上调整新加入的结点n
}
- pop_heap
实现的是堆排序的删除操作pop_heap //向下调整
void downAdjust(int L, int R)
{
int i = L, j = * L + ;//i为父节点,j为左子节点
while (j <= R)
{
if (j + <= R && v[j + ] > v[j])//若有右节点,且右节点大,那么就选右节点,即选取最大的子节点与父节点对比
++j;//选取了右节点
if (v[j] <= v[i])//孩子节点都比父节点小,满足条件,无需调整
break;
//不满足的话,那么我就将最大孩子节点j与父节点i对调,
swap(v[i], v[j]);
i = j;
j = * i + ;//继续向下遍历
}
} //删除堆顶元素 void deleteTop()
{
v[] = v[n - ];//也就是堆顶使用最后一个数值来替代
downAdjust(, n - );//然后对前n-1个数进行排序
}
- sort_heap
就是不断的pop出最大的元素sort_heap
实现的就是堆排序
for (int i = n - ; i > ; --i)//从最后开始交换,直到只剩下最后一个数字
{
swap(v[i], v[]);//每次都将最大值放到最后
downAdjust(, i - );//将前0-i个数字重新构成大根堆
}
- make_heap
实现的是堆排序的构建
//建堆
void createHeap()
{
for (int i = n / ; i >= ; --i)
downAdjust(i, n - );
}
- heap没有迭代器
8、 priority_queue
- 概念
- priority_queue带有权值观念,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。权值最高者,排在最前面。
- 定义
- 由于priority_queue 完全以底部容器为根据,再加上heap处理规则,所以其实现非常简单。缺省情况下是以vector为底部容器。具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器),因此,STL priority-queue往往不被归类为container(容器),而被归类为container adapter。
- 没有迭代器
9、slist
- 概述
- STL list是个双向链表(double linked list)。SGI STL另提供了一个单向链表(single linked list),名为slist。
- slist和list的主要差别在于,前者的迭代器属于单向的Forward lerator,后者的迭代器属于双向的Bidirectional lterator。单向链表所耗用的空间更小,某些操作更快,不失为另一种选择。
- 注意,根据STL的习惯,插入操作会将新元素插入于指定位置之前,而非之后。然而作为一个单向链表,slist没有任何方便的办法可以回头定出前一个位置,因此它必须从头找起。换句话说,除了slist起点处附近的区域之外,在其它位置上采用insert或erase操作函数,都属不智之举。这便是slist相较于1ist之下的大缺点。为此,slist 特别提供了insert_after()和erase_after()供灵活运用。
- 迭代器
10、常见错误总结:
迭代器失效:
·由于vector在扩容时,是在一块新地址上开辟空间,然后将原数据复制过来,并把原来的内存空间给释放了,所以一旦vector发生扩容,那么指向原来迭代器将会失效。
·由于list不是连续空间,所以删除和添加都在原的内存上添加或删除一个空间即可,所以指向原来的迭代器不会失效【除非该迭代器指向的位置被删除了】。
·迭代器不仅可以后移,而且可以前进的,--ptr, ++ptr
《STL源码剖析》——第四章、序列容器的更多相关文章
- Stl源码剖析 第三章 iterator摘要
1. Stl的设计思想是: 将数据容器和算法分开,彼此独立设计,最后再以一贴胶合剂将它们撮合在一起,这个胶合剂就是迭代器. 2. 从3.2节迭代器的实现可知,如果要设计一个与容器分离实现的迭代器,会暴 ...
- STL源码剖析之序列式容器
最近由于找工作需要,准备深入学习一下STL源码,我看的是侯捷所著的<STL源码剖析>.之所以看这本书主要是由于我过去曾经接触过一些台湾人,我一直觉得台湾人非常不错(这里不涉及任何政治,仅限 ...
- STL源码剖析 迭代器(iterator)概念与编程技法(三)
1 STL迭代器原理 1.1 迭代器(iterator)是一中检查容器内元素并遍历元素的数据类型,STL设计的精髓在于,把容器(Containers)和算法(Algorithms)分开,而迭代器(i ...
- 《STL源码剖析》相关面试题总结
原文链接:http://www.cnblogs.com/raichen/p/5817158.html 一.STL简介 STL提供六大组件,彼此可以组合套用: 容器容器就是各种数据结构,我就不多说,看看 ...
- 面试题总结(三)、《STL源码剖析》相关面试题总结
声明:本文主要探讨与STL实现相关的面试题,主要参考侯捷的<STL源码剖析>,每一个知识点讨论力求简洁,便于记忆,但讨论深度有限,如要深入研究可点击参考链接,希望对正在找工作的同学有点帮助 ...
- 【STL 源码剖析】浅谈 STL 迭代器与 traits 编程技法
大家好,我是小贺. 点赞再看,养成习惯 文章每周持续更新,可以微信搜索「herongwei」第一时间阅读和催更,本文 GitHub : https://github.com/rongweihe/Mor ...
- STL"源码"剖析-重点知识总结
STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...
- 【转载】STL"源码"剖析-重点知识总结
原文:STL"源码"剖析-重点知识总结 STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点 ...
- 《STL源码剖析》环境配置
首先,去侯捷网站下载相关文档:http://jjhou.boolan.com/jjwbooks-tass.htm. 这本书采用的是Cygnus C++ 2.91 for windows.下载地址:ht ...
- STL源码剖析读书笔记之vector
STL源码剖析读书笔记之vector 1.vector概述 vector是一种序列式容器,我的理解是vector就像数组.但是数组有一个很大的问题就是当我们分配 一个一定大小的数组的时候,起初也许我们 ...
随机推荐
- 七层模型? IP ,TCP/UDP ,HTTP ,RTSP ,FTP 分别在哪层?
IP: 网络层TCP/UDP: 传输层HTTP.RTSP.FTP: 应用层协议
- CocosCreator与Laya2.0区别
1图集: Laya:直接拖拽res里面的图片,当生成图集后,会自动优先使用图集的 Cocos:应该先打图集,且图集里的图就是图集里的图,资源里的图就是资源里的.2者不同 addChild Laya:会 ...
- 数组去重ES6
原文链接:https://juejin.im/post/5b17a2c251882513e9059231 1,去除简单类型 //ES6中新增了Set数据结构,类似于数组,但是 它的成员都是唯一的 ...
- Git 出现Branch master set up to track remote branch master问题 与忽略文件上传
错误:在push 到远程仓库是一直提示下列错误,检查了使用status检查了也没有发现错误,最后排查出来是当前分支为((no branch))即右上那个id (┬_┬)..... 原因:出现这个问题的 ...
- jQuery学习总结01-选择器
下面简单介绍一个常用的jQuery使用方法,其他详细的猛戳这里 一.选择器 1.parent > child 说明:在给定父类的情况下匹配所有的子类 示例: 描述:匹配表单中所有的的子级inpu ...
- R语言封装函数
R语言封装函数 原帖见豆瓣:https://www.douban.com/note/279077707/ 一个完整的R函数,需要包括函数名称,函数声明,函数参数以及函数体几部分. 1. 函数名称,即要 ...
- 本地主机访问远程linux系统服务器上的jupyter notebook
1,机器情况:服务器 centos python环境已经配置好了,在虚拟环境下安装了anaconda 并且在里面安装了jupyter notebook 2,主机是 windows ipytho ...
- Linux系统Docker启动问题Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service"
在Liunx中使用Docker, 注: Liunx使用的是在虚拟机下的centOS7版本在刚开始安装Docker时没有任何错误, 但是在后续的docker启动过程中, 出现以下问题: [root@zk ...
- windows openssh安装
下载地址:https://github.com/PowerShell/Win32-OpenSSH/releases 解压好后打开目录,执行以下命令: powershell.exe -Execution ...
- MainRun
package Testlink; import java.io.IOException; public class MainRun { public static void main(String[ ...