【c++】STL 核心组件解析:容器、算法与迭代器

深入剖析底层实现原理,包括核心数据结构设计、内存分配机制、扩容策略等关键技术细节。不仅要掌握API的使用方法,更要理解容器类库的架构思想和实现逻辑,达到知其然更知其所以然的深度。


一、标准容器

STL容器分为以下几类:

  • 序列容器:vector, deque, list
  • 容器适配器:stack, queue, priority_queue
  • 关联容器:set, multiset, map, multimap
  • 无序关联容器:unordered_set, unordered_multiset, unordered_map, unordered_multimap

1. vector 向量容器

底层数据结构

是一个动态一维数组(连续内存空间)

vector的扩容

  • 扩容机制:当容量不足时,按当前容量的2倍重新分配内存
  • 扩容代价:需要拷贝原有元素到新空间,时间复杂度O(n)

在这里插入图片描述

声明与初始化

vector<int> vec;// 空
vectorvector<int> vec(10);// 10个元素,默认值0
vector<int> vec(10, 5);// 10个元素,值均为5
vector<int> vec2(vec);// 拷贝构造

元素操作

操作时间复杂度说明
push_back(val)O(1)*尾部插入,可能触发扩容
pop_back()O(1)尾部删除
insert(it, val)O(n)在迭代器位置插入,可能导致扩容和后续元素移动
erase(it)O(n)删除迭代器位置元素,导致后续元素前移
operator[]O(1)随机访问(不检查越界)
at(i)O(1)随机访问(会检查越界,抛出异常)

*注:均摊时间复杂度为O(1),考虑扩容因素

迭代器与遍历

// 常规迭代器
for(auto it = vec.begin(); it != vec.end(); ++it)

// 反向迭代器
for(auto rit = vec.rbegin(); rit != vec.rend(); ++rit)

// 范围for循环(C++11)
for(auto& val : vec)

// 算法遍历
for_each(vec.begin(), vec.end(), [](int val){...});

insert、erase 导致迭代器失效

  1. insert 插入元素
    • 若触发扩容(size == capacity),所有迭代器失效(包括 begin(), end())。
    • 未触发扩容,仅 插入位置及之后的迭代器失效(后面的元素被移动)。
  2. erase 删除元素
    • 被删除元素及之后的所有迭代器失效(前面的元素不受影响)。
    • 正确做法:用 erase 的返回值更新迭代器(指向下一个有效元素)。

常用成员函数

函数说明
size()当前元素数量
empty()判断是否为空
reserve(n)预分配至少能容纳n个元素的内存空间(不改变size)
resize(n)调整容器大小为n个元素(多删少补默认值)
v1.swap(v2)与另一个vector交换内容(高效操作,O(1)时间复杂度)

补充说明:

  1. reserve(n) 给vector预留空间,只给容器底层开辟指定大小的内存空间,并不会添加新的元素。
  2. resize(n) 容器扩容用的,不仅给容器底层开辟指定大小的内存空间,还会添加新的元素。
  3. v1.swap(v2) 两个容器进行元素交换。是高效操作,仅交换内部指针,不复制元素。

2. deque 双端队列

底层数据结构

是一个二维数组,第一维用来存放中控区,第二维用来存放缓冲区的数据。
分段连续空间(多个固定大小的数组 + 中控器)
deque采用一块map作为主控。这里map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段连续线性空间,称为缓冲区,这个缓冲区才是deque的主体,大致如下图:

在这里插入图片描述

deque的扩容

  • 以两倍的方式进行扩容
  • 每次扩容后,原来第二维的数组,从新的第一维数组的下表 oldsize/2 开始存放,上下都预留相同的空行,方便支持deque的首尾元素添加

在这里插入图片描述

声明与初始化

deque<int> dq;// 空
deque<int> dq(10);// 10个元素,默认值0
deque<int> dq(10, 5);// 10个元素,值均为5
deque<int> dq2(dq);// 拷贝构造

元素操作

操作时间复杂度说明
push_front(val)O(1)头部插入
push_back(val)O(1)尾部插入
pop_front()O(1)头部删除
pop_back()O(1)尾部删除
insert(it, val)O(n)中间插入效率低于list
erase(it)O(n)中间删除效率低于list
operator[]O(1)随机访问(比vector稍慢)

特点

  1. 首尾操作性能接近list,随机访问性能接近vector
  2. 迭代器失效情况:
    • 首尾插入只会使被操作位置的迭代器失效
    • 中间插入/删除会使所有迭代器失效

常用成员函数

d.front();// 访问首元素
d.back();// 访问尾元素
d.resize(n);// 调整大小

3. list 双向链表容器

底层数据结构

  • 双向循环链表
  • 每个节点存储数据+前后指针
  • 插入删除不会使其他迭代器失效

声明与初始化

list<int> lst;// 空
list<int> lst(10);// 10个元素,默认值0
list<int> lst(10, 5);// 10个元素,值均为5
list<int> lst2(lst);// 拷贝构造

元素操作

操作时间复杂度说明
push_front(val)O(1)头部插入
push_back(val)O(1)尾部插入
pop_front()O(1)头部删除
pop_back()O(1)尾部删除
insert(it, val)O(1)任意位置高效插入
erase(it)O(1)任意位置高效删除
remove(val)O(n)删除所有等于val的元素

特点

  1. 不支持随机访问(没有operator[]

  2. 插入删除不会使其他迭代器失效(被删除元素的迭代器除外)

  3. 特有的操作:

    lst.sort();// 链表排序(比通用sort算法更高效)
    lst.unique();// 去重(需先排序)
    lst.merge(lst2);// 合并两个有序链表
    lst.splice(it, lst2);// 将lst2的所有元素转移到指定位置
    

4.vector、deque、list对比

vector 和 deque 之间的区别?

  • 底层数据结构:vector是动态一维数组,deque是二维数组
  • 头尾插入元素的时间复杂度:头部 deque O(1) vector O(n), 尾部都是O(1)。如果需要频繁在头部操作,deque更合适
  • 对内存的使用效率:vector需要的内存空间必须是连续的;deque可以分块进行数据存储,不需要内存空间是连续的
  • 在中间进行 insert 或者erase,时间复杂度都是O(n), vector 的内存完全连续,它移动元素会更加方便。由于deque的第二维内存空间不是连续的,所以在deque中间进行元素的insert或者erase, 造成元素移动的时候,比vector要慢

vector 和 list 之间的区别?

  • 底层数据结构:vector是动态一维数组,list是一个双向循环链表
  • 随机访问:vector支持随机访问O(1);list不支持随机访问,访问某个元素的效率为O(n)
  • 对内存的使用效率:vector需要的内存空间必须是连续的;list不需要内存空间是连续的

5.容器适配器

怎么理解容器适配器

  • 容器适配器底层没有自己的数据结构,它是另外一个容器的封装,它的方法全部由底层依赖的容器实现
  • 没有实现自己的迭代器
  • 主要的容器适配器:stack(栈)、queue(队列)、priority_queue(优先级队列)

stack: 后进先出数据结构,默认基于deque实现

queue: 先进先出数据结构,默认基于deque实现

priority_queue: 元素按优先级排序,最高优先级元素总是位于队首。默认基于vector实现,使用大堆结构

主要成员函数:

操作stack (栈)queue (队列)priority_queue (优先队列)
插入元素push(val)push(val)push(val)
删除元素pop()pop()pop()
访问顶部/首元素top()front()top()
访问尾部元素-back()-
检查是否为空empty()empty()empty()
获取元素数量size()size()size()

【思考】stack => deque 为什么不依赖vector? queue => deque 为什么不依赖vector?

  1. 内存效率与扩容策略
    • vector 需要连续内存,扩容时需整体拷贝(O(n)),初始扩容频繁(0→1→2→4→8…)。
    • deque 分块分配(默认是 4096/sizeof(int) = 1024),按需增删块,无整体重分配,内存利用率更高。
  2. 对于queue来说,需要支持尾部插入,头部删除, O(1) 。如果queue依赖vector, 其出队效率很低
  3. 大规模数据下的稳定性
    • vector 大连续内存可能引发碎片化或分配失败。
    • deque 分散存储避免连续内存需求,更适合海量数据场景。

【思考】priority_queue 为什么依赖vector?

priority_queue 底层使用 堆(大根堆),而堆依赖 随机访问(O(1))连续内存 来高效计算父子节点位置(parent = (i-1)/2, left = 2i+1)。vector 的连续内存布局完美匹配这一需求,而 deque 的分块存储会显著增加计算复杂度。

6.关联容器

关联容器是C++标准库中基于键(Key)高效存储和检索数据的容器,与顺序容器不同,它们不依赖物理存储顺序,而是通过红黑树哈希表实现快速查找。

容器类型底层实现键唯一性元素顺序查找复杂度
set红黑树唯一升序O(log n)
multiset红黑树可重复升序O(log n)
map红黑树唯一按键升序O(log n)
multimap红黑树可重复按键升序O(log n)
unordered_set哈希表唯一无序O(1)~O(n)
unordered_multiset哈希表可重复无序O(1)~O(n)
unordered_map哈希表唯一无序O(1)~O(n)
unordered_multimap哈希表可重复无序O(1)~O(n)

注:

  • 有序容器(set/map等)基于红黑树,保证元素按键排序,查找稳定(O(log n))。
  • 无序容器(unordered_set/unordered_map等)基于哈希表,平均查找O(1),最坏情况O(n)(哈希冲突时)。

核心操作

(1)通用操作(所有关联容器支持)

操作说明示例
insert(key)插入元素set.insert(42);
erase(key)删除指定键的元素map.erase("Alice");
find(key)查找元素,返回迭代器auto it = set.find(42);
count(key)统计键出现的次数multiset.count(5);
size()返回元素个数map.size();
empty()判断是否为空if (set.empty()) {...}
clear()清空容器unordered_set.clear();

(2)有序容器特有操作

操作说明示例
lower_bound(key)返回第一个≥key的迭代器auto it = set.lower_bound(10);
upper_bound(key)返回第一个>key的迭代器auto it = set.upper_bound(10);
equal_range(key)返回键匹配的范围(pair)auto [first, last] = map.equal_range("Bob");

(3)无序容器特有操作

操作说明示例
bucket_count()返回哈希桶数量unordered_map.bucket_count();
load_factor()返回当前负载因子unordered_set.load_factor();
rehash(n)调整哈希桶数量unordered_map.rehash(100);

二、近容器

近容器是C++标准库中提供的一组特殊容器,它们在功能上与标准容器相似,但针对特定场景进行了优化或简化。下面总结 array, string, bitset 的常见方法:

array(固定大小数组)

操作说明示例
size()返回元素数量arr.size()
empty()检查是否为空arr.empty()
operator[]无检查访问arr[2] = 5
at()带边界检查访问arr.at(3)
front()/back()访问首尾元素arr.front()
fill()填充相同值arr.fill(0)
data()返回底层数组指针int* p = arr.data()

string(字符串容器)

操作说明示例
size()/length()返回字符数量str.size()
empty()检查是否为空str.empty()
operator[]无检查访问str[1]
at()带边界检查访问str.at(2)
c_str()返回C风格字符串str.c_str()
append()追加内容str.append("abc")
substr()获取子串str.substr(2,3)
find()查找子串str.find("xyz")

bitset(位集合容器)

操作说明示例
size()返回位数bitset.size()
count()统计1的位数bitset.count()
test()检查指定位bitset.test(3)
set()置位(设为1)bitset.set(2)
reset()复位(设为0)bitset.reset(4)
flip()翻转位bitset.flip(1)
to_string()转为字符串bitset.to_string()

三、迭代器

以下整理几种常用迭代器类型:

迭代器类型读写权限遍历方向获取方法特点示例代码片段
iterator读写正向begin()/end()可以修改元素值`auyo it = v.begin();
*it = 10;`
const_iterator只读正向cbegin()/cend()禁止修改元素值`auyo cit = v.cbegin();
int x = *cit;`
reverse_iterator读写反向rbegin()/rend()从尾向头遍历,可修改`auto rit = v.rbegin();
*rit = 5;`
const_reverse_iterator只读反向crbegin()/crend()从尾向头遍历,不可修改`auto crit = v.crbegin();
int y = *crit;`

四、函数对象

函数对象(Function Object),也称为仿函数(Functor),是C++中行为类似函数的对象。它们通过重载 operator() 实现函数调用功能。

class Add {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

// 使用
Add adder;
int sum = adder(3, 4);  // 调用operator()

c++用函数对象来代替函数指针

1.通过函数对象调用opertator(),可以省略函数的调用开销,比通过函数指针调用函数(不能够用inline内联调用)效率高

2.因为函数对象是用类生成的,所以可以添加相关的成员变量,用来记录函数对象使用时更多的信息

五、泛型算法

泛型算法是C++标准库中一组与容器类型无关的通用算法,通过迭代器抽象与各种容器协作,提供高效的数据处理能力。

特点一:泛型算法接收的都是迭代器

特点二:泛型算法的参数还可以接收函数对象

C++常用泛型算法

算法类别算法名称功能描述示例用法
非修改序列算法find查找第一个等于值的元素auto it = find(v.begin(), v.end(), 42);
find_if查找第一个满足条件的元素find_if(v.begin(), v.end(), [](int x){ return x>10; });
count统计等于值的元素数量int n = count(v.begin(), v.end(), 5);
for_each对每个元素执行操作for_each(v.begin(), v.end(), [](int& x){ x*=2; });
修改序列算法copy复制序列到目标位置copy(src.begin(), src.end(), dest.begin());
transform对元素进行转换并存储transform(v.begin(), v.end(), result.begin(), [](int x){ return x*x; });
replace替换指定值的元素replace(v.begin(), v.end(), 0, 42);
排序与相关算法sort快速排序(不稳定)sort(v.begin(), v.end());
stable_sort稳定排序stable_sort(v.begin(), v.end());
binary_search二分查找(要求已排序)bool b = binary_search(v.begin(), v.end(), 42);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

海绵宝宝的好伙伴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值