c++基础复习(5)---STL

STL简介

STL(Standard Template Library),即标准模板库。该库提供了常用的数据结构和算法。

STL三种基本组件:

1、容器(container):容器是容纳、包含一组元素的对象。容器类库包括7种基本容器:向量(vector)、双端队列(deque)、列表(list)、集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)。其中容器又分为顺序容器和关联容器。

顺序容器:将一组具有相同的类型的元素已严格的线性形式组织起来(vector、deque、list)。

关联容器:具有根据一组索引来快速提取元素的能力(set、,multiset、map、multimsp)。

2、迭代器(iterator):迭代器提供了访问容器种每个元素的方法。对迭代器可以使用“++”运算符来获得指向下一个元素的迭代器,可以使用“*”运算符访问一个迭代器所指向的元素,也可以使用“->”运算符来访问类的成员,和指针的使用比较相似。指针本身也是一种迭代器,迭代器就是泛化的指针。

3、算法(algorithm):STL种包含七十多种算法,包括查找、排序、消除、计数等算法。

除了上面的三种基本组件,STL还包括三个辅助组件。

1、函数对象(function object):函数对象又被称为“仿函数”,可以像使用函数那样去使用它。重载“()”运算符的类的对象都可以被当作函数对象使用,函数对象是泛化的函数。用来扩展算法。

2、适配器(adapter):用于为已有对象提供新的接口的对象,适配器本身一般并不提供新的功能,只是为了改变对象的接口而存在的。

3、分配器(allocator):负责空间的配置和管理。

一、STL容器的底层实现和适用的场景

(1)vector(向量)

vector的底层实现为数组,和一般的数组不同的是,一般的数组在需要在定义时指定的数组的大小,一旦定义好之后数组的大小就不能发生改变,所以需要程序员在定义时,就确定数组的大小。而vector的大小是动态的,即可以在适用时自动扩容(只能增大,不能减少,即一旦容量升上去,就不会在降下来。)因为vector的底层是数组实现的,所以要求内存必须连续。但内存分配时,并不是在原数组的地址上向后扩容,而是会重新选择一块空间,将原来的数组拷贝过来(原因是:该数组原来地址的后面不一定有足够的空间)。所以vector在扩容上就比较浪费时间,所以在使用vector时,最好先指定数组的大小,以避免频繁的扩容浪费时间。在删除容器中元素时,和普通的数组的处理方式相同,将从该元素后面的元素全部向前移动一个位置,所以如果vector的元素类型时类时,就会导致对象的构造和析构,就会浪费大量的时间。所以当需要使用vector存储类时,一般建议使用对象的指针。

总结:适用于数据的频繁数据随机访问,且不需要频繁的插入和删除。

(2)list(链表)

list的底层实现为双向链表,和普通的链表相同,即插入和删除的时间复杂度都是O(1),但不支持数据元素的随机访问。

总结:适用于数据的频繁插入和删除,且不需要频繁的随机访问。

(3)deque(双向队列)

deque是一种双向开口(先入先出,即:头删尾插)的连续性空间。所谓的连续性,不过时让用户感觉为连续的,实际上是不连续的。duque的底层采用了“中央控制器”和缓冲区的结合方式,对外造成了整体连续的假象。“中央控制器”实际上就是使用了map。map占用一小段连续的空间,其中每个元素都是一个指针,用来记录每个缓冲区的地址,而且缓冲区的大小是固定的,默认为512b。所以每一块中存储的元素个数是相同的。

deque支持[]运算符,支持数据随机访问,在队尾插入和队头删除的效率都是O(1)。

总结:适用于需要随机存取,而且两端的数据需要插入和删除。

(4)set(集合)和 multiset(多重集合)

set的底层的数据结构是采用insert_unique方式插入红黑树(自平衡二叉查找树)

set总结:适用于去除数据中重复的元素的情况。

multiset的底层数据结构是采用insert_equal方式插入的红黑树。

(5)map(映射)和multimap(多重映射)

map的底层数据结构和set相同使用的是insert_unique方式插入红黑树,和set不同map的元素是键值对,键值对由键值(key)和实值(value)构成的,set的元素的只有一个实值。

map的用法很多:容器内元素频繁的查找,且数据的下标不一定是整形的情况。

这里需要注意的map和set内部的采用的结构是红黑树,所以会对加入容器的数据自动排序。

(6)hash_set(哈希集合)和hash_multiset(哈希多重集合)

hash_set和hash_multiset的底层实现是哈希表,由于哈希表是通过散列函数对数据进行分配空间的,所以在数据的查找上的速度为常数级。它和set和multiset的区别是它对存入容器的数据不排序。

(7)hash_map(哈希映射)和hash_multimap(哈希多重映射)

hash_map和hash_multimap的底层实现是哈希表,它和map和multimap的区别也是对容器中的数据不排序。

这里需要注意一点:现在的c++11规定无序容器的同一命名规则为unordered_xx,所以hash_set,hash_multimap,hash_map和hash_multimap在现在c++11中的名称为:unordered_set,unordered_multiset,unordered_map和unordered_multimap。

二、适配器(adapter

适配器的底层并没有具体数据结构实现,而是对其他STL的组件进行的一定的修改和封装。适配器的优点就在于可以让程序员选择最合适的容器来使用。

适配器分为三类:1、容器适配器 2、迭代器适配器 3、仿函数适配器

1、容器适配器

(1)stack(栈,默认基于deque(双向队列)实现)

(2)queue(队,默认基于deque(双向队列)实现)

(3)priority_queue(优先队列,默认基于vector(向量)实现)

三、分配器(allocaotr)

负责空间配置与管理。从实现的角度来看,配置器是一个实现动态空间配置、空间管理、空间释放的模板类。

STL分配器将内存管理中的分配和释放分开,其中内存分配交给alloc::allocate()负责,内存释放由alloc::dealloccate()负责;对像的构造由::construct()负责,对象的析构::destroy()负责。

同时为了提高内存管理的效率,减少申请内存造成的内存碎片问题,STL采用了两级配置器,当分配的内存大于等于128kb时,会使用第一级的空间配置器,在第一级空间适配器中内存是利用malloc()、realloc(),free()函数进行分配和释放的。当空间小于128k时,采用第二级空间配置器,第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

四、迭代器失效

迭代器失效有两种表现形式:

(1)无法通过迭代器++,--,操作遍历整个容器(第一层失效)。

(2)无法通过迭代器存取迭代器所指向的内存(第二层失效)。

因为迭代器适合容器中的元素绑定的,而是和内存绑定的,所以在删除容器中元素时,几乎都会导致迭代器失效,需要重新定位的情况(第一层失效),而对于部分容器来说,插入操作可能也会导致迭代器失效(第二层失效)。

(注意:在使用的erase删除容器的元素时,erase函数会返回下一个有效的迭代器)

迭代器是失效对于不同的容器有不同的表现形式。

(1)vector、deque

对于这两种顺序容器来说,在删除元素时会导致当前未知之后的迭代器都会失效。vector和deque中的元素在删除之后,当前位置之后的元素的位置会向前移动(第一层失效)。而对于插入操作来说,由于vector、deque的存在空间时连续的,所以在插入元素时可能会导致内存重新分配的问题(第二层失效),如果内存没有重新分配,迭代器就不会失效。

(2)list

list的空间时动态分配的,空间本身就不连续,所以插入一般不会产生迭代器失效问题,在删除时会导致第一层迭代器失效问题。

(3)map、set

删除map和set的元素,会导致当前的迭代器失效,但不会影响下一个元素的迭代器。插入也不会导致迭代器失效。

五、map和set又什么区别,分别是如何实现的?

map和set都是c++的关联容器,其底层都是红黑树(RB-Tree)。由于map和set所开放的各种操作接口,红黑数也都提供了,索引几乎所有的map和set的操作行为,都只是转调红黑数的操作行为。

map和set的区别在于:

(1)map中的元素使key-value(关键字-值)对,关键字起到索引的作用,值则表示与索引相关联的数据;set与之相对的就是关键字的简单集合,set中每个元素只包含一个关键字。

(2)set的迭代器使const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字来保证其有序性,如果允许修改key值的,就会破坏底层红黑树的结构,而这样就会导致迭代器失效,所以STL不允许修改关键字,而map的数据是k-v键值对,是通过k值来索引v的值,所有v的修改,并不会影响到底层红黑树的结构,所以STL允许修改map的value值

(3)map支持下标操作,set不支持下表操作。map可以用key做下标,map的下标运算符[]将关键字作为下标去查找。(注意:如果map索引的下标并不存在时,map并不会报错,而是会在自动添加该关键字,并为该关键字对应的value赋予一个默认的值)

六、vector和list的区别?

(1)vector的底层实现是数组;而list的底层实现是双向链表。

(2)vector的内存的空间时连续的;而list的内存空间时不连续的。

(3)vector支持数据的随机访问;而list不支持数据的随机访问。

(4)vector在插入时如果空间不够才会自动申请新的空间的扩容,但容器中数据删除时不会释放空间;而list在每次插入和删除都会申请和释放空间。

(5)vector在数据的插入或删除都可能会导致迭代器失效(插入或删除位置后面额迭代器全部失效);而list的在插入时不会导致迭代器失效,而在删除时会导致当前迭代器失效。

(6)vector在删除和插入元素时会导致内存拷贝(插入位置之后的元素需要全部后移或删除位置的元素需要全部前移);而list不会。

(7)vector适用于对容器中元素进行频繁的访问的情况;而list适用于对容器中元素进行频繁插入和删除的情况。

七、vector和deque的区别?

(1)vector的底层实现时数组;而deque的底层实现时“中央控制区”和缓冲的结构。

(2)vector的内存空间时连续的;而deque的内存空间是一种假的连续,实际上是不连续的。

(3)vecotr的支持数组的随机访问;而deque虽然也是支持使用[]的运算符,但实际上是需要通过中央控制器二次寻址的,效率比vector略低。

(4)vector在空间不够重新申请新空间策略是先申请一块比原来空间大的空间(1.5倍或两倍)将原来的数据拷贝一份到新的空间中,然后将原来的空间的释放;而deque的策略是申请一块固定大小(默认大小:512bytes)的空间,然后将该空间的地址存放在中央控制器中,并没有数据拷贝和空间的释放的过程,所以扩容时deque的效率要高于vector

(5)vector在对尾部进行插入和删除的效率时O(1);而deque在尾部和头部的插入和删除的效率都是O(1)。

(6)vector适用于对容器中元素进行频繁的访问的情况;而deque的适用于元素的频繁访问,并且在数据需要在头部和尾部进行插入和删除的情况。

八、容器容量和容器大小(capacity和size)

(1)容器容量:容器所能容纳元素的个数,通常大于容器的实际存储元素的个数。

s.capacity();//查看容器容量
s.reserve(len);//扩展当前容器的容量,如果len大于当前容器容量,那么将会将容器的容量扩展为len,且不会对新扩展的空间进行初始化;当len小于等于当前容器容量时,什么也不做。

(2)容器大小:容器中当前存在元素的个数。

s.size();//查看当前容器的中元素的个数
s.resize(len);//设置当前容器的容量,如个len大于当前容器中元素的个数,新增的元素将并初始化为0。len小于容器的实际大小时,会将len之后的元素的全部清除为0。

通过情况向容器的容量是大于等于容器的大小的,就像杯子和杯子中的水的关系。一般需要容器的大小不会引起内存的重新分配(向杯子中加水),但修改容器的容量就会导致容器内存的重新分配(原来的杯子太小了,需要换新的杯子)。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
	vector<int> intArr;
	intArr.push_back(1);
	intArr.push_back(2);
	intArr.push_back(3);
	intArr.push_back(4);
	intArr.push_back(5);
	intArr.push_back(6);
	cout << "1、修改前:容器size:" << intArr.size() << ",容器capacity:" << intArr.capacity() << endl;
	vector<int>::iterator viter = intArr.begin();
	for (; viter != intArr.end(); ++viter)
	{
		cout << *viter << ",";
	}
	cout << endl;
	intArr.resize(3);
	intArr.reserve(3);
	cout << "2、修改后、容器size:" << intArr.size() << ",容器capacity:" << intArr.capacity() << endl;
	viter = intArr.begin();
	for (; viter != intArr.end();++viter)
	{
		cout << *viter << ",";
	}
	cout << endl;
	intArr.resize(6);
	intArr.reserve(10);
	cout << "3、修改后、容器size:" << intArr.size() << ",容器capacity:" << intArr.capacity() << endl;
	viter = intArr.begin();
	for (; viter != intArr.end(); ++viter)
	{
		cout << *viter << ",";
	}
	cout << endl;
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值