第四章:C++ 之 STL(1)

目录

一、STL的基本组成部分

二、STL中常见的容器及实现原理

三、STL 中 map、hashtable、deque、list的实现原理

四、STL 的空间配置器(allocator)

五、STL 容器的查找时间复杂度是多少?

六、迭代器什么情况下会失效?

七、STL中迭代器的作用,有指针为何还要迭代器?

八、容器上迭代器分类

九、STL中resize和reserve的区别


一、STL的基本组成部分

        标准模板库(Standard Template Library,STL)是一些常用数据结构和算法的模板的集合。

        广义来讲,STL分为3类:Algorithm(算法)Container(容器)Iterator(迭代器)。容器和算法通过迭代器可以进行无缝连接。

        详细来说,STL由6部分组成:容器(Container)、算法(Algorithm)、迭代器(Iterator)、仿函数(Function object)、适配器(Adaptor)、空间配置器(Allocator)。

        1、容器。是一种数据结构,如list、vector、deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。

        2、算法。是用来操作容器中的数据的模板函数。例如,STL用sort()来对一个vector中的数据进行排序,用find()来搜索一个list中的对象,函数本身与他们操作的数据的结构和类型无关,因此他们可以用于从简单数组到高度复杂容器的任何数据结构上。

        3、迭代器。提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象,迭代器就如同一个指针。事实上,C++的指针也是一种迭代器。但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象。

        4、仿函数。又称为函数对象。其实就是重载了操作符的struct,无特别之处。

        5、适配器。简单地说是一种接口类,专门用来修改现有类的接口,提供一种新的接口;或调用现有的函数来实现所需的功能。主要包括3种:container adaptor、iterator adaptor、function adaptor。

        6、空间配置器。为STL提供空间配置的系统。其中主要工作包括两部分:

        ① 对象的创建与销毁;② 内存的获取与释放

二、STL中常见的容器及实现原理

        容器可以用于存放各种类型的数据(基本类型的变量、对象等)的数据结构,都是模板类,分为顺序容器关联式容器容器适配器三种类型。特性分别如下:

        1、顺序容器。容器并非排序的,元素的插入位置同元素的值无关。包含vector、deque、list,具体实现原理如下:

        (1)vector头文件

        动态数组。元素在内存连续存放随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。

        (2)deque头文件

        双向队列。元素在内存连续存放随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。

        (3)list头文件

        双向链表。元素在内存中不连续存放。在任何位置增删元素都能在常数时间完成不支持随机存取

        2、关联式容器。元素是排序的,插入任何元素,都按相应的排序规则来确定其位置。在查找时具有非常好的性能,通常以平衡二叉树的方式实现。包含setmultisetmapmultimap,具体实现原理如下:

        (1)set/multiset头文件

        set即集合。set中不允许相同元素,multiset中允许存在相同元素。

        (2)map/multimap头文件

        map与set的不同在于map中存放的元素有且仅有两个成员,一个名为first,另一个名为second,map根据first值对元素从小到大排序,并可快速地根据first来检索元素。

注意:map与multimap的不同在于是否允许相同first值的元素。

        3、容器适配器。封装了一些基本的容器,使之具有新的函数功能。比如把deque封装一下变为一个具有stack功能的数据结构。新得到的数据结构就叫适配器。包含stack、queue、priority_queue,具体实现原理如下:

        (1)stack头文件

        栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最近插入序列的项(栈顶的项)。即后进先出

        (2)queue头文件

        队列。插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出

        (3)priority_queue头文件

        优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列

三、STL 中 map、hashtable、deque、list的实现原理

         maphashtabledequelist实现机理分别是红黑树函数映射双向队列双向链表。特性为:

        1、map实现原理

        map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可以将键值按照从小到大遍历出来

        2、hashtable(也称散列表,哈希表)实现原理

        hashtable采用了函数映射的思想将记录的存储位置与记录的关键字关联起来,从而能够很快的进行查找。这决定了哈希表特殊的数据结构,它与数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来查找。

        3、deque实现原理

        deque内部实现的是一个双向队列元素在内存连续存放,随机存取任何元素都在常数时间完成(仅次于vector)。所有适用于vector的操作都适用于deque,在两端增删元素具有较佳的性能。

        4、list实现原理

        list内部实现的是一个双向链表元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。无成员函数,给定一个下标i,访问第i个元素的内容,只能从头挨个遍历到第i个元素。

四、STL 的空间配置器(allocator)

         一般情况下,一个程序包括数据结构和算法,而数据结构作为存储数据的组织形式,与内存空间有着密切的联系。在C++ STL中,空间配置器便是用来实现内存空间(一般是内存,也可以是硬盘等空间)分配的工具,与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。

1、两种C++类对象实例化方式的异同

        在C++ 中,创建类对象一般分为两种方式:一种是直接利用构造函数,直接构造类对象。如Test test();另一种是通过new来实例化一个类对象,如Test *pTest = new Test;这两种方式的异同点是:

        (1)静态存储区分配:内存在程序编译的时候已经分配好,这块内存在程序的整个运行空间内都存在。如全局变量、静态变量等。

        (2)栈空间分配:程序在运行期间,函数内的局部变量通过栈空间来分配存储(函数调用栈),当函数执行完毕返回时,相对应的栈空间被立即回收。主要是局部变量

        (3)堆空间分配:程序在运行期间,通过在堆空间上为数据分配存储空间,通过malloc和new创建的对象都是从堆空间分配内存,这类空间需要程序员自己来管理,必须通过free()或者是delete()函数对堆空间进行释放,否则会造成内存溢出。

        从内存空间分配的角度来对这两种方式区分:

        (1)对于第一种方式来说,是直接通过调用Test类的构造函数来实例化Test类对象的,如果该实例化对象是一个局部变量,则其是在栈空间分配相应的存储空间。

        (2)对于第二种方式来说,则比较复杂。new一个类对象,其实是执行了两步操作:首先,调用new在堆空间分配内存,然后调用类的构造函数构造对象的内容。同样,使用delete释放时,也是经历了两步:首先调用类的析构函数释放类对象,然后调用delete释放堆空间。

2、C++ STL空间配置器实现

        很容易想象,为了实现空间配置器,完全可以利用new和delete函数并对其进行封装实现STL的空间配置器,的确可以这样。但是,为了最大化提升效率,SGI STL版本并没有简单的这样做,而是采取了一定的措施,实现了更加高效复杂的空间分配策略。由于以上的构造都分为两部分,所以在SGI STL中将对象的构造切分开来,分成空间配置和对象构造两部分。

        内存配置操作:通过alloc:allocate()实现。

        内存释放操作:通过alloc:deallocate()实现。

        对象构造操作:通过::construct()实现。

        对象释放操作:通过::destroy()实现。

        关于内存空间的配置与释放,SGI STL 采用了两级配置器:一级配置器主要是考虑大块 内存空间,利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一起块,内存块一旦被使用,则被从链表中剔除,易于维护。

五、STL 容器的查找时间复杂度是多少?

        STL中常用的容器有vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set等。

        1、vector

        采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:

        插入:O(N)            查看:O(1)            删除:O(N)

        2、deque

        采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:

        插入:O(N)            查看:O(1)            删除:O(N)

        3、list

        采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:

        插入:O(1)            查看:O(N)            删除:O(1)

        4、map、set、multimap、multiset

        上述四种容器采用红黑树实现,不同操作的时间复杂度为:

         插入:O(logN)            查看:O(logN)            删除:O(logN)

        5、unordered_map、unordered_set、unordered_multimap、unordered_multiset

        上述四种容器采用哈希表实现,不同操作的时间复杂度为:

        插入:O(1),最坏情况O(N)      查看:O(1) ,最坏情况O(N)     删除:O(1),最坏情况O(N) 

注意:容器的时间复杂度 取决于其底层实现方式。

六、迭代器什么情况下会失效?

        1、对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器

        2、对于关联容器map,set来说,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。

        3、对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。

七、STL中迭代器的作用,有指针为何还要迭代器?

        1、迭代器的作用

        (1)用于指向顺序容器和关联容器中的元素;

        (2)通过迭代器可以读取它指向的元素;

        (3)通过非const迭代器还可以修改其指向的元素。

        2、迭代器和指针的区别

        迭代器不是指针,是类模板,表现的像指针。它只是模拟了指针的一些功能,重载了指针的一些操作符,-->、++、--等。迭代器封装了指针,是一个“可遍历STL容器内全部或部分元素”的对象,本质封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针。它可以根据不同类型的数据结构来实现不同的++、--等操作。

        迭代器返回的是对象引用而不是对象的值。所以cout只能输出迭代器使用取值后的值而不是直接输出其自身。

        3、迭代器产生的原因

        Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果

        Iterator(迭代器)模式又称游标(Cursor)模式,用于提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、 vector、stack等容器类及ostream_iterator等扩展Iterator。

        示例:

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> v; //一个存放int元素的数组,一开始里面没有元素
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    vector<int>::const_iterator i; //常量迭代器
    for (i = v.begin(); i != v.end(); ++i) //v.begin()表示v第一个元素迭代器指针,++i
指向下一个元素
        cout << *i << ","; //*i表示迭代器指向的元素
    cout << endl;

    vector<int>::reverse_iterator r; //反向迭代器
    for (r = v.rbegin(); r != v.rend(); r++)
        cout << *r << ",";
    cout << endl;
    vector<int>::iterator j; //非常量迭代器
    for (j = v.begin();j != v.end();j++)
        *j = 100;
    for (i = v.begin();i != v.end();i++)
        cout << *i << ",";
    return 0;
}


/* 运行结果:
    1,2,3,4,
    4,3,2,1,
    100,100,100,100,
*/

八、容器上迭代器分类

容器容器上的迭代器类别
vector随机访问
deque随机访问
list双向
set/multiset双向
map/multimap双向
stack不支持迭代器
queue不支持迭代器
priority_queue不支持迭代器

九、STL中resize和reserve的区别

1、首先明白两个概念:

        ① capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。

        ② size:此时容器中实际的元素个数。可以通过下标访问0~(size-1)范围内的对象。

2、resize和reserve区别

        resize 和 reserve 既有差别,也有共同点。两个接口的共同点它们都保证了vector的空间大小 (capacity)最少达到它的参数所指定的大小

        (1)resize既分配了空间,也创建了对象。reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象

        (2)resize既修改了capacity大小,也修改了size大小。reserve只修改capacity大小,不修改size大小

        (3)两者的形参个数不一样。resize带两个参数,一个表示容器大小,一个表示初始值(默认为0);reserve只带一个参数,表示容器的预留大小。

        为实现resize的语义,resize接口做了两个保证:

        (1)保证区间[0, new_size)范围内数据有效,如果下标index在此区间内,vector[indext]是合法的;

        (2)保证区间[0, new_size)范围以外数据无效,如果下标index在区间外,vector[indext]是非法的。

        reserve只是保证vector的空间大小(capacity)最少达到它的参数所指定的大小n。在区间[0, n)范围内,如果下标是index,vector[index]这种访问有可能是合法的,也有可能是非法的,视具体情况而定。

        以下是两个接口的源代码:

void resize(size_type new_size)
{
    resize(new_size,T());
}
void resize(size_type new_size, const T& x)
{
    if (new_size < size())
        erase(begin() + new_size, end()); // erase区间范围以外的数据,确保区间以
外的数据无效
    else
        insert(end(), new_size - size(), x); // 填补区间范围内空缺的数据,确保区
间内的数据有效
}


#include<iostream>
#include<vector>
using namespace std;

int main()
{
    vector<int> a;
    cout<<"initial capacity:"<<a.capacity()<<endl;
    cout<<"initial size:"<<a.size()<<endl;
    
    /*resize改变capacity和size*/
    a.resize(20);
    cout<<"resize capacity:"<<a.capacity()<<endl;
    cout<<"resize size:"<<a.size()<<endl;

    vector<int> b;
    /*reserve改变capacity,不改变resize*/
    b.reserve(100);
    cout<<"reserve capacity:"<<b.capacity()<<endl;
    cout<<"reserve size:"<<b.size()<<endl;

    return 0;
}


/* 运行结果:
    initial capacity:0
    initial size:0
    resize capacity:20
    resize size:20
    reserve capacity:100
    reserve size:0
*/

注意:如果n大于当前的vector的容量(是容量,并非vector的size),将会引起自动内存分配。所以现有的pointer,references,iterators将会失效,而内存的重新配置会很耗时间。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿何试Bug个踌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值