几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。STL容器就是将运用最广泛的一些数据结构实现出来。
常用的数据结构:数组(array) , 链表(list), tree(树),栈(stack), 队列(queue), 集合(set),映射表(map) , 根据数据在容器中的排列特性,这些数据分为序列式容器 和关联式容器 两种。
序列式容器 强调值的排序,序列式容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。Vector容器、Deque容器、List容器等。
关联式容器 是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。Set/multiset容器 Map/multimap容器
算法,问题的解法,以有限 的步骤,解决逻辑或数学上的问题。我们所编写的每个程序都是一个算法,其中的每个函数也都是一个算法,毕竟它们都是用来解决或大或小的逻辑问题或数学问题。STL收录的算法经过了数学上的效能分析与证明,是极具复用价值的,包括常用的排序,查找等等。特定的算法往往搭配特定的数据结构,算法与数据结构相辅相成。
算法分为:质变算法和非质变算法 。
质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等
非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等
迭代器(iterator)是一种抽象的设计概念,现实程序语言中并没有直接对应于这个概念的实物。 在Design Patterns一书中提供了23种设计模式的完整描述, 其中iterator模式定义如下:提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
迭代器的设计思维-STL的关键所在,STL的中心思想在于将容器(container)和算法(algorithms)分开,彼此独立设计,最后再一贴胶着剂将他们撮合在一起。
从技术角度来看,容器和算法的泛型化并不困难,c++的class template和function template可分别达到目标,如果设计出两这个之间的良好的胶着剂,才是大难题。
C风格字符串char *(以空字符结尾的字符数组)太过复杂难于掌握,不适合大程序的开发,所以C++标准库定义了一种string类,定义在头文件。
String和c风格字符串对比:
Array是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。
Vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此vector的运用对于内存的合理利用与运用的灵活性有很大的帮助。
Vector所采用的数据结构非常简单,线性连续空间,它以两个迭代器_Myfirst和_Mylast分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器_Myend指向整块连续内存空间的尾端。为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求大一些,以备将来可能的扩充,这边是容量的概念。换句话说,一个vector的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下次再有新增元素,整个vector容器就得另觅居所。
动态扩展: 动态增加大小并不是在原空间之后续接新空间(因为无法保证之后尚有可供分配的空间),而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间(每次再分配原大小两倍的内存空间)。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。重新分配的空间为原来的两倍 。
Vector维护一个线性空间,所以不论元素的型别如何,普通指针都可以作为vector的迭代器,因为vector迭代器所需要的操作行为,如operaroe*, operator->, operator++, operator–, operator+, operator-, operator+=, operator-=, 普通指针天生具备。Vector支持随机存取,而普通指针正有着这样的能力。所以vector提供的是随机访问迭代器(Random Access Iterators).
Vector容器是单向开口的连续内存空间(vector 容器也可以在头尾两端插入元素,但是在其头部操作效率奇差),deque则是一种双向开口的连续线性空间(头尾两端分别做元素的插入和删除操作),每段数据空间内部是连续的,而每段数据空间之间则不一定连续。
deque内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间。deque采用一块所谓的map(注意,不是STL的map容器)作为主控。这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体
Deque容器和vector容器最大的差异
deque容器实现原理
Deque容器是连续的空间,至少逻辑上看来如此,连续现行空间总是令我们联想到array和vector,array无法成长,vector虽可成长,却只能向尾端成长,而且其成长其实是一个假象,事实上(1) 申请更大空间 (2)原数据复制新空间 (3)释放原空间 三步骤,如果不是vector每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的。
Deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
既然deque是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。Deque代码的实现远比vector或list都多得多。
Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。
deque除了维护一个指向map的指针外,也维护start、finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一位置)。此外,也必须记住目前的map大小,因为一旦map的所提供的空间不足,它将需要重新配置一个更大的空间,依然是经过三个步骤:配置更大的、拷贝原来的、释放原空间。
stack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口,stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。有元素推入栈的操作称为:push,将元素推出stack的操作称为pop.
stack没有迭代器
Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,也不提供迭代器。
底层实现:deque
在STL中栈的的默认容器是双端队列 deque,也可以使用 list 和vector 自定义队列,因为 list 和 vector 都提供了删除最后一个元素的操作(出栈)。
Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。
queue没有迭代器
Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,也不提供迭代器。
底层实现:deque
在STL中队列queue的默认容器是双端队列 deque,也可以使用 list 自定义队列,但是vector不行,因为vector不能提供删除第一个元素这个操作。
⭐priority_queue优先队列
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先出队的行为特征。
优先队列实现了类似堆的功能(其实底层就是用堆实现的)。
STL默认使用 <操作符来确定对象之间的优先级关系(也就是从大到小排序,默认大根堆)
优先队列的底层是用堆实现的。 在优先队列中默认存放数据的容器是vector,在声明时也可以用deque(双向队列)
没有迭代器,不提供遍历功能
链表是一种物理存储单元上非连续、非顺序 的存储结构,链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。相较于vector的连续线性空间,list就显得负责许多,它的好处是每次插入或者删除一个元素,就是配置或者释放一个元素的空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素的移除,list永远是常数时间。
链表较长(单元数量很多)又无法提前知道数量,只能一个个 push_back,这时如果用 vector 效率受影响,因为 vector 增长过程中会不时的重新分配内存,每重新分配一次就要把 vector 中已有的数据都 copy 一遍。
链表中按值保存一个自定义结构,结构中有资源指针(不是智能指针),虽然结构的析构函数会释放资源,但这时只能用 list,如果用 vector,出现 vector 重新分配内存后就会出错。如果实在想用 vector,可以给自定义结构写拷贝构造函数或在 vector 里保存结构的智能指针。
链表中间位置频繁插入删除元素,用 list。
List容器的迭代器
List容器不能像vector一样以普通指针作为迭代器,因为其节点不能保证在同一块连续的内存空间上。
List迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取操作。所谓”list正确的递增,递减、取值、成员取用”是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员。
由于list是一个双向链表 ,迭代器必须能够具备前移、后移的能力,所以list容器提供的是Bidirectional Iterators.
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效,甚至List元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受任何影响。
Set的所有元素都会根据元素的键值自动被排序。Set的元素不像map那样可以同时拥有实值和键值,set的元素即是键值又是实值。Set不允许两个元素有相同的键值。
⭐为什么底层不用hash:
首先set,不像map那样是key-value对,它的key与value是相同的。关于set有两种说法,第一个是STL中的set,用的是红黑树;
第二个是hash_set,底层用得是hash table。红黑树与hash table最大的不同是,红黑树是有序结构,而hash table不是。但不是说set就不能用hash,如果只是判断set中的元素是否存在,那么hash显然更合适,因为set 的访问操作时间复杂度是log(N)的,而使用hash底层实现的hash_set是近似O(1)的。
然而,set应该更加被强调理解为“集合”,而集合所涉及的操作并、交、差等,即STL提供的如交集set_intersection()、并集set_union()、差集set_difference()和对称差集set_symmetric_difference(),都需要进行大量的比较工作,那么使用底层是有序结构的红黑树就十分恰当了,这也是其相对hash结构的优势所在。
multiset容器基本概念
hash_set容器基本概念
Map器实现原理
Multimap器实现原理
hashtable器实现原理
hash_map器实现原理
unordered_map器实现原理
⭐map、hash_map、unordered_map比较
⭐必须了解:
capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。
reserve:
resize:
resize既分配了空间,也创建了对象,可以通过下标访问。当resize的大小
resize是改变容器的大小,并且创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。
再者,两个函数的形式是有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。
如果n比当前的vector元素数目要小,vector的容量要缩减到resize的第一个参数大小,既n。并移除那些超出n的元素同时销毁他们。
如果n比当前vector元素数目要大,在vector的末尾扩展需要的元素数目,如果第二个参数val指定了,扩展的新元素初始化为val的副本,否则按类型默认初始化。
注意: 如果n大于当前的vector的容量(是容量,并非vector的size),将会引起自动内存分配。所以现有的pointer,references,iterators将会失效。而内存的重新配置会很耗时间。
因此为了避免内存的重新配置,可以用reserve预留出足够的空间或者利用构造函数构造足够的空间。
⭐lower_bound()和upper_bound()都是c++ 标准库中的函数。
二者都利用二分查找的方法查找已排序的数组中的元素。
它们的返回值都是一个地址。
lower_bound()用于在已排好序的数组中找出大于等于目标元素的下标最小的元素的地址。
upper_bound()用于在已排好序的数组中找出大于目标元素的下标最小的元素的地址。
template <class ForwardIterator, class T, class Compare>
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,const T& val, Compare comp);
⭐Vector在堆中分配了一段连续的内存空间来存放元素。
vector<int>v;
v.push_back(1);
std::vector<int>::iterator iter1 = v.begin();
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
若声明时未设定好vector的大小,每来一个元素,就意味着会重新开辟空间,拷贝元素,这样不仅仅时间复杂度上很高。而且在重新开辟空间之后,原来的指向第一个元素的迭代器,也就是那个指针无法使用了。
这时reserve函数的作用就体现出来了,如果一开始预留的空间足够大,也就是capacity足够使用,那么在这个过程中,不用重新开辟空间,这样原来的迭代器还可以使用,这就是reserve的作用。
解决方案:
在创建容器后,第一时间为容器分配足够大的空间,避免重新分配内存。
std::vector<int> v;//create an empty vector
v.reserve(80);// reserve memory for 80 elements
这样在push_back的过程中,在不超过capacity之前,都不需要重新开辟空间,迭代器都不会失效。
同时也可以使用resize的方法为接下来的数据预留空间,只不过使用resize会使得capacity变得比当前的size更大。
下面说一下关于reserve和resize之间的区别:
在前面如果说使用reserve(10)
将容器的大小设置成10
那么之后每次push_back的过程中,在数量小于10的情况下,不会去重新开辟内存空间,然后进行数据的拷贝等等操作。
减少内存拷贝的复杂,和迭代器的失效等问题
如果说设置成resize,此时空间size为10,且capacity为10,并且里面被填充上了0,
即使之后push_back(1),这时因为capacity已经满了,此时还是需要重新开辟内存空间,之前的迭代器失效
那么此时设置resize的目的在哪呢?因为此时再次开辟空间的时候,因为有一个基数10在那,所以在进行空间开辟的时候,之间去另一个内存空间处,开辟出15大小的capacity。这样在接着进行push_back的过程中,就可以减少空间的开辟次数。
⭐在vector中reserve和resize之间的区别:
1.reserve是开辟一段空的空间,push_back的过程中直接往里面插入数据,设置的大小就是capacity的大小
2.resize是开辟一段空间,但是全部填充元素0,push_back是在后面接着添加,但是不可避免的是,第一步就要重新开辟空间,而resize的初始值的意义只是有一个基数在那,使得capacity无论是以1.5还是2倍都会减少内存开辟的次数
class A
{
public:
A(){ cout << "construct" << endl; }
A(const A &a){ cout << "copy construct" << endl; }
~A(){ cout << "destruction" << endl; }
};
int main(void)
{
vector<A> p;
A a;
A b;
p.push_back(a);
p.push_back(b);
}
分析:
首先构造a,b (construct,construct) push_back(a)会调用对象的复制构造函数构造一个对象p(copy construction),在将这个p的放入vector中(push_back的参数是const引用),当需要放b时,容量不够,扩容(分配新的大小使用allocate类,分配的capacity为原来的1.5(我测试是这样,但是源码中好像是两倍)),将容器里的对象复制构造然后放入到新的容器中(copy construction)),将旧容器里的对象删除(destruction),然后调用b的copy struction,最后程序结束后调用a,b和容器里的两个对象的析构函数(4个destruction)
⭐线性容器vector在拷贝构造数据时导致数据量size值扩大,size值扩大到等于capacity时,容器为了使capacity变大就会扩容。reserve函数数值扩容,避免容器自动扩容带来性能消耗,自动扩容导致拷贝构造函数频繁被调用
问题:
1、线性容器以vector为例,
2、每次扩大capacity时,容器就需要重新在堆中申请一块更大的内存,
3、然后将源容器中的值一个个拷贝构造到新容器中,
4、如果扩大的capacity不够,比如一次push_back了100000个数据到vector容器中,vector容器就会不停的扩容,每次扩容都会消耗更多的内存和时间。
5、这会导致大量的内存消耗,时间消耗,十分影响程序性能。
6、为什么要成倍的扩容而不是一次增加一个固定大小的容量呢?
7、为什么是以两倍的方式扩容而不是三倍四倍,或者其他方式呢?
C++中迭代器失效
迭代器iterator就是类似指针,迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。
迭代器失效情况根据数据结构分为三种情况,分别为数组型,链表型,树型数据结构。
⭐针对数组型数据结构
⭐针对链表型数据结构
⭐针对树形数据结构
内部实现机理不同
优缺点以及适用处
map:
优点:
缺点:
适用处:对于那些有顺序要求的问题,用map会更高效一些
unordered_map:
内存占有率的问题就转化成红黑树 VS hash表 , 还是unorder_map占用的内存要高。
但是unordered_map执行效率要比map高很多
对于unordered_map或unordered_set容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的
map和unordered_map的使用