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

目录

一、STL容器动态链接可能产生的问题

二、map和unordered_map的区别

三、vector和list的区别以及适用场景

四、vector的实现原理

五、vector和list中,删除末尾元素时指针和迭代器如何变化?若删除中间元素呢?

六、map和set有何区别,如何实现?

七、push_back和emplace_back的区别

八、vector和list的具体实现方式,常规操作的时间复杂度是多少?


一、STL容器动态链接可能产生的问题

        1、问题

        容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库哈数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。

        2、原因

        容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏的问题。

二、map和unordered_map的区别

        map和unordered_map的区别在于他们的实现机理不同。

        1、map实现机理

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

        2、unordered_map实现机理

        unordered_map内部实现了一个哈希表(也称散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。

三、vector和list的区别以及适用场景

        vector和list的区别在于底层实现机理不同,因而特性和适用场景也不同。

        1、vector(一维数组)

        元素在内存连续存放,动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后内存也不会释放。

        优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高,时间复杂度O(1)。

        缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动,比较麻烦,时间复杂度O(n),另外当空间不足时还需要进行扩容。

        应用场景:拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随机访问而不在乎插入和删除的效率,可以使用vector。

        2、list(双向链表)

        元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。

        优点:底层实现是循环双链表,当对大量数据进行插入删除时,时间复杂度是O(1)。

        缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历,时间复杂度O(n),没有提供 [ ] 操作符的重载。

        应用场景:拥有一段不连续的内存空间,如果需要高效率的插入和删除,而不关心随机访问,则应该使用list。

四、vector的实现原理

       底层实现是一维数组(元素连续存放)。

        1、新增元素

        vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素。插入新的数据分为:在最后插入push_back和通过迭代器在任何位置插入。这里说一下通过迭代器插入,通过迭代器与第一个元素的距离知道要插入的位置,即int index=iter-begin()。这个元素后面的所有元素都向后移动一个位置,在空出来的位置上存入新增的元素。

//新增元素
void insert(const_iterator iter, const T& t)
{
    int index=iter-begin();
    if(index<size_)
    {
        if(size_==capacity_)
        {
            int capa=calculateCapacity();
            newCapacity(capa);
        }
        memmove(buf+index+1,buf+index,(size_-index)*sizeof(T));
        buf[index]=t;
        size_++;
        }
    }
}

        2、删除元素

        删除和新增相似,也分两种:删除最后一个元素pop_back和通过迭代器删除任意一个元素erase(iter)。通过迭代器删除还是先找到要删除元素的位置,即int index=iter-begin()。这个位置后面的每个元素都向前移动一个元素的位置。同时,我们知道erase不释放内存,只初始化成默认值。删除全部元素clear是循环调用了erase,所以删除全部元素的时候,不释放内存。内存是在析构函数中释放的。

//删除元素
iterator erase(const_iterator iter)
{
    int index=iter-begin();
    if(index < size_ && size_>0)
    {
        memmove(buf+index,buf+index+1,(size_-index)*sizeof(T));
        buf[--size_]=T();
    }
    return iterator(iter);
}

        3、迭代器

        迭代器iterator是STL的一个重要组成部分,通过iterator可以很方便的存储集合中的元素。STL为每个集合都写了一个迭代器,迭代器其实是对一个指针的包装,实现一些常用的方法,如++、--、!=、==、*、->等。通过这些方法可以找到当前元素或是别的元素。vector是STL集合中比较特殊的一个,因为vector中的每个元素都是连续的,所以在自己实现vector的时候可以用指针代替。

//迭代器的实现
template<class _Category,
    class _Ty,
    class _Diff=ptrdiff_t,
    class _Pointer=_Ty *,
    calss _Reference = _Ty& >
    struct iterator
    {
        //base type for all iterator classes
        typedef _Category iterator_category;
        typedef _Ty value_type;
        typedef _Diff difference_type;
        typedef _Diff distance_type;
        typedef _Pointer pointer;
        typedef _Reference reference;
    };

五、vector和list中,删除末尾元素时指针和迭代器如何变化?若删除中间元素呢?

        1、迭代器和指针的区别

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

        2、vector增删元素

        删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。

        3、list增删元

        删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。

六、map和set有何区别,如何实现?

        1、set是一种关联式容器,特性如下:

        (1)set以RBTree作为底层容器;

        (2)所得元素的只有key没有value,value就是key;

        (3)不允许出现键值重复;

        (4)所有的元素都会被自动排序;

        (5)不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的。

        2、map和set一样是关联式容器,特性如下:

        (1)map以RBTree作为底层容器;

        (2)所有元素都是键+值存在;

        (3)不允许键重复;

        (4)所有元素是通过键进行自动排序的;

        (5)map的键是不能修改的,但是其键对应的值是可以修改的。

综上所述,map和set底层实现都是红黑树。map和set的区别在于map的值不作为键,键和值是分开的。

七、push_back和emplace_back的区别

        如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。

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

class A {
public:
    A(int i){
        str = to_string(i);
        cout << "构造函数" << endl;
    }
    ~A(){}
    A(const A& other): str(other.str){
        cout << "拷贝构造" << endl;
    }

public:
    string str;
};


int main()
{
    vector<A> vec;
    vec.reserve(10);
    for(int i=0;i<10;i++){
        vec.push_back(A(i)); //调用了10次构造函数和10次拷贝构造函数,
        // vec.emplace_back(i); //调用了10次构造函数一次拷贝构造函数都没有调用过
}

八、vector和list的具体实现方式,常规操作的时间复杂度是多少?

        1. vector 一维数组(元素在内存连续存放)是动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放; 如果新增大小当前大小时才会重新分配内存。

        扩容方式:

        a. 倍放开辟三倍的内存

        b. 旧的数据开辟到新的内存

        c. 释放旧的内存

        d. 指向新内存

        2. list 双向链表(元素存放在堆中) 元素存放在堆中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点,使得它的随机存取变得非常没有效率,因此它没有提供[ ]操作符的重载。但是由于链表的特点,它可以很有效的支持任意地方的删除和插入操作。

        特点:

        a. 随机访问不方便

        b. 删除插入操作方便

        3. 常见时间复杂度

        (1)vector插入、查找、删除时间复杂度分别为:O(n)、O(1)、O(n);

        (2)list插入、查找、删除时间复杂度分别为:O(1)、O(n)、O(1)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猿何试Bug个踌

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

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

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

打赏作者

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

抵扣说明:

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

余额充值