STL(Standard Template Library,标准模板库)是 C++ 标准库中的一个重要组成部分。STL 提供了一组通用的模板类和算法,极大地简化了数据结构和算法的使用。STL 的设计思想是基于泛型编程,允许用户在不牺牲效率的情况下,使用高度抽象的通用数据结构和算法。
1. STL 的核心组件
STL 由以下三个主要部分组成:
- 容器(Containers):存储和管理数据的集合。
- 迭代器(Iterators):遍历容器元素的工具,充当容器与算法之间的桥梁。
- 算法(Algorithms):对容器中的数据进行操作的函数集。
1.1 容器(Containers)
容器是 STL 中用于存储数据的模板类。不同的容器适用于不同的应用场景,根据数据的存储和访问方式,STL 容器可以分为三类:
序列式容器(Sequence Containers)
序列式容器用于存储有序的数据,这些容器中的元素按插入顺序进行存储。
- std::vector:动态数组,提供随机访问能力,可以动态调整大小。
- std::deque:双端队列,支持在两端快速插入和删除元素。
- std::list:双向链表,支持在任意位置快速插入和删除元素,但不支持随机访问。
- std::forward_list:单向链表,类似于 std::list,但只能向前遍历。
关联式容器(Associative Containers)
关联式容器用于存储有序的键值对集合,支持高效的查找、插入和删除操作。
- std::set:有序集合,存储唯一的元素,支持高效的查找。
- std::multiset:类似于 std::set,但允许重复的元素。
- std::map:有序字典,以键值对的形式存储元素,键唯一。
- std::multimap:类似于 std::map,但允许重复的键。
无序关联式容器(Unordered Associative Containers)
无序关联式容器类似于关联式容器,但它们的元素不是按顺序存储的,而是使用哈希表存储的,提供更快的平均时间复杂度。
- std::unordered_set:无序集合,存储唯一的元素。
- std::unordered_multiset:类似于 std::unordered_set,但允许重复的元素。
- std::unordered_map:无序字典,以键值对的形式存储元素,键唯一。
- std::unordered_multimap:类似于 std::unordered_map,但允许重复的键。
容器适配器(Container Adapters)
容器适配器提供了对基本容器的一层包装,使其行为更像特定的数据结构。
- std::stack:栈,后进先出(LIFO)的数据结构,基于 std::deque 实现。
- std::queue:队列,先进先出(FIFO)的数据结构,基于 std::deque 实现。
- std::priority_queue:优先级队列,允许访问最高优先级的元素,基于堆实现。
1.2 迭代器(Iterators)
迭代器是 STL 中用于遍历容器元素的工具,它们为容器与算法之间提供了统一的接口。迭代器的设计使得算法可以独立于具体的容器操作。
迭代器的种类
- 输入迭代器(Input Iterator):只读访问,支持从容器中读取数据,通常用于单遍扫描。
- 输出迭代器(Output Iterator):只写访问,支持向容器中写入数据。
- 前向迭代器(Forward Iterator):可以多次遍历,同样支持只读访问。
- 双向迭代器(Bidirectional Iterator):支持在容器中双向遍历,如 std::list 和 std::set。
- 随机访问迭代器(Random Access Iterator):支持在容器中进行任意位置的访问,如 std::vector 和 std::deque。
迭代器的使用
迭代器的使用非常类似于指针,支持通过 * 操作符访问元素,++ 操作符移动到下一个元素。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
1.3 算法(Algorithms)
STL 提供了一组通用算法,用于操作容器中的元素。算法是 STL 的核心组件之一,可以执行诸如搜索、排序、修改和排序等操作。
常用算法
- 非变异算法:不修改容器内容,只读取数据。例如 std::find, std::count, std::accumulate。
- 变异算法:修改容器内容。例如 std::copy, std::replace, std::fill。
- 排序算法:对容器元素进行排序。例如 std::sort, std::partial_sort, std::nth_element。
- 数值算法:执行数值计算。例如 std::accumulate, std::inner_product, std::adjacent_difference。
算法的使用
算法通常以容器的迭代器为参数,从而对容器中的元素进行操作。下面是一个简单的例子,使用 std::sort 对 std::vector 进行排序:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2};
std::sort(vec.begin(), vec.end());
for (const auto& val : vec) {
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
1.4 泛型编程
STL 是泛型编程的经典应用,它使得算法与容器分离,能够独立于特定的数据结构而工作。通过使用模板,STL 实现了通用的数据结构和算法,它们可以适用于不同类型的容器和数据类型。
模板函数
STL 中的算法大多是模板函数,可以用于任何符合迭代器接口的容器类型。例如,std::sort 既可以用于 std::vector,也可以用于 std::deque。
模板类
STL 中的容器和迭代器都是模板类。模板类的设计使得它们可以存储和操作任何类型的元素。例如,std::vector 是一个存储 int 类型元素的向量,而 std::vectorstd::string 是一个存储字符串的向量。
1.5 STL 的高级特性
容器与算法的互操作性
由于迭代器提供了统一的接口,STL 中的所有算法都可以与任何容器类型配合使用。这种互操作性是 STL 强大的原因之一。
函数对象与 Lambda 表达式
STL 中的算法通常接受函数对象(function object)或 Lambda 表达式作为参数,用于指定自定义的比较规则或操作。例如:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2};
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b; // 自定义比较规则:降序排序
});
for (const auto& val : vec) {
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
迭代器适配器
STL 还提供了迭代器适配器(iterator adapters),如 std::back_inserter、std::front_inserter 和 std::reverse_iterator,用于扩展迭代器的功能。例如,std::back_inserter 可以将元素插入到容器的末尾。
2. 容器使用
容器(Containers)是 C++ 标准模板库(STL)的核心组件之一,用于存储和管理数据。C++ 提供了多种类型的容器,每种容器都有其特定的用途和特性。下面我将详细讲解 C++ 中常用的几种容器及其使用方法。
2.1 序列式容器(Sequence Containers)
序列式容器是用于存储有序集合的容器,它们按照插入顺序存储元素。
2.1.1 std::vector
std::vector 是一个动态数组,允许你在数组的末尾添加或删除元素。std::vector 支持快速随机访问,但在中间插入或删除元素的开销较大。
基本操作
#include <iostream>
#include <vector>
int main() {
// 创建一个空的 vector
std::vector<int> vec;
// 添加元素
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
// 访问元素
std::cout << "Element at index 1: " << vec[1] << std::endl;
// 使用迭代器遍历元素
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 删除元素
vec.pop_back(); // 删除最后一个元素
// 获取大小和容量
std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
return 0;
}
常用操作
- push_back():在向量的末尾添加元素。
- pop_back():删除向量的最后一个元素。
- size():返回向量中的元素个数。
- capacity():返回向量的容量,即在不重新分配内存的情况下,向量能够存储的元素个数。
- resize():改变向量的大小。
- reserve():为向量预留一定的内存空间。
push_back()、pop_back()、size()、capacity()、resize() 和 reserve() 是 std::vector 中常用的成员函数,它们各自的功能和用途有所不同。下面详细解释这些函数的作用及其区别:
- push_back()
功能:push_back() 用于在 std::vector 的末尾添加一个新元素。每次调用 push_back() 时,向量的大小(即元素个数)都会增加 1。
扩展能力:如果 vector 的当前容量(capacity)不足以容纳新元素,push_back() 会自动扩展容量。扩展容量时,vector 会重新分配内存,并将现有元素复制到新内存块中。
示例:
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
// vec 现在包含两个元素:{1, 2}
- pop_back()
功能:pop_back() 删除 std::vector 的最后一个元素。每次调用 pop_back() 时,向量的大小(size)会减少 1。
注意:pop_back() 不返回被删除的元素,也不减少向量的容量(capacity),只是减少了向量的大小。
示例:
std::vector<int> vec = {1, 2, 3};
vec.pop_back();
// vec 现在包含两个元素:{1, 2}
- size()
功能:size() 返回 std::vector 中当前存储的元素个数,也就是向量的大小。
与容量的区别:size() 返回的是实际存储在向量中的元素数量,而不是向量可以存储的总容量(那是 capacity() 的作用)。
示例:
std::vector<int> vec = {1, 2, 3};
std::cout << "Size: " << vec.size() << std::endl; // 输出: Size: 3
- capacity()
功能:capacity() 返回 std::vector 当前的容量,即在不需要重新分配内存的情况下,向量最多可以存储的元素数量。
扩展与缩减:capacity() 可能大于或等于 size()。当 vector 需要增加元素且当前容量不足时,capacity() 会自动扩展。当 capacity() 扩展时,通常会按倍数增加。
示例:
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
std::cout << "Capacity: " << vec.capacity() << std::endl; // 输出: Capacity: 2(或者更大,取决于实现)
- resize()
功能:resize() 改变 std::vector 的大小(size)。它可以增加或减少向量的大小。
扩大向量:如果新大小大于当前大小,resize() 会在末尾插入值初始化的元素(默认值为 T() 或者你指定的值)。
缩小向量:如果新大小小于当前大小,resize() 会删除多余的元素。
对容量的影响:resize() 可能会改变 capacity(),但不总是如此。如果新大小超过当前容量,则 vector 会重新分配内存并扩展容量。
示例:
std::vector<int> vec = {1, 2, 3};
vec.resize(5);
// vec 现在包含:{1, 2, 3, 0, 0}
vec.resize(2);
// vec 现在包含:{1, 2}
- reserve()
功能:reserve() 为 std::vector 预留至少指定数量的内存空间,但不会改变向量的大小(size)。
与 resize() 的区别:reserve() 只影响容量(capacity),不影响大小(size)。它仅预分配内存,以避免后续添加大量元素时频繁的内存重新分配(提高性能)。
用途:reserve() 适用于知道将要添加多少元素的情况下,通过预先分配足够的空间来优化性能。
示例:
std::vector<int> vec;
vec.reserve(10);
std::cout << "Capacity after reserve: " << vec.capacity() << std::endl; // 输出: Capacity after reserve: 10
总结与区别
- push_back():在向量的末尾添加元素,如果容量不足则自动扩展容量。
- pop_back():删除向量的最后一个元素,不影响容量。
- size():返回向量中的元素个数(实际存储的数量)。
- capacity():返回向量的容量,即在不重新分配内存的情况下,向量能够存储的最大元素数量。
- resize():改变向量的大小,可能会增加或减少向量的元素数量,也可能影响容量。
- reserve():为向量预留一定的内存空间,不改变向量的大小,只影响容量。
2.1.2 std::deque
std::deque 是双端队列,支持在两端快速插入和删除元素。std::deque 提供了几乎所有 std::vector 的功能,此外还支持在前端快速插入和删除元素。
基本操作
#include <iostream>
#include <deque>
int main() {
// 创建一个 deque
std::deque<int> deq;
// 在前端和末尾添加元素
deq.push_front(1);
deq.push_back(2);
deq.push_front(3);
// 访问元素
std::cout << "First element: " << deq.front() << std::endl;
std::cout << "Last element: " << deq.back() << std::endl;
// 删除元素
deq.pop_front(); // 删除第一个元素
deq.pop_back(); // 删除最后一个元素
// 使用迭代器遍历元素
for (std::deque<int>::iterator it = deq.begin(); it != deq.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
常用操作
- push_front() 和 push_back():在 deque 的前端或末尾添加元素。
- pop_front() 和 pop_back():删除 deque 的第一个或最后一个元素。
- front() 和 back():访问 deque 的第一个和最后一个元素。
2.1.3 std::list 和 std::forward_list
std::list 是双向链表,允许在容器中的任意位置快速插入和删除元素,但不支持随机访问。std::forward_list 是单向链表,比 std::list 更节省内存,但只能向前遍历。
基本操作
#include <iostream>
#include <list>
int main() {
// 创建一个 list
std::list<int> lst = {1, 2, 3};
// 在任意位置插入元素
lst.insert(++lst.begin(), 4); // 在第二个位置插入 4
// 访问第一个和最后一个元素
std::cout << "First element: " << lst.front() << std::endl;
std::cout << "Last element: " << lst.back() << std::endl;
// 删除元素
lst.pop_front(); // 删除第一个元素
// 使用迭代器遍历元素
for (std::list<int>::iterator it = lst.begin(); it != lst.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
常用操作
- push_front() 和 push_back():在 list 的前端或末尾添加元素。
- pop_front() 和 pop_back():删除 list 的第一个或最后一个元素。
- insert() 和 erase():在 list 的任意位置插入或删除元素。
2.2 关联式容器(Associative Containers)
关联式容器用于存储键值对,通常是有序的,支持高效的查找、插入和删除操作。
2.2.1 std::set 和 std::multiset
std::set 是一个有序集合,存储唯一的元素。std::multiset 允许重复元素。
基本操作
#include <iostream>
#include <set>
int main() {
// 创建一个 set
std::set<int> s = {3, 1, 4, 1, 5};
// 插入元素
s.insert(2);
// 查找元素
auto it = s.find(3);
if (it != s.end()) {
std::cout << "Found 3" << std::endl;
}
// 遍历元素
for (auto& elem : s) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
常用操作
- insert():向 set 中插入元素,set 会自动保持元素的有序性。
- find():查找某个元素是否存在于 set 中。
- erase():删除 set 中的某个元素。
2.2.2 std::map 和 std::multimap
std::map 是一个有序字典,以键值对的形式存储元素,键唯一。std::multimap 允许多个相同的键。
基本操作
#include <iostream>
#include <map>
int main() {
// 创建一个 map
std::map<int, std::string> m;
// 插入键值对
m[1] = "one";
m[2] = "two";
m[3] = "three";
// 访问元素
std::cout << "Key 1: " << m[1] << std::endl;
// 遍历键值对
for (const auto& pair : m) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
常用操作
- operator[]:通过键访问或插入元素。
- insert():插入键值对,map 会自动保持键的有序性。
- find():查找某个键是否存在于 map 中。
- erase():删除 map 中的某个键值对。
2.3 无序关联式容器(Unordered Associative Containers)
无序关联式容器使用哈希表实现,元素的存储顺序与插入顺序无关,但查找和插入操作的平均时间复杂度较低。
2.3.1 std::unordered_set 和 std::unordered_multiset
std::unordered_set 是一个无序集合,存储唯一的元素。std::unordered_multiset 允许重复元素。
基本操作
#include <iostream>
#include <unordered_set>
int main() {
// 创建一个 unordered_set
std::unordered_set<int> uset = {3, 1, 4, 1, 5};
// 插入元素
uset.insert(2);
// 查找元素
auto it = uset.find(3);
if (it != uset.end()) {
std::cout << "Found 3" << std::endl;
}
// 遍历元素
for (auto& elem : uset) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
常用操作
- insert():向 unordered_set 中插入元素,unordered_set 使用哈希表存储元素。
- find():查找某个元素是否存在于 unordered_set 中。
- erase():删除 unordered_set 中的某个元素。
2.3.2 std::unordered_map 和 std::unordered_multimap
std::unordered_map 是一个无序字典,以键值对的形式存储元素,键唯一。std::unordered_multimap 允许多个相同的键。
基本操作
#include <iostream>
#include <unordered_map>
int main() {
// 创建一个 unordered_map
std::unordered_map<int, std::string> umap;
// 插入键值对
umap[1] = "one";
umap[2] = "two";
umap[3] = "three";
// 访问元素
std::cout << "Key 1: " << umap[1] << std::endl;
// 遍历键值对
for (const auto& pair : umap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
常用操作
- operator[]:通过键访问或插入元素。
- insert():插入键值对,unordered_map 使用哈希表存储键值对。
- find():查找某个键是否存在于 unordered_map 中。
- erase():删除 unordered_map 中的某个键值对。
2.4 容器适配器(Container Adapters)
容器适配器为基础容器提供了特定的接口,使其行为类似于其他数据结构。
2.4.1 std::stack
std::stack 是后进先出(LIFO)的数据结构,通常基于 std::deque 实现。
基本操作
#include <iostream>
#include <stack>
int main() {
// 创建一个 stack
std::stack<int> s;
// 压入元素
s.push(1);
s.push(2);
s.push(3);
// 访问栈顶元素
std::cout << "Top element: " << s.top() << std::endl;
// 弹出元素
s.pop();
std::cout << "New top element: " << s.top() << std::endl;
return 0;
}
常用操作
- push():将元素压入栈顶。
- pop():移除栈顶元素。
- top():访问栈顶元素。
2.4.2 std::queue 和 std::priority_queue
std::queue 是先进先出(FIFO)的数据结构,通常基于 std::deque 实现。std::priority_queue 是一个优先级队列,元素按照优先级排序,通常基于堆实现。
基本操作
#include <iostream>
#include <queue>
int main() {
// 创建一个 queue
std::queue<int> q;
// 压入元素
q.push(1);
q.push(2);
q.push(3);
// 访问队头元素
std::cout << "Front element: " << q.front() << std::endl;
// 弹出元素
q.pop();
std::cout << "New front element: " << q.front() << std::endl;
return 0;
}
常用操作
- push():将元素压入队尾。
- pop():移除队头元素。
- front() 和 back():访问队头和队尾元素。
2.5 选择合适的容器
选择合适的容器取决于应用场景的需求。以下是一些选择的建议:
- 随机访问:如果需要快速随机访问元素,使用 std::vector 或 std::deque。
- 频繁插入/删除:如果需要频繁在中间插入或删除元素,使用 std::list 或 std::deque。
- 唯一性与排序:如果需要存储唯一的元素,并且需要保持有序,使用 std::set 或 std::map。
- 哈希查找:如果需要快速查找和插入操作,使用 std::unordered_set 或 std::unordered_map。
- 栈/队列:如果需要后进先出或先进先出的数据结构,使用 std::stack 或 std::queue。