cpp - STL 常用操作
一、序列容器(Sequential Containers)
序列容器按照元素插入的顺序存储数据,支持位置访问。
array
array 是固定大小的数组容器,在编译时确定大小,提供了类似 C 数组的性能和 STL 容器的接口。
内存布局: 连续内存存储。元素直接存储在对象内部(栈上或作为类成员),没有额外的堆分配开销。大小在编译时确定,无法动态改变。
array<int, 5> 对象: [元素0][元素1][元素2][元素3][元素4]
← 5 * sizeof(int) 字节 →
时间复杂度:
- 随机访问([]):O(1) - 连续内存,直接偏移
- 查找:O(n) - 需要遍历
- 不支持插入和删除操作
空间复杂度: O(n),无额外开销,大小固定
初始化
int iArr[10];
constexpr len0 = 10; int jArr[len];
const len = 10; int jArr[len]; // 错误。len 不是常量表达式
string strs[get_size()]; 当 get_size(); // 返回常量表达式时正确,否则错误
array 是固定大小的容器,不能增加元素,删除元素
改
array<int, 5> arr = {1, 2, 3, 4, 5};
arr[2] = 10; // {1, 2, 10, 4, 5}
arr.at(3) = 20; // {1, 2, 10, 20, 5}
查
array<int, 5> arr = {1, 2, 3, 4, 5};
int first = arr[0]; // 1
int last = arr.back(); // 5
int val = arr.at(2); // 3 (带边界检查)
auto it = find(arr.begin(), arr.end(), 3); // 返回迭代器
size_t size = arr.size(); // 5
bool empty = arr.empty(); // false
list
list 是双向链表容器,支持在任意位置快速插入和删除,但不支持随机访问。
内存布局: 双向链表。每个节点包含数据和两个指针(prev 和 next),节点在堆上独立分配,非连续存储。
list 对象: [head] → 节点1 ⇄ 节点2 ⇄ 节点3 ⇄ 节点4 ← [tail]
↓ ↓ ↓ ↓
[prev] [prev] [prev] [prev]
[data] [data] [data] [data]
[next] [next] [next] [next]
每个节点独立分配,内存不连续
时间复杂度:
- 头部/尾部插入删除(push_front/push_back/pop_front/pop_back):O(1) - 直接修改头尾指针
- 中间插入删除(已知迭代器位置):O(1) - 只需修改前后节点的指针
- 随机访问:不支持 - 必须从头或尾遍历
- 查找:O(n) - 需要遍历链表
- 排序(sort):O(n log n) - 归并排序
空间复杂度: O(n),每个节点额外存储两个指针
初始化
list<int> l1; // 空列表
list<int> l2 = {1, 2, 3, 4, 5}; // 使用初始化列表
list<int> l3(5, 10); // 包含5个值为10的元素
list<int> l4(l2); // 复制构造
增
list<int> lst = {1, 2, 3};
lst.push_back(4); // 在末尾添加: {1, 2, 3, 4}
lst.push_front(0); // 在开头添加: {0, 1, 2, 3, 4}
lst.insert(next(lst.begin()), 5); // 在指定位置插入: {0, 5, 1, 2, 3, 4}
lst.emplace_back(6); // 在末尾构造: {0, 5, 1, 2, 3, 4, 6}
lst.emplace_front(-1); // 在开头构造: {-1, 0, 5, 1, 2, 3, 4, 6}
删
list<int> lst = {1, 2, 3, 4, 5};
lst.pop_back(); // 删除末尾元素
lst.pop_front(); // 删除首个元素
lst.erase(next(lst.begin())); // 删除指定位置的元素
lst.remove(3); // 删除所有值为3的元素
lst.clear(); // 清空列表
改
list<int> lst = {1, 2, 3, 4, 5};
auto it = next(lst.begin(), 2); // 获取第三个元素的迭代器
*it = 10; // 修改元素值
lst.sort(); // 排序
lst.reverse(); // 反转
lst.unique(); // 删除连续重复元素
查
list<int> lst = {1, 2, 3, 4, 5};
int first = lst.front(); // 获取第一个元素
int last = lst.back(); // 获取最后一个元素
size_t size = lst.size(); // 获取元素个数
bool empty = lst.empty(); // 检查是否为空
auto it = find(lst.begin(), lst.end(), 3); // 查找元素
string
string 是 C++ 标准库提供的动态字符串类,用于处理可变长度的字符序列。
内存布局: 动态分配的连续内存。通常采用 SSO(Small String Optimization)优化:短字符串(通常 ≤15 字节)直接存储在对象内部,避免堆分配;长字符串则在堆上分配连续内存。
短字符串 (SSO):
string 对象: [字符数据内联存储...][长度][容量]
长字符串:
string 对象: [指针] → 堆内存: [字符数据...]
[长度]
[容量]
时间复杂度:
- 尾部插入(push_back/append):均摊 O(1) - 容量不足时需要重新分配并拷贝
- 头部/中间插入:O(n) - 需要移动插入点之后的所有字符
- 删除:O(n) - 需要移动删除点之后的所有字符
- 随机访问([]):O(1) - 连续内存直接索引
- 查找(find):O(n*m) - 子串查找,n 为字符串长度,m 为子串长度
- 拼接(+/+=):O(n) - 需要拷贝字符
空间复杂度: O(n),额外维护长度和容量信息
初始化
string s0;
string s1 = "value";
string s4 = string("value");
string s2 = s1;
string s2(s1);
string s3 = string(4, "c") // "cccc"
增
// 追加
string s = "hello";
s += " world"; // "hello world"
s.append(" !"); // "hello world !"
s.push_back('!'); // "hello world !!"
s.insert(5, " my"); // "hello my world !!"
删
string s = "hello world";
s.erase(5, 6); // 从位置5开始删除6个字符: "hello"
s.erase(s.begin() + 4); // 删除迭代器指向的单个字符
s.erase(s.begin(), s.begin() + 4); // 删除迭代器范围内的字符
s.pop_back(); // 删除最后一个字符: "hell"
改
string s = "hello world";
s.replace(6, 5, "there"); // 从位置6开始的5个字符替换为"there": "hello there"
s[0] = 'H'; // 修改单个字符: "Hello there"
查
string s = "hello world";
char c = s[0]; // 访问单个字符: 'h'
string sub = s.substr(6, 5); // 从位置6开始截取5个字符: "world"
size_t pos = s.find("world"); // 查找子串,返回首次出现位置: 6
// 如果未找到返回 string::npos
size_t pos2 = s.find_first_not_of("helo "); // 查找第一个不在给定字符集中的字符位置: 6 ('w')
size_t pos3 = s.find_last_of("o"); // 查找最后一个'o'的位置: 7
size_t pos4 = s.find_first_of("o"); // 查找第一个'o'的位置: 4
size_t pos5 = s.rfind("l"); // 从右向左查找'l'首次出现的位置: 9
size_t pos6 = s.find_last_not_of(" "); // 查找最后一个不是空格的字符位置: 10
vector
vector 是动态数组容器,支持快速随机访问和尾部高效插入删除。
内存布局: 连续内存存储。所有元素存储在一块连续的堆内存中,维护三个指针:start(起始位置)、finish(已用空间末尾)、end_of_storage(已分配空间末尾)。
vector 对象: [start] → 堆内存: [元素0][元素1][元素2][...][未使用空间...]
[finish] --------→ ↑
[end_of_storage] → ↑
size = finish - start
capacity = end_of_storage - start
时间复杂度:
- 尾部插入(push_back):均摊 O(1) - 容量不足时扩容(通常 2 倍),需要拷贝所有元素
- 尾部删除(pop_back):O(1) - 直接移动 finish 指针
- 头部/中间插入:O(n) - 需要移动插入点之后的所有元素
- 头部/中间删除:O(n) - 需要移动删除点之后的所有元素
- 随机访问([]):O(1) - 连续内存,直接指针偏移
- 查找:O(n) - 需要遍历
空间复杂度: O(n),通常会预留额外空间(capacity > size)以减少扩容次数
初始化
vector<T> v0;
vector<T> v1 = {a, b, c};
vector<T> v1{a, b, c};
vector<T> v2 = v1;
vector<T> v2(v1);
vector<T> v3(n, val); // 含有 n 个 val 的vector
增
vector<int> v = {1, 2, 3};
v.push_back(4); // {1, 2, 3, 4}
v.insert(v.begin(), 0); // {0, 1, 2, 3, 4}
v.insert(v.begin() + 2, 5); // {0, 1, 5, 2, 3, 4}
v.emplace_back(6); // {0, 1, 5, 2, 3, 4, 6}
删
vector<int> v = {1, 2, 3, 4, 5};
v.pop_back(); // {1, 2, 3, 4}
v.erase(v.begin()); // {2, 3, 4}
v.erase(v.begin(), v.begin() + 2); // {4}
v.clear(); // {}
改
vector<int> v = {1, 2, 3, 4, 5};
v[2] = 10; // {1, 2, 10, 4, 5}
v.at(3) = 20; // {1, 2, 10, 20, 5}
查
vector<int> v = {1, 2, 3, 4, 5};
int first = v[0]; // 1
int last = v.back(); // 5
int val = v.at(2); // 3 (带边界检查)
auto it = find(v.begin(), v.end(), 3); // 返回迭代器
size_t size = v.size(); // 5
bool empty = v.empty(); // false
伪代码
#include <iostream>
#include <algorithm>
#include <sstream>
#include <string>
#include <stdexcept>
template <typename T>
class Vector {
private:
T* elements_ = nullptr;
size_t capacity_ = 0;
size_t size_ = 0;
public:
Vector() = default;
~Vector() {
delete[] elements_;
}
// 拷贝构造函数
Vector(const Vector& o) {
capacity_ = o.capacity_;
size_ = o.size_;
elements_ = new T[capacity_];
std::copy(o.elements_, o.elements_ + size_, elements_);
}
// 拷贝赋值操作符
Vector& operator=(const Vector& o) {
if (this != &o) {
delete[] elements_;
capacity_ = o.capacity_;
size_ = o.size_;
elements_ = new T[capacity_];
std::copy(o.elements_, o.elements_ + size_, elements_);
}
}
void push_back(const T& value) {
if (size_ == capacity_) {
reserve(capacity_ == 0 ? 1 : capacity_ << 1);
}
elements_[size_++] = value;
}
size_t getSize() const {
return size_;
}
size_t getCapacity() const {
return capacity_;
}
T& operator[](size_t index) {
if (index >= size_) {
throw std::out_of_range("index out of range");
}
return elements_[index];
}
const T& operator[](size_t index) const {
if (index >= size_) {
throw std::out_of_range("index out of range");
}
return elements_[index];
}
void insert(size_t index, const T& value) {
if (index > size_) {
throw std::out_of_range("index out range");
}
if (size_ == capacity_) {
reserve(capacity_ == 0 ? 1 : capacity_ << 1);
}
for (size_t i = size_; i > index; --i) {
elements_[i] = elements_[i - 1];
}
elements_[index] = value;
++size_;
}
void pop_back() {
if (size_ > 0) {
--size_;
}
}
void clear() {
size_ = 0;
}
T* begin() {
return elements_;
}
T* end() {
return elements_ + size_;
}
const T* begin() const {
return elements_;
}
const T* end() const {
return elements_ + size_;
}
void printElements() const {
for (size_t i = 0; i < size_; i++) {
std::cout << elements_[i] << " ";
}
std::cout << std::endl;
}
private:
void reserve(size_t newCapacity) {
if (newCapacity > capacity_) {
auto* newElements = new T[newCapacity];
std::copy(elements_, elements_ + size_, newElements);
delete elements_;
elements_ = newElements;
capacity_ = newCapacity;
}
}
};
int main()
{
// 创建一个 Vector 对象
Vector<int> Vector;
int N;
std::cin >> N;
// 读走回车
getchar();
std::string line;
for (int i = 0; i < N; i++)
{
// 读取整行
std::getline(std::cin, line);
std::istringstream iss(line);
std::string command;
iss >> command;
if (command == "push")
{
int value;
iss >> value;
Vector.push_back(value);
}
else if (command == "print")
{
if (Vector.getSize() == 0) {
std::cout << "empty" << std::endl;
continue;
}
Vector.printElements();
}
else if (command == "size")
{
std::cout << Vector.getSize() << std::endl;
}
else if (command == "get")
{
int index;
iss >> index;
std::cout << Vector[index] << std::endl;
}
else if (command == "insert")
{
int index, value;
iss >> index >> value;
Vector.insert(index, value);
}
else if (command == "pop")
{
Vector.pop_back();
}
else if (command == "iterator")
{
if (Vector.getSize() == 0)
{
std::cout << "empty" << std::endl;
continue;
}
for (auto it = Vector.begin(); it != Vector.end(); ++it)
{
std::cout << *it << " ";
}
std::cout << std::endl;
}
else if (command == "foreach")
{
if (Vector.getSize() == 0)
{
std::cout << "empty" << std::endl;
continue;
}
for (const auto &element : Vector)
{
std::cout << element << " ";
}
std::cout << std::endl;
}
else if (command == "clear")
{
Vector.clear();
}
}
return 0;
}
deque
deque(double-ended queue,双端队列)是一个支持在两端高效插入和删除的动态数组容器。
内存布局: 分段连续存储(segmented array)。deque 由多个固定大小的连续内存块组成,通过中央索引数组(map)管理这些内存块。每个内存块存储一定数量的元素,map 数组存储指向这些内存块的指针。
map: [ptr0] [ptr1] [ptr2] [ptr3] ...
↓ ↓ ↓ ↓
[###] [###] [###] [###]
块0 块1 块2 块3
时间复杂度:
- 头部/尾部插入删除:O(1) - 只需在首/尾块操作,必要时分配新块
- 随机访问:O(1) - 通过索引计算:块索引 = index / 块大小,块内偏移 = index % 块大小
- 中间插入删除:O(n) - 需要移动插入点到最近一端之间的所有元素
- 查找:O(n) - 需要遍历
空间复杂度: O(n),额外维护 map 数组和多个内存块
初始化
deque<int> d1; // 空双端队列
deque<int> d2 = {1, 2, 3, 4, 5}; // 使用初始化列表
deque<int> d3(5, 10); // 包含5个值为10的元素
deque<int> d4(d2); // 复制构造
增
deque<int> dq = {1, 2, 3};
dq.push_back(4); // 在末尾添加: {1, 2, 3, 4}
dq.push_front(0); // 在开头添加: {0, 1, 2, 3, 4}
dq.insert(dq.begin() + 2, 5); // 在指定位置插入: {0, 1, 5, 2, 3, 4}
dq.emplace_back(6); // 在末尾构造: {0, 1, 5, 2, 3, 4, 6}
dq.emplace_front(-1); // 在开头构造: {-1, 0, 1, 5, 2, 3, 4, 6}
删
deque<int> dq = {1, 2, 3, 4, 5};
dq.pop_back(); // 删除末尾元素
dq.pop_front(); // 删除首个元素
dq.erase(dq.begin() + 1); // 删除指定位置的元素
dq.erase(dq.begin(), dq.begin() + 2); // 删除一个范围的元素
dq.clear(); // 清空双端队列
改
deque<int> dq = {1, 2, 3, 4, 5};
dq[2] = 10; // 使用下标修改元素
dq.at(3) = 20; // 使用at()修改元素(带边界检查)
查
deque<int> dq = {1, 2, 3, 4, 5};
int first = dq.front(); // 获取第一个元素
int last = dq.back(); // 获取最后一个元素
int val = dq[2]; // 使用下标访问
int val_safe = dq.at(2); // 使用at()访问(带边界检查)
size_t size = dq.size(); // 获取元素个数
bool empty = dq.empty(); // 检查是否为空
auto it = find(dq.begin(), dq.end(), 3); // 查找元素
二、关联容器(Associative Containers)
关联容器基于键值进行存储和查找,使用哈希表实现,提供快速的查找性能。
unordered_map
unordered_map 是基于哈希表实现的键值对容器,提供快速的查找、插入和删除操作。
内存布局: 哈希表 + 链表法解决冲突。维护一个桶数组(bucket array),每个桶是一个链表,存储哈希值相同的元素。通过哈希函数将键映射到桶索引。
桶数组: [0] → [key1:val1] → [key2:val2] → nullptr
[1] → nullptr
[2] → [key3:val3] → nullptr
[3] → [key4:val4] → [key5:val5] → nullptr
...
负载因子 = 元素数量 / 桶数量
当负载因子超过阈值(默认1.0)时,rehash 扩容
时间复杂度:
- 插入/删除/查找:平均 O(1),最坏 O(n) - 哈希冲突导致链表过长
- 遍历:O(n) - 需要遍历所有桶和链表节点
- rehash:O(n) - 需要重新计算所有元素的哈希值并插入新桶数组
空间复杂度: O(n),额外维护桶数组和链表节点
初始化
unordered_map<string, int> map1;
unordered_map<string, int> map2 = {{"apple", 1}, {"banana", 2}};
unordered_map<string, int> map3(map2);
增/改
unordered_map<string, int> map;
map["apple"] = 1; // 插入或更新键值对
map.insert({"banana", 2}); // 仅插入,如果键已存在则不更新
map.insert_or_assign("apple", 3); // 插入新元素或更新现有值
map.emplace("orange", 4); // 原地构造并插入元素
删
unordered_map<string, int> map = {{"apple", 1}, {"banana", 2}, {"orange", 3}};
map.erase("apple"); // 删除指定键值对
map.erase(map.begin()); // 使用迭代器删除
map.clear(); // 清空所有元素
查
unordered_map<string, int> map = {{"apple", 1}, {"banana", 2}};
int value = map["apple"]; // 访问元素(如果键不存在会插入默认值)
int value2 = map.at("apple"); // 访问元素(如果键不存在会抛出异常)
auto it = map.find("banana"); // 查找元素,返回迭代器
bool exists = map.count("apple") > 0; // 检查键是否存在
size_t size = map.size(); // 获取元素个数
bool empty = map.empty(); // 检查是否为空
unordered_set
unordered_set 是基于哈希表实现的集合容器,存储唯一元素,提供快速的查找、插入和删除操作。
内存布局: 哈希表 + 链表法解决冲突。与 unordered_map 类似,但只存储键(元素),不存储值。维护桶数组,每个桶是链表。
桶数组: [0] → [元素1] → [元素2] → nullptr
[1] → nullptr
[2] → [元素3] → nullptr
[3] → [元素4] → [元素5] → nullptr
...
负载因子 = 元素数量 / 桶数量
超过阈值时 rehash 扩容
时间复杂度:
- 插入/删除/查找:平均 O(1),最坏 O(n) - 哈希冲突导致链表过长
- 遍历:O(n) - 需要遍历所有桶和链表节点
- rehash:O(n) - 需要重新计算所有元素的哈希值并插入新桶数组
空间复杂度: O(n),额外维护桶数组和链表节点
初始化
unordered_set<int> s1; // 空集合
unordered_set<int> s2 = {1, 2, 3, 4, 5}; // 使用初始化列表
unordered_set<int> s3(s2); // 复制构造
增
unordered_set<int> s = {1, 2, 3};
s.insert(4); // 插入单个元素: {1, 2, 3, 4}
s.insert({5, 6}); // 插入多个元素: {1, 2, 3, 4, 5, 6}
s.emplace(7); // 原地构造并插入元素: {1, 2, 3, 4, 5, 6, 7}
// 检查是否存在,不存在则插入
auto [it, inserted] = s.insert(8); // 返回 pair<iterator, bool>
if (inserted) {
cout << "元素 8 被插入" << endl;
} else {
cout << "元素 8 已存在" << endl;
}
// 使用 emplace 也可以实现类似功能
auto [it2, inserted2] = s.emplace(9); // 返回 pair<iterator, bool>
if (inserted2) {
cout << "元素 9 被插入" << endl;
} else {
cout << "元素 9 已存在" << endl;
}
删
unordered_set<int> s = {1, 2, 3, 4, 5};
s.erase(3); // 删除指定元素: {1, 2, 4, 5}
s.erase(s.begin()); // 使用迭代器删除: {2, 4, 5}
s.erase(s.begin(), s.end()); // 删除一个范围的元素: {}
s.clear(); // 清空集合
查
unordered_set<int> s = {1, 2, 3, 4, 5};
bool exists = s.count(3) > 0; // 检查元素是否存在
auto it = s.find(2); // 查找元素,返回迭代器
size_t size = s.size(); // 获取元素个数
bool empty = s.empty(); // 检查是否为空
其他操作
unordered_set<int> s = {1, 2, 3, 4, 5};
s.bucket_count(); // 返回桶的数量
s.load_factor(); // 返回负载因子
s.max_load_factor(); // 返回最大负载因子
s.rehash(10); // 重新设置桶的数量
s.reserve(100); // 预留空间
遍历
unordered_set<int> s = {1, 2, 3, 4, 5};
// 使用迭代器遍历
for (auto it = s.begin(); it != s.end(); ++it) {
cout << *it << " ";
}
// 使用范围for循环遍历
for (const auto& elem : s) {
cout << elem << " ";
}
unordered_set 的特点:
- 无序存储,元素顺序不固定
- 不允许重复元素
- 查找、插入、删除操作的平均时间复杂度为 O(1)
- 使用哈希表实现
- 不支持下标访问
- 不支持排序操作
三、容器适配器(Container Adapters)
容器适配器是基于其他容器实现的特殊接口封装,不直接存储数据,而是对底层容器进行包装,提供特定的访问模式。
queue
queue 是先进先出(FIFO, First In First Out)的容器适配器,只允许在尾部插入元素,在头部删除元素。
// queue 的简化实现原理
template<typename T, typename Container = deque<T>>
class queue {
private:
Container c; // 底层容器
public:
void push(const T& value) {
c.push_back(value); // 调用底层容器的 push_back
}
void pop() {
c.pop_front(); // 调用底层容器的 pop_front
}
T& front() { return c.front(); }
T& back() { return c.back(); }
size_t size() const { return c.size(); }
bool empty() const { return c.empty(); }
};
内存布局: 基于底层容器实现(默认使用 deque,也可以使用 list)。queue 本身只是一个接口封装,真正的数据存储在底层容器中。
queue 对象: [底层容器对象]
只暴露 front/back/push/pop 接口
底层容器(deque): [元素0][元素1][元素2][元素3]
↑ front ↑ back
出队 入队
为什么默认使用 deque:
- deque 两端插入/删除都是 O(1),完美符合 queue 的需求
- list 也可以,但每个节点有额外的指针开销
- vector 不行,因为头部删除是 O(n)
时间复杂度:
- 入队(push):O(1) - 在尾部插入
- 出队(pop):O(1) - 在头部删除
- 访问队首(front):O(1) - 直接访问
- 访问队尾(back):O(1) - 直接访问
- 不支持随机访问和遍历
空间复杂度: O(n),取决于底层容器
初始化
queue<int> q1; // 空队列(默认使用 deque)
queue<int, list<int>> q2; // 使用 list 作为底层容器
queue<int> q3(deque<int>{1, 2, 3}); // 从底层容器构造
增
queue<int> q;
q.push(1); // 入队: {1}
q.push(2); // 入队: {1, 2}
q.push(3); // 入队: {1, 2, 3}
q.emplace(4); // 原地构造并入队: {1, 2, 3, 4}
删
queue<int> q;
q.push(1); q.push(2); q.push(3);
q.pop(); // 出队(删除队首元素): {2, 3}
q.pop(); // 出队: {3}
// 注意:pop() 不返回值,需要先用 front() 获取值再 pop()
查
queue<int> q;
q.push(1); q.push(2); q.push(3);
int front_val = q.front(); // 获取队首元素: 1
int back_val = q.back(); // 获取队尾元素: 3
size_t size = q.size(); // 获取元素个数: 3
bool empty = q.empty(); // 检查是否为空: false
stack
stack 是后进先出(LIFO, Last In First Out)的容器适配器,只允许在顶部插入和删除元素。
容器适配器特性:
stack 默认使用 deque 作为底层容器,也可以使用 vector 或 list。stack 只需要支持尾部操作(push_back/pop_back/back),因此这三种容器都适用。
// 默认使用 deque
stack<int> s1;
// 等价于
stack<int, deque<int>> s1;
// 也可以使用 vector
stack<int, vector<int>> s2;
// 也可以使用 list
stack<int, list<int>> s3;
内存布局: 基于底层容器实现。stack 本身只是一个接口封装,限制只能访问顶部元素。
stack 对象: [底层容器对象]
只暴露 top/push/pop 接口
底层容器(deque): [元素0][元素1][元素2][元素3]
↑ top
入栈/出栈
为什么默认使用 deque:
- deque 尾部插入/删除都是 O(1)
- deque 不需要像 vector 那样频繁重新分配内存
- list 也可以,但有额外的指针开销
时间复杂度:
- 入栈(push):O(1) - 在顶部插入
- 出栈(pop):O(1) - 在顶部删除
- 访问栈顶(top):O(1) - 直接访问
- 不支持随机访问和遍历
空间复杂度: O(n),取决于底层容器
初始化
stack<int> s1; // 空栈(默认使用 deque)
stack<int, vector<int>> s2; // 使用 vector 作为底层容器
stack<int, list<int>> s3; // 使用 list 作为底层容器
增
stack<int> s;
s.push(1); // 入栈: {1}
s.push(2); // 入栈: {1, 2}
s.push(3); // 入栈: {1, 2, 3}
s.emplace(4); // 原地构造并入栈: {1, 2, 3, 4}
删
stack<int> s;
s.push(1); s.push(2); s.push(3);
s.pop(); // 出栈(删除栈顶元素): {1, 2}
s.pop(); // 出栈: {1}
// 注意:pop() 不返回值,需要先用 top() 获取值再 pop()
查
stack<int> s;
s.push(1); s.push(2); s.push(3);
int top_val = s.top(); // 获取栈顶元素: 3
size_t size = s.size(); // 获取元素个数: 3
bool empty = s.empty(); // 检查是否为空: false
priority_queue
priority_queue 是优先级队列,基于堆实现,元素按优先级排序,每次弹出的是优先级最高(默认最大)的元素。
容器适配器特性:
priority_queue 默认使用 vector 作为底层容器,因为堆操作需要随机访问能力。也可以使用 deque,但不能使用 list(list 不支持随机访问)。
// 默认使用 vector
priority_queue<int> pq1;
// 等价于
priority_queue<int, vector<int>, less<int>> pq1;
// 也可以使用 deque
priority_queue<int, deque<int>, less<int>> pq2;
内存布局: 基于 vector 实现的最大堆(max heap)。内部使用 vector 存储元素,通过堆算法维护堆性质:父节点的值总是大于等于子节点的值。
priority_queue 对象: [底层 vector 对象]
[比较函数对象]
底层 vector(最大堆):
[9]
/ \
[7] [5]
/ \ / \
[3][4][1][2]
存储为: [9, 7, 5, 3, 4, 1, 2]
时间复杂度:
- 入队(push):O(log n) - 插入新元素后需要上浮(sift up)调整堆
- 出队(pop):O(log n) - 删除堆顶后需要下沉(sift down)调整堆
- 访问堆顶(top):O(1) - 直接访问 vector 首元素
- 构造堆(从容器构造):O(n) - 使用 heapify 算法
- 不支持遍历和查找
空间复杂度: O(n),底层 vector 存储所有元素
初始化
priority_queue<int> pq1; // 默认最大堆
priority_queue<int, vector<int>, greater<int>> pq2; // 最小堆
priority_queue<int, vector<int>, less<int>> pq3; // 最大堆(显式指定)
// 从容器构造
vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};
priority_queue<int> pq4(data.begin(), data.end());
// 自定义比较函数
auto cmp = [](int a, int b) { return a > b; }; // 最小堆
priority_queue<int, vector<int>, decltype(cmp)> pq5(cmp);
增
priority_queue<int> pq;
pq.push(3); // 入队: {3}
pq.push(1); // 入队: {3, 1}
pq.push(4); // 入队: {4, 3, 1}
pq.push(2); // 入队: {4, 3, 1, 2}
pq.emplace(5); // 原地构造并入队: {5, 4, 3, 1, 2}
删
priority_queue<int> pq;
pq.push(3); pq.push(1); pq.push(4);
int top_val = pq.top(); // 获取堆顶元素: 4
pq.pop(); // 删除堆顶元素: {3, 1}
// 注意:pop() 不返回值,需要先用 top() 获取值再 pop()
查
priority_queue<int> pq;
pq.push(3); pq.push(1); pq.push(4);
int top_val = pq.top(); // 获取堆顶元素(最大值): 4
size_t size = pq.size(); // 获取元素个数: 3
bool empty = pq.empty(); // 检查是否为空: false
四、视图与工具类(Views & Utilities)
视图和工具类不直接拥有数据,提供轻量级的访问和操作能力。
span
span 是 C++20 引入的轻量级视图,提供对连续序列(array、vector、string、指针)的非拥有型引用。不拥有数据,不能增加删除数据。
内存布局: 仅存储指针和大小。span 对象只包含两个成员:指向数据起始位置的指针和元素数量,不拷贝底层数据。
原始数据(vector/array/etc): [元素0][元素1][元素2][元素3][元素4]
↑
span 对象: [指针] --------------┘
[大小: 5]
sizeof(span<int>) ≈ 16 字节(指针8字节 + size_t 8字节)
时间复杂度:
- 创建 span:O(1) - 只存储指针和大小,不拷贝数据
- 随机访问([]):O(1) - 直接通过指针偏移
- 子视图(subspan/first/last):O(1) - 创建新的 span 对象,指向原数据的子区间
- 不支持插入和删除操作
空间复杂度: O(1),只存储指针和大小,不拷贝底层数据
初始化
vector<int> vec = {1, 2, 3, 4, 5};
span<int> s1{vec}; // 原来数据创建
span<int> s2{vec.begin(), 2}; // constexpr span( It first, size_type count );
span<int> s2{vec.begin(), vec.end()}; // constexpr span( It first, End last );
// 不可变的 vec
const vector<int> vec_const = {1, 2, 3, 4, 5};
span<const int> s3{vec_const};
查
span<int> s = {arr, 5};
int first = s[0]; // 访问元素
int first_alt = s.front(); // 访问第一个元素
int last = s.back(); // 访问最后一个元素
size_t size = s.size(); // 获取元素个数
bool empty = s.empty(); // 检查是否为空
int* data = s.data(); // 获取底层数据指针
// 子视图操作
span<int> first_half = s.first(3); // 前3个元素
span<int> last_half = s.last(2); // 后2个元素
span<int> sub = s.subspan(1, 3); // 从索引1开始的3个元素
改
span<int> s = {arr, 5};
s[2] = 10; // 修改原始数据
optional
optional 是 C++17 引入的模板类,用于表示一个可能存在也可能不存在的值,提供了一种类型安全的方式来处理可选值。
内存布局: 存储值 + 布尔标志。optional 对象包含实际值的存储空间和一个布尔标志位,指示值是否存在。即使为空也会占用完整的值空间。
optional<int> 对象:
有值时: [标志: true][值: 42]
无值时: [标志: false][未初始化的存储空间]
sizeof(optional<T>) ≈ sizeof(T) + sizeof(bool) + 对齐填充
时间复杂度:
- 赋值/构造:O(1) 或 O(T的构造成本) - 取决于类型 T
- 访问(value/value_or/*):O(1) - 直接访问存储的值
- 检查(has_value):O(1) - 检查布尔标志
- 重置(reset):O(1) 或 O(T的析构成本) - 调用析构函数并设置标志为 false
空间复杂度: O(1),固定大小,约等于 sizeof(T) + 1 字节(加对齐)
初始化
optional<int> o1; // 空 optional
optional<int> o2 = 42; // 包含值 42
optional<int> o3 = nullopt; // 显式设置为空
optional<int> o4 = make_optional(42); // 使用 make_optional 创建
增/改
optional<int> o;
o = 42; // 赋值
o.emplace(42); // 原地构造
o = nullopt; // 设置为空
查
optional<int> o = 42;
if (o.has_value()) { // 检查是否有值
cout << "有值" << endl;
}
if (o) { // 也可以直接用作布尔值
cout << "有值" << endl;
}
int value = o.value(); // 获取值(如果为空则抛出异常)
int value2 = o.value_or(0); // 获取值,如果为空则返回默认值
int value3 = *o; // 使用解引用操作符获取值
// 使用 if-let 风格的语法(C++17)
if (auto value = o) {
cout << *value << endl;
}
其他操作
optional<int> o = 42;
o.reset(); // 重置为空
o.swap(o2); // 交换两个 optional 的值
链式调用
optional<int> o1 = 42;
optional<int> o2 = nullopt;
// or_else: 当 optional 为空时执行一个函数
auto result1 = o1.or_else([] { return optional<int>(0); }); // 返回 optional<int>(84)
auto result2 = o2.or_else([] { return optional<int>(0); }); // 返回 optional<int>(0)
// transform: 当 optional 有值时,对其值进行转换
auto result3 = o1.transform([](int x) { return x * 2; }); // 返回 optional<int>(84)
auto result4 = o2.transform([](int x) { return x * 2; }); // 返回 nullopt
// and_then: 当 optional 有值时,执行一个返回 optional 的函数
auto result5 = o1.and_then([](int x) {
return x > 0 ? optional<int>(x * 2) : nullopt;
}); // 返回 optional<int>(84)
auto result6 = o2.and_then([](int x) {
return x > 0 ? optional<int>(x * 2) : nullopt;
}); // 返回 nullopt
// 链式调用示例
optional<int> o3 = 10;
auto result7 = o3
.transform([](int x) { return x * 2; }) // 20
.and_then([](int x) {
return x > 15 ? optional<int>(x) : nullopt;
}) // optional<int>(20)
.or_else([] { return optional<int>(0); }); // optional<int>(20)
实现伪代码
template <typename F>
auto transform(F&& f) const {
if (this->has_value()) {
// f 返回普通值,外层包成 optional
return std::optional{f(this->value_)};
} else {
return std::nullopt;
}
}
template <typename F>
auto and_then(F&& f) const {
if (this->has_value()) {
// f 必须返回 optional
return f(this->value_);
} else {
return std::nullopt;
}
}
std::ranges
std::ranges 是 C++20 引入的新特性,提供了一种更现代、更安全的方式来处理容器和迭代器,引入了概念(concepts)、视图(views)和范围算法(range algorithms)。
核心思想: 惰性求值(Lazy Evaluation)+ 可组合性。视图(views)不会立即执行操作或拷贝数据,只在真正需要结果时才进行计算,并且可以通过管道操作符(|)链式组合。
时间复杂度:
- 创建视图(views):O(1) - 惰性求值,不立即执行
- 迭代视图:取决于底层操作和数据量
- filter:O(n) - 需要检查每个元素
- transform:O(n) - 需要转换每个元素
- take(k):O(k) - 只处理前 k 个元素
- 范围算法:与传统 STL 算法相同,但接受范围而非迭代器对
空间复杂度:
- 视图:O(1) - 不拷贝底层数据,只存储必要的状态(如迭代器、谓词函数等)
- 转换为容器(ranges::to):O(n) - 需要分配新容器并拷贝数据
concepts
#include <ranges>
#include <vector>
#include <iostream>
// 范围概念
std::ranges::range auto r = std::vector{1, 2, 3, 4, 5};
// 输入范围
std::ranges::input_range auto input_r = std::vector{1, 2, 3};
// 前向范围
std::ranges::forward_range auto forward_r = std::vector{1, 2, 3};
// 双向范围
std::ranges::bidirectional_range auto bidir_r = std::vector{1, 2, 3};
// 随机访问范围
std::ranges::random_access_range auto random_r = std::vector{1, 2, 3};
// 连续范围
std::ranges::contiguous_range auto contig_r = std::vector{1, 2, 3};
views
视图是惰性求值的范围适配器,不会复制数据。
#include <ranges>
#include <vector>
#include <iostream>
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 过滤视图
auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; });
// 结果: {2, 4, 6, 8, 10}
// 转换视图
auto doubled = numbers | std::views::transform([](int n) { return n * 2; });
// 结果: {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
// 取前N个元素
auto first_three = numbers | std::views::take(3);
// 结果: {1, 2, 3}
// 跳过前N个元素
auto skip_first_three = numbers | std::views::drop(3);
// 结果: {4, 5, 6, 7, 8, 9, 10}
// 反转视图
auto reversed = numbers | std::views::reverse;
// 结果: {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
// 链式视图操作
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; }) // 过滤偶数
| std::views::transform([](int n) { return n * n; }) // 平方
| std::views::take(3); // 取前3个
// 结果: {4, 16, 36}
// 打印结果
for (int n : result) {
std::cout << n << " ";
}
std::cout << std::endl;
惰性求值(Lazy Evaluation)的含义:
惰性求值是指视图不会立即执行操作,而是等到真正需要数据时才进行计算。
#include <ranges>
#include <vector>
#include <iostream>
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 创建视图时不会执行任何计算
auto view = numbers | std::views::filter([](int x) {
std::cout << "检查元素: " << x << std::endl; // 这行代码此时不会执行
return x % 2 == 0;
}) | std::views::transform([](int x) {
std::cout << "转换元素: " << x << std::endl; // 这行代码此时不会执行
return x * 2;
});
std::cout << "视图已创建,但还没有执行任何计算" << std::endl;
// 只有在迭代时才会执行计算
for (int n : view) {
std::cout << "得到结果: " << n << std::endl;
}
// 输出顺序:
// 检查元素: 1
// 检查元素: 2
// 转换元素: 2
// 得到结果: 4
// 检查元素: 3
// 检查元素: 4
// 转换元素: 4
// 得到结果: 8
// ...以此类推
惰性求值的优势:
// 1. 内存效率 - 不需要存储中间结果
std::vector<int> large_data(1000000);
auto processed = large_data
| std::views::filter([](int x) { return x % 2 == 0; }) // 不复制数据
| std::views::transform([](int x) { return x * 2; }) // 不复制数据
| std::views::take(10); // 只取前10个
// 2. 可以处理无限序列
auto infinite = std::views::iota(1); // 无限序列 1, 2, 3, 4, ...
auto first_ten = infinite | std::views::take(10); // 只取前10个
// 3. 避免不必要的计算
auto expensive_view = large_data
| std::views::filter([](int x) {
std::cout << "执行昂贵的过滤操作" << std::endl;
return x > 500000;
})
| std::views::take(5); // 只计算前5个满足条件的元素
// 4. 支持短路求值
auto short_circuit = large_data
| std::views::filter([](int x) { return x > 0; })
| std::views::take(1); // 找到第一个就停止
algorithms
#include <ranges>
#include <vector>
#include <algorithm>
#include <iostream>
std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6};
// 查找算法
auto it1 = std::ranges::find(numbers, 5); // 查找元素
auto it2 = std::ranges::find_if(numbers, [](int n) { return n > 5; }); // 条件查找
// 计数算法
int count = std::ranges::count(numbers, 1); // 计数特定元素
int count_if = std::ranges::count_if(numbers, [](int n) { return n % 2 == 0; }); // 条件计数
// 排序算法
std::ranges::sort(numbers); // 排序
std::ranges::sort(numbers, std::greater{}); // 降序排序
std::ranges::partial_sort(numbers, numbers.begin() + 3); // 部分排序
// 复制算法
std::vector<int> dest(8);
std::ranges::copy(numbers, dest.begin()); // 复制到目标容器
// 转换算法
std::vector<int> doubled;
std::ranges::transform(numbers, std::back_inserter(doubled), [](int n) { return n * 2; });
// 填充算法
std::ranges::fill(numbers, 0); // 填充为0
std::ranges::generate(numbers, []() { return rand() % 100; }); // 生成随机数
// 移除算法
auto new_end = std::ranges::remove(numbers, 1); // 移除特定元素
auto new_end2 = std::ranges::remove_if(numbers, [](int n) { return n < 3; }); // 条件移除
// 唯一化算法
auto unique_end = std::ranges::unique(numbers); // 移除连续重复元素
// 替换算法
std::ranges::replace(numbers, 1, 10); // 替换特定元素
std::ranges::replace_if(numbers, [](int n) { return n < 3; }, 0); // 条件替换
// 旋转算法
std::ranges::rotate(numbers, numbers.begin() + 3); // 旋转
// 洗牌算法
std::ranges::shuffle(numbers, std::mt19937{std::random_device{}}()); // 随机洗牌
工具函数
#include <ranges>
#include <vector>
#include <iostream>
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 距离计算
auto distance = std::ranges::distance(numbers.begin(), numbers.end());
// 下一个迭代器
auto next_it = std::ranges::next(numbers.begin(), 2);
// 前一个迭代器
auto prev_it = std::ranges::prev(numbers.end(), 2);
// 交换元素
std::ranges::swap(numbers[0], numbers[1]);
// 迭代器对
auto [begin, end] = std::ranges::subrange(numbers);
// 范围大小
auto size = std::ranges::size(numbers);
// 范围是否为空
bool empty = std::ranges::empty(numbers);
// 范围数据指针
auto data = std::ranges::data(numbers);
std::ranges::to - 范围转换工具:
std::ranges::to 是 C++23 引入的工具函数,用于将范围(range)转换为指定的容器类型。它提供了一种简洁、类型安全的方式来将视图或其他范围转换为具体的容器。
#include <ranges>
#include <vector>
#include <list>
#include <set>
#include <string>
#include <iostream>
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 基本用法 - 将视图转换为 vector
auto even_numbers = numbers
| std::views::filter([](int x) { return x % 2 == 0; })
| std::ranges::to<std::vector<int>>(); // 转换为 vector<int>
// 转换为不同类型的容器
auto doubled_list = numbers
| std::views::transform([](int x) { return x * 2; })
| std::ranges::to<std::list<int>>(); // 转换为 list<int>
auto unique_set = numbers
| std::views::filter([](int x) { return x > 5; })
| std::ranges::to<std::set<int>>(); // 转换为 set<int>
// 字符串处理
std::string text = "hello world";
auto words = text
| std::views::split(' ')
| std::ranges::to<std::vector<std::string>>();
// 嵌套容器
auto matrix = std::views::iota(1, 4)
| std::views::transform([](int i) {
return std::views::iota(1, i + 1) | std::ranges::to<std::vector<int>>();
})
| std::ranges::to<std::vector<std::vector<int>>>();