C++面试:STL篇

本文深入探讨了C++标准模板库(STL)的关键组件,包括容器、算法、迭代器、仿函数、适配器和空间配置器等。重点介绍了vector、deque、list等容器的特点与应用场景,以及哈希表和红黑树的底层实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

STL个人小结:

stl是c++的标准模板库,stl6大组件
容器:存储数据,本质是类模板
vector:底层是动态数组,连续内存支持随机存取,尾部增删效率高,内部增删O(n)。适用于随机存取的场景
list:底层是双链表,内存不连续,只能顺序访问,任意位置增删都是O(1)。适用于频繁增删,尤其是在中间。
deque:整体连续,支持随机存取,首尾增删效率高,但是迭代器太复杂,所以一般只有当既要随机存取又要首尾增删采用deque。
unordered_set:无序集合,去重,用于查找元素是否存在。
unordered_map:用于查找元素是否存在及其出现次数。
map:底层红黑树,有序,也是用于查找元素,它的特点就是稳定。
multimap,multiset允许键重复
stack:先进后出。
queue:先进先出。
注意: 实际上即使需要频繁增删,vector也比list快很多。所以vector和list优先vector;

算法:处理数据,本质是函数模板,如find,sort
迭代器:是个模板类,提供了一种通用的遍历容器的接口。它的存在意义:是容器和算法提供了一个桥梁,没有迭代器,算法操作容器会显得很乱。迭代器内部封装了指针,它像是一个智能指针,所以它的通用性、安全性比一般指针更好。
迭代器失效的情况
1、vector、deque:对元素增删会导致该位置及其之后全部失效,且若增加元素导致扩容,则全部失效
2、list、map、set这些关联容器:增加元素不会导致任何迭代器失效,删除元素仅该位置失效。

仿函数:也叫函数对象:重载了函数调用运算符operator()的类,把函数操作封装在类里,复用性高。

class A{
public:
    bool flag;
    A(bool a): flag(a) {}
    bool operator()(int a, int b) {return flag ? a < b : a > b;}
};
int main() {
    vector<int>nums{1,3,2};
    sort(nums.begin(), nums.end(), A(0));//只需要改变一个数字就可实现升降序
    cout << nums[0];
}

适配器:用于把一个容器接口转换成我们需要的接口。
1、stack与queue,底层是deque,对外提供栈和队列的操作接口,内部就是调用的deque的操作方法。
2、preority_queue:底层是vector,堆是数据处理规则,,它对外提供队列的操作接口,且对数据进行优先级排列,常见的大小顶堆排列规则。

空间配置器allocator
1、c++里的new/delete都是2个操作,stl allocator把他们分开:对象构造有construct()负责,对象释放由destroy()负责(内部调用构造和析构函数),内存配置由allocate()负责,内存释放由deallocate()负责。
2、对于内存的申请和释放,stl采用两级空间配置器,主要是更好的处理小内存(brk的内存池管理差一点)。当需要的内存<=128字节,调用二级配置器,>128字节,调用一级配置器。一级配置器中allocate(),deallocate()内部就是malloc,free(我们知道大于128k时malloc调用的是mmap)。二级配置器使用内存池机制,他用一个数组维护16条链表,每个链表挂着不同大小的内存块,0号链表挂的全是8字节的内存块,最大128字节,他会按你的需要去查找对应大小链表是否空闲,若不够就找更大的链表,若不空闲就要向内存池申请,若所有链表都无法满足,就调用一级配置器。

vector:底层是动态数组,可以动态扩容,扩容过程:当size>capcity,先重新申请一块2倍大小的连续内存,拷贝原数据,释放原内存。为什么是成倍扩容:若是固定大小扩容,当数据增多可能导致频繁扩容,效率太低了,而成倍扩容可以较好的应对元素数量变化,减少扩容次数,所以选择成倍扩容,考虑内存的使用,倍数不能太大。
vector释放空间:vector空间是只增不减的,即clear(清空容器size变0),resize只会动size,不会动capcity,reserve改变最大容量,只有大于当前的才会生效。只有析构才会回收空间。

push_back与emplace_back():当vector存对象时会有区别:

#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass(int x, int y) : x(x), y(y) {
        std::cout << "有参构造" << std::endl;
    }
    MyClass(const MyClass& other) : x(other.x), y(other.y) {
        std::cout << "拷贝构造" << std::endl;
    }
    MyClass(MyClass&& other): x(other.x), y(other.y) {
        std::cout << "移动构造" << std::endl;
    }
private:
    int x, y;
};

int main() {
    std::vector<MyClass> v;
    v.reserve(10); //防止出现扩容影响结果
    MyClass obj1(5, 6);
    MyClass obj2(5, 6);
    std::cout << "-------------" << std::endl;

    std::cout << "传左值对比" << std::endl;
    std::cout << "push_back:" << std::endl;
    v.push_back(obj1); 
    std::cout << "emplace_back:" << std::endl;
    v.emplace_back(obj1);
    
    std::cout << "-------------" << std::endl;

    std::cout << "传右值对比" << std::endl;
    std::cout << "push_back:" << std::endl;
    v.push_back(std::move(obj1)); 
    std::cout << "emplace_back:" << std::endl;
    v.emplace_back(std::move(obj2));

    std::cout << "-------------" << std::endl;

    std::cout << "传参数列表对比" << std::endl;
    std::cout << "push_back:" << std::endl;
    v.push_back({1, 2}); 
    std::cout << "emplace_back:" << std::endl;
    v.emplace_back(1, 2);   //emplace_back形参是可变参数模板的万能引用, 可接收任意数量、类型的左值右值: emplace_back(_Args&&... __args)
    //v.emplace_back(MyClass{1, 2}); //也可以这么写,但性能就与push_back一样了
}

结果:
在这里插入图片描述
结论:

传左值/右值,push_back和emplace_back性能一样。传参数列表,push_back要先构造一个临时对象再调移动构造,而emplace_back直接调移动构造,即emplace_back性能更好,但可读性差。

deque:双端队列,以多段连续空间组成。
结构:采用一块map数组作中控台,存的每个元素都是指针,分别指向一段连续内存,叫缓冲区,这里就是存储数据的地方。deque的迭代器有4个,cue指向缓冲区当前元素,first指向缓冲区头,last指向尾,使得首尾增删效率高,node指向map主控。当当前缓冲区遍历完就要通过node访问中控台,得到下一个指针来到下一块缓冲区。维护了整体连续的假象,支持随机存取。且扩容也方便:只需要在map数组里添加指针元素指向新内存即可,不需要像vector那样麻烦。

哈希表
1、unordered_map/unordered_set底层都是哈希表,无序,首先定义一个哈希函数如取余法,把key映射到哈希表的数组的索引上,即把元素放入哈希表,但是不同元素通过哈希函数计算结果可能相同,即哈希冲突,解决方法有3种:
负载因子=元素数量/桶数量
1、用拉链法挂着一个链表,即常见的数组+链表的形式
2、线性探测法,向后找到一个空槽放入元素,一般用于负载因子较小的情况,因为若负载因子较大,探头的时间就会比较长。
3、再哈希:即扩容,把桶翻倍。

对于哈希表的增删改查,都是先通过数组定位是O(1),然后再来到链表上定位,只要冲突不是很严重,链表不是很长,链表定位也是O(1),然后在链表上增删改查也是O(1)。所以说哈希表的增删改查都是O(1),但不稳定。

红黑树: map和set底层红黑树,set的节点是value, map的节点是键值对,有序,set的迭代器是const,不允许修改元素值,map可以修改value,支持下标操作,但不能改key。
1、先谈二叉搜索树,它容易出现单链表情况,复杂度就变成O(n)
2、所以有了AVL树(任何节点的左右子树高度差<=1)通过旋转保持二叉搜索树的高度平衡,查找复杂度永远是O(logn),但每次增删节点都要旋转整棵树,所以AVL树增删效率很低。
3、而红黑树则是二叉查找数和AVL树的折中,他是大致平衡的二叉查找树。它有几条性质:节点是红或黑;根节点、叶节点、红节点的子节点都是黑色等。这些性质保证了它大致平衡,即查找是O(logn)。增删节点通过变色和旋转,且之旋转该节点到叶节点距离,保证它的增删复杂度也是O(logn)。故整体都是O(logn)。
对比:哈希表无序,更快,但不稳定。红黑树稳定,有序。

map[]、find、insert:map[]访问若存在返回引用,不存在会创建一个新的键值对,不会报错。find用于查询键若存在返回迭代器,不存在返回map.end()的迭代器

array: 底层是静态数组,相当于把静态数组变成了容器,使得它可以使用一些容器的操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值