【知识点】深入浅出STL标准模板库
前几天谈论了许多关于数论和数据结构的东西,这些内容可能对初学者而言比较晦涩难懂(毕竟是属于初高等算法/数据结构的范畴了)。今天打算来讲一些简单的内容 - STL 标准模板库。
STL 标准模板库
C++ 标准模板库 (Standard Template Library, STL),是 C++ 语言非常重要的一个构成部分。它提供了一组通用的类和函数,用于实现一些数据结构和算法。STL 主要包含以下五个部分:
- 容器 (Containers):STL 提供了不同的容器来供开发者根据所需要维护数据来选择存储。
- 算法 (Algorithms):STL 提供了许多通用算法,用于排序、搜索、复制、修改等操作。
- 迭代器 (Iterators):STL 迭代器的作用是遍历容器元素的对象。
- 函数对象 (Function Objects):这是一种行为类似于函数的对象(由于不太常见,因此本篇文章将不会详细讲解 STL 中的函数对象)。
- 适配器 (Adapters):STL 的适配器用于修改容器、迭代器或函数对象的接口。为方便起见,容器适配器将会与容器部分一同讲解。
综上所述,本文将会主要围绕 容器、算法和迭代器这三者来展开叙述。本文只会阐述一些相对常见的模板/方法,部分生僻的内容还请各位自行上网搜索。
容器 Containers
STL 容器还可以被细分成三个类别,分别是 序列式容器 (Sequence Containers)、关联式容器 (Associative Containers) 和 无序关联式容器 (Unordered Associative Containers)。
序列式容器 (Sequence Containers) 与常见容器适配器。
vector
向量容器:向量容器可以被理解为我们常说的动态数组。与普通数组不同的是,动态数组的大小可以在运行过程中更改,并且用户可以任意地在此数组的末尾添加数据。它的优点是支持快速随机访问和在末尾插入删除元素。向量容器的基本使用方法如下:
#include <iostream>
#include <vector> // 引入头文件
using namespace std;
int main(){
// 创建一个整数类型,名为 vec 的向量容器,初始存放了数字1-5。
vector<int> vec = {1, 2, 3, 4, 5};
vec.push_back(6); // 向容器末尾添加一个元素6。
vec.size(); // 获取容器的大小,即容器内元素的个数。
vec.resize(10); // 动态更改容器的大小,将容器的大小设置为10。
// 通过 for 循环对容器进行遍历:
for (int i=0; i<vec.size(); i++){}
for (int i : vec) {}
return 0;
}
queue
队列容器适配器:队列是一种基本的数据结构,其讲究 先进先出 (FIFO),即最先添加进适配器的元素会被第一个弹出,以此类推。在队列中,所有元素都会从队尾入队,并从对首出队。队列适配器的优点是支持随机访问队首和队尾元素。队列容器严格意义上并非序列式容器,而是一种容器适配器。队列容器适配器的基本使用方法如下:
#include <iostream>
#include <queue> // 引入头文件
using namespace std;
int main(){
// 创建一个存放整数类型,名为 que 的队列容器适配器,一开始默认为空。
queue<int> que;
que.push(10); // 向队尾添加一个新元素。
que.front(); // 获取队首元素,此时队首元素为数字10。
que.pop(); // 将队首元素出队,此时的队列容器为空。
que.size(); // 获取队列的长度,即对内元素的个数。
que.empty(); // 判断队列是否为空,为空返回真,否则返回假。
// 队列元素不支持直接遍历,只能从头一个一个弹出。
// 在弹出元素时需要保证队内不为空。
while (!que.empty()){
int t = que.front();
que.pop();
cout << t << endl;
}
return 0;
}
stack
栈容器适配器:栈也是一种基本的数据结构,栈实现的是 后进先出 (LIFO),即最后添加进该适配器的元素会最先弹出。在栈中,所有元素都会从栈顶入栈,并从栈顶出栈。栈容器严格意义上并非序列式容器,而是一种容器适配器。栈容器适配器的基本使用方法如下:
#include <iostream>
#include <stack> // 引入头文件
using namespace std;
int main(){
// 创建一个存放整数类型,名为 s 的栈容器适配器,一开始默认为空。
stack<int> s;
s.push(10); // 将元素压入栈顶。
s.top(); // 获取栈顶元素,此时的栈顶元素为数字10。
s.pop(); // 将栈顶元素出栈,此时栈内没有元素。
s.size(); // 获取栈内元素的个数。
s.empty(); // 判断栈是否为空,为空返回真,否则返回假。
// 与队列相同,栈内元素不支持直接遍历,只能从栈顶一个一个弹出。
// 在弹出元素时需要保证栈内不为空。
while (!s.empty()){
int t = s.top();
s.pop();
cout << t << endl;
}
return 0;
}
deque
双端队列容器:与队列类似,双端对类容器支持快速随机访问和在两端插入删除。上面提及到的queue
容器适配器就是基于双端队列所实现的。双端队列容器的基本使用方法如下:
#include <iostream>
#include <deque>
int main() {
// 创建一个整数类型,名为 deq 的双端队列容器,初始存放了数字1-5。
deque<int> deq = {1, 2, 3, 4, 5};
deq.push_front(0); // 在前端添加元素。
deq.push_back(6); // 在末尾添加元素。
// 通过 for 循环对容器进行遍历:
for (int i : deq) cout << i << endl;
return 0;
}
list
双向链表容器:链表数据结构支持快速插入删除操作,但是通过索引访问元素会比较慢。双向链表的语法与双端队列相似。在一般算法实现上,双向链表的应用并不算广泛,因此不过多阐述。双向链表的基本使用方法如下:
#include <iostream>
#include <list> // 引入头文件
int main() {
// 创建一个整数类型,名为 lst 的双向队列容器,初始存放了数字1-5。
list<int> lst = {1, 2, 3, 4, 5};
lst.push_front(0); // 在前端添加元素。
lst.push_back(6); // 在末尾添加元素。
// 通过 for 循环对容器进行遍历:
for (int i : lst) cout << i << endl;
return 0;
}
关联式容器 (Associative Containers)
set
集合容器:集合容器的定义与数学中的集合完全相同。集合中相同的元素只会被存入一次。集合可以进行高效的查找操作。集合容器的基本使用方法如下:
#include <iostream>
#include <set> // 引入头文件
int main() {
// 创建一个整数类型,名为 s 的集合容器,初始存放了许多数字。
set<int> s = {3, 1, 4, 1, 5, 9};
s.insert(2); // 向集合内插入元素2。
s.erase(2); // 向集合中移除元素2。
s.count(2); // 检查元素是否存在于集合,如果返回真即存在,否则即不存在。
s.size(); // 获取集合内元素的个数。
s.clear(); // 清空集合。
// 通过 for 循环对容器进行遍历:
// 由于集合自带“去重”功能,最输出的结果为:1 3 4 5 9。
for (auto i : s) cout << i << " ";
return 0;
}
map
映射容器:除了集合容器之外,该容器也经常在算法中被应用。映射容器用于存储键值对。其中键在该容器中是唯一,同时它还支持高效的查找操作(对于某些数据如果懒得离散化一定要试一下这个容器,我觉得真的很好用)。以下是映射容器的基本用法:
#include <iostream>
#include <map> // 引入头文件
int main() {
// 创建一个键为整数类型、值为字符串类型,名为 m 的映射容器。
map<int, string> m;
m[1] = "Macw07"; // 插入键值对。
s.count(2); // 检查元素是否存在于集合,如果返回真即存在,否则即不存在。
s.size(); // 获取集合内元素的个数。
return 0;
}
无序关联式容器 (Unordered Associative Containers)
一般情况下,所有的关联式容器默认都会按照存放数组(值/键)从小到大进行排序。但如果将这些关联式容器变成无序关联式容器,那么 STL 内部就不会默认对其进行排序。一般情况下,如果没有特殊需求推荐使用无序关联式容器,因为它们的运行效率通常都比有序的关联式容器要高很多。
以下是常见的无序关联式容器和有序关联式容器的声明代码:
map
与 unordered_map
:
#include <map> // 有序的。
#include <unordered_map> // 无序的。
map<int, int> mp1; // 有序映射表的声明。
unordered_map<int, int> mp2; // 无序映射表的声明。
set
与 unordered_set
:
#include <set> // 有序的。
#include <unordered_set> // 无序的。
set<int> mp1; // 有序集合的声明。
unordered_set<int> mp2; // 无序集合的声明。
算法 Algorithms
终于讲完了容器部分,接下来来到了算法的部分。C++ 的 STL 供了许多算法,用于对容器中的元素进行操作。这些算法主要包括:排序、搜索、修改、集合操作、数值操作等。STL 的算法通常以函数模板的形式实现,可以与任意容器类型配合使用。
以下所有有关区间的内容均默认为 闭区间操作。
- 排序算法
sort
与stable_sort
。两种算法的目的都是对一个容器/数组进行排序。两个算法均传入三个参数,分别是排序区间的头尾地址和排序方式。其中排序方式不强制传参,对于整形类型的容器而言,默认按照从小到大的顺序进行排序。两种算法的作用基本相同,但stable_sort
保证了排序的稳定性,普通的sort
无法保证排序的稳定性。基本使用代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> vec = {5, 3, 8, 6, 2};
int arr[] = {5, 4, 3, 2, 1};
// 前两个参数传入的是地址。
// xxx.begin()/ end() 表示获取某容器的首地址或尾地址。
sort(vec.begin(), vec.end()); // 对动态数组进行排序。
sort(arr, arr+5); // 对数组进行排序。
// 输出排序后的动态数组。
for (int i : vec) cout << n << " ";
return 0;
}
如果想要使用自定义排序,那么可以仿照以下方法来写:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 创建一个自定义比较函数。
// 传入两个参数(类型与所需要排序的容器类型一致)。
// 说直白点,想要哪个前一个参数考前就返回真,否则返回假。
bool cmp(int a, int b){
return a > b; // 按照从大到小的顺序排。
// return a < b; // 按照从小到大的顺序排。
}
int main() {
vector<int> vec = {5, 3, 8, 6, 2};
sort(vec.begin(), vec.end(), cmp); // 对动态数组按照cmp规则进行排序。
// 输出排序后的动态数组。
for (int i : vec) cout << n << " ";
return 0;
}
- 搜索算法
find
:该算法帮助我们在指定范围内查找等于指定值的第一个元素。该算法也传入三个参数,也分别是查找区间的左地址和右地址,以及需要查找的对象。该算法的返回值也是一个地址。具体的使用方法如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> vec = {5, 3, 8, 6, 2};
// 如需要获取所查找对象的索引,因在最后减去首地址。
auto index = std::find(vec.begin(), vec.end(), 8) - vec.begin();
return 0;
}
- 集合操作算法
set_union
和set_intersection
:之前已经提到了集合的容器,这两个算法均应用在集合容器(当然其他容器也可以)当中。其中set_union
算法会将两个集合合并。set_intersection
算法可以求出两个集合的交集。这两个算法均传入五个参数,分别是第一个集合的左右地址、第二个集合的左右地址和存放最终计算结果的容器。两个函数的使用方法如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> vec1 = {1, 2, 3};
vector<int> vec2 = {3, 4, 5};
vector<int> unionVec, intersectionVec;
// 计算 vec1 和 vec2 的并集,并将结果存储到 unionVec 数组中。
set_union(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), back_inserter(unionVec));
// 计算 vec1 和 vec2 的交集,并将结果存储到 intersectionVec 数组中。
set_union(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), back_inserter(intersectionVec));
return 0;
}
- 反转算法
reverse
:反转一个容器(数组/字符串也可以)指定范围内的元素顺序。该算法传入两个参数,分别表示区间的左右地址。该算法具体的使用方法如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 4, 5};
// 反转整一个 vec 数组
reverse(vec.begin(), vec.end()); // 此时 vec = {5, 4, 3, 2, 1}
string str = "Macw07";
reverse(str.begin(), str.end()); // 此时 str = "70wcaM"。
return 0;
}
- 去重算法
unique
和erase
:unique
算法用于移除指定范围内相邻的重复元素(需要先排序)。该算法只能在保证容器内有序地情况下使用,否则算法将无法返回正确的结果。该算法的会将重复的元素存放到区间的末尾,并返回一个指针,表示从该指针开始到区间结束是重复元素所在的区间。而erase
的用处是删除容器指定区间内的所有元素。相同地,两个算法都需传入两个参数,分别表示区间的左右地址。该算法具体的使用方法如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> vec = {1, 2, 2, 3, 4, 4, 5};
// 去重。
auto last = unique(vec.begin(), vec.end());
// 删除重复元素。
vec.erase(last, vec.end());
return 0;
}
- 查找算法
lower_bound
和upper_bound
:lower_bound
和upper_bound
是两种常用的搜索算法,主要用于有序范围内查找元素。它们分别返回 第一个不小于(大于或等于)指定值 和 第一个大于指定值的位置 。请注意,这两个算法需要在给定区间内的元素从小到大升序的情况下使用,否则将会返回错误的结果。这两个算法的底层原理是通过二分查找来实现的。假设存在一个数列 \([1, 2, 3, 5, 6, 6, 7]\),那么这个数列对 \(4\) 求 \(lower\_bound\) 就是 \(3\)(索引),而这个数列对 \(6\) 求 \(upper\_bound\) 就是 \(6\)(索引)两个算法的代码示例如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 4, 5, 6};
// ---------- lower_bound 算法 ----------
auto it1 = lower_bound(vec.begin(), vec.end(), 4);
// 如果要获取索引,那么再减去首地址即可。
int index1 = lower_bound(vec.begin(), vec.end(), 4) - vec.begin();
if (it1 != vec.end())
cout << "Lower bound of 4 is: " << *it1 << endl;
else
cout << "4 is not in the vector" << endl;
// ---------- upper_bound 算法 ----------
auto it2 = upper_bound(vec.begin(), vec.end(), 4);
// 如果要获取索引,那么再减去首地址即可。
int index2 = upper_bound(vec.begin(), vec.end(), 4) - vec.begin();
if (it2 != vec.end())
cout << "Upper bound of 4 is: " << *it2 << endl;
else
cout << "All elements are <= 4" << endl;
return 0;
}
迭代器 Iterators
接下来来到了迭代器部分,迭代器是一种用于遍历容器元素的抽象概念。它提供了一种统一的访问容器内元素的方式,使得算法可以独立于容器类型工作,增强了代码的通用性和可重用性。
迭代器的基本概念
迭代器 是一种指针-like 对象(表示与指针类似),它允许你遍历容器中的元素。你可以使用迭代器访问容器的每个元素,而不需要了解容器的内部实现细节。
迭代器范围 由两个迭代器表示,通常是一个指向范围起始位置的迭代器和一个指向范围末尾位置的迭代器。这两个迭代器标识了一个半开区间,即左闭右开。
虽然迭代器与指针近乎相同,但也有细微的差别(在本文前问中,由于未详细讲解迭代器,因此将迭代器和指针统称为指针)。以下是迭代器的常见方法:
.begin()
:获取指向容器的第一个元素的迭代器。.end()
:获取指向容器的最后一个元素的下一个位置的迭代器。迭代器++
:将迭代器指向容器的下一个位置。*迭代器
:获取迭代器指向的元素(类似于通过地址取值)。
一般情况下,我们通过使用 auto
类型来实现迭代器(因为这个类型比较方面,可以在任何时候用)。auto
类型是 C++11 引入的一个关键字,用于在编译时自动推断变量的类型。使用 auto
关键字声明的变量会根据其初始化表达式的类型自动推导出变量的类型。这样做可以简化代码、提高可读性,并且使代码更具灵活性
通过使用 auto
类型来对 STL 容器进行遍历操作:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 4, 5};
// 使用迭代器遍历容器并输出元素。
for (auto it = vec.begin(); it != vec.end(); it++)
cout << *it << " ";
return 0;
}
小结
至此,对于 STL 的基本讲解就到此为止了。STL 标准模板库 本身是非常庞大的,因此本文并不涵盖所有的知识点。与此同时,我希望各位在使用不同的算法/容器的时候自行搜索有关该模板的时间复杂度,以更好的在做题过程中预估算法的整体耗时。
本文后续可能会持续更新。
【知识点】深入浅出STL标准模板库的更多相关文章
- STL标准模板库(简介)
标准模板库(STL,Standard Template Library)是C++标准库的重要组成部分,包含了诸多在计算机科学领域里所常见的基本数据结构和基本算法,为广大C++程序员提供了一个可扩展的应 ...
- STL标准模板库介绍
1. STL介绍 标准模板库STL是当今每个从事C++编程的人需要掌握的技术,所有很有必要总结下 本文将介绍STL并探讨它的三个主要概念:容器.迭代器.算法. STL的最大特点就是: 数据结构和算法的 ...
- STL学习系列一:STL(标准模板库)理论基础
STL(Standard Template Library,标准模板库)是惠普实验室开发的一系列软件的统称.现然主要出现在C++中,但在被引入C++之前该技术就已经存在了很长的一段时间. STL的从广 ...
- STL(标准模板库)理论基础,容器,迭代器,算法
基本概念 STL(Standard Template Library,标准模板库)是惠普实验室开发的一系列软件的统称.现然主要出现在C++中,但在被引入C++之前该技术就已经存在了很长的一段时间. ...
- STL(标准模板库)基本概念
一.什么是STL STL(Standard Template Library,标准模板库)的从广义上讲分为三类:algorithm(算法).container(容器)和iterator(迭代器),容器 ...
- C++ Templates STL标准模板库的基本概念
STL标准库包括几个重要的组件:容器.迭代器和算法.迭代器iterator,用来在一个对象群集的元素上进行遍历操作.这个对象群集或许是一个容器,或许是容器的一部分.迭代器的主要好处是,为所有的容器提供 ...
- STL标准模板库 向量容器(vector)
向量容器使用动态数组存储.管理对象.因为数组是一个随机访问数据结构,所以可以随机访问向量中的元素.在数组中间或是开始处插入一个元素是费时的,特别是在数组非常大的时候更是如此.然而在数组末端插入元素却很 ...
- STL标准模板库之vector
目录 vector容器 1)什么是vector 2)如何定义 3)常用的Vector函数 1.容量函数 2.增加函数 3.删除函数 4.迭代器 5.访问函数 6.其他函数及操作 7.算法 STL提供了 ...
- STL(标准模板库) 中栈(stack)的使用方法
STL 中栈的使用方法(stack) 基本操作: stack.push(x) 将x加入栈stack中,即入栈操作 stack.pop() 出栈操作(删除栈顶),只是出栈,没有返回值 stack.t ...
- STL 标准模板库
<vector> 可变长的数组 Vector<int>v int是一个模板参数,这样传进来的都会是int V.push_back(a)将a传进v,且放在最后一个 V.clear ...
随机推荐
- 1、android Studio 打Jar包
1.创建一个AndroidStudio 工程 注意下面这个Package Name 2.进入Android Studio工程中 选择Project Flies 选项 然后找到 app->src- ...
- k8s之持久卷NFS
一.简介 NFS网络存储卷,Kubernetes原生支持NFS作为Kubernetes的持久存储卷之一.NFS可以实现Pod的跨界点的数据持久性. 首先需要创建一个nfs 服务器,作为存储服务器: 将 ...
- Python根据主播直播时间段判定订单销售额归属
写在前面:最近在群里看到一个这样的直播电商的场景觉得还是挺有趣的,于是就想用Python来实现. 需求描述:根据主播直播时间段结合销售订单的付款时间判断所属销售的归属 生成主播在线直播时间段数据 fr ...
- Spring Cloud Alibaba 官方实践指南【文章有点长自备咖啡茶点】
注:本文内容均转自官方文档,方便胖友们学习,不代表博主观点. 链接地址:SpringCloudAlibaba | Spring Cloud Alibaba 基于网关实现 SpringCloud 服务发 ...
- 重新整理.net core 计1400篇[五] (.net core 修改为Startup模式 )
前言 随着不断的升级改版,我们离dotnet帮我们生成的文件中还差一步,那就是我们少了一个Startup,那么这个有什么用呢?让我们来补上it吧. 在此之前需要明白一件事,那就是Startup是一种约 ...
- CPVT:美团提出动态位置编码,让ViT的输入更灵活 | ICLR 2023
论文提出了一种新的ViT位置编码CPE,基于每个token的局部邻域信息动态地生成对应位置编码.CPE由卷积实现,使得模型融合CNN和Transfomer的优点,不仅可以处理较长的输入序列,也可以在视 ...
- python异步正则字符串替换,asyncio异步正则字符串替换re
自然语言处理经常使用re正则模块进行字符串替换,但是文本数量特别大的时候,需要跑很久,这就需要使用asyncio异步加速处理 import pandas as pd import re import ...
- 【走进RDS】之SQL Server性能诊断案例分析
简介: 数据库性能诊断不仅对其数据库技能要求较高,而且需要大量的前期准备工作,如收集各种性能基线.性能指标和慢SQL日志等,尤其是面对多数据库性能调优时,往往事倍功半. 客户的困扰 前几天某程序员小王 ...
- 【全观测系列】Elasticsearch应用性能监控最佳实践
简介:本文介绍了应用性能监控的应用价值以及解决方案等. 1.什么是全观测? 要了解全观测,我们先看看传统运维存在哪些问题. 数据孤岛,分散在不同部门,分析排查故障困难: 多个厂商的多种工具,无法自动 ...
- [Py] Python dict 倒序操作
倒序操作很简单,使用 reversed( ) 方法,原本是 ['a', 'b', 'c'],倒序后就是 ['c', 'b', 'a'] Ref:在线运行Python代码 Refer:Python di ...