目录
一、STL的基础知识
1、STL简介
标准模板库(Standard Template Library,简称STL)定义了一套概念体系,为泛型程序设计提供了逻辑基础。STL中的各个类模板、函数模板的参数都是用这个体系中的概念来规定的。使用STL的模板时,类型参数既可以是C++标准库中已有的类型,也可以是自定义的类型——只要这些类型是所要求概念的模型。该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。
2、STL基本组成
通常认为,STL是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的,它们各自的含义下表所示。
STL的组成 | 含义 |
---|---|
容器 | 一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。 |
算法 | STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 <algorithm> 中,少部分位于头文件 <numeric> 中。 |
迭代器 | 在C++ STL中,对容器中数据的读写,是通过迭代器完成的,扮演着容器和算法间的胶合剂。 |
函数对象 | 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。 |
适配器 | 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。 |
内存分配器 | 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。 |
2.1 容器
STL中的容器有序列式容器和关联式容器,容器适配器(stack,queue,priority queue),位集(bit_set),串包(string_package)等等。
- 顺序容器:array(数组)、vector(向量)、deque(双端队列)、forward_list(单链表)、list(列表)
- 有序关联容器:set(集合)、multiset(多重集合)、map(映射)、multimap(多重映射)
- 无序关联容器:unordered_set (无序集合)、unordered_multiset(无序多重集合) 、unordered_map(无序映射)、unorder_multimap(无序多重映射)
标准容器类 | 特点 |
顺序性容器 | |
vector | 从后面快速的插入与删除,直接访问任何元素 |
deque | 从前面或后面快速的插入与删除,直接访问任何元素 |
list | 双链表,从任何地方快速插入与删除 |
关联容器 | |
set | 快速查找,不允许重复值 |
multiset | 快速查找,允许重复值 |
map | 一对多映射,基于关键字快速查找,不允许重复值 |
multimap | 一对多映射,基于关键字快速查找,允许重复值 |
容器适配器 | |
stack | 后进先出 |
queue | 先进先出 |
priority_queue | 最高优先级元素总是第一个出列 |
序列式容器:排列次序取决于插入时机和位置
关联式容器:排列顺序取决于特定准则
容器的通用功能
- 用默认构造函数构造空容器
- 支持关系运算符:==、!=、<、<=、>、>=
- begin()、end():获得容器首、尾迭代器
- cbegin() 、cend():获取容器首、尾常迭代器,不需要改变容器时更加安全
- clear():将容器清空
- empty():判断容器是否为空
- size():得到容器元素个数
- s1.swap(s2):将s1和s2两容器内容交换
2.2 迭代器
迭代器是算法和容器的桥梁
- 迭代器用作访问容器中的元素
- 算法不直接操作容器中的数据,而是通过迭代器间接操作
算法和容器独立
- 增加新的算法,无需影响容器的实现
- 增加新的容器,原有的算法也能适用
迭代器的分类
- 输入迭代器:可以用来从序列中读取数据,如输入流迭代器
- 输出迭代器:允许向序列中写入数据,如输出流迭代器
- 前向迭代器:既是输入迭代器又是输出迭代器,并且可以对序列进行单向的遍历
- 双向迭代器:与前向迭代器相似,但是在两个方向上都可以对数据遍历
- 随机访问迭代器:也是双向迭代器,但能够在序列中的任意两个位置之间进行跳转,如指针、使用vector的begin()、end()函数得到的迭代器
迭代器的区间
- 两个迭代器表示一个区间:[p1, p2)
- STL算法常以迭代器的区间作为输入,传递输入数据
- 合法的区间:p1经过n次(n > 0)自增(++)操作后满足p1 == p2
- 区间包含p1,但不包含p2
迭代器的辅助函数
- advance(p, n):对p执行n次自增操作
- distance(first, last):计算两个迭代器first和last的距离,即对first执行多少次“++”操作后能够使得first == last
对可逆容器的访问
STL为每个可逆容器都提供了逆向迭代器,逆向迭代器可以通过下面的成员函数得到:
- rbegin() :指向容器尾的逆向迭代器
- rend():指向容器首的逆向迭代器
逆向迭代器的类型名的表示方式如下(S表示容器类型):
- S::reverse_iterator:逆向迭代器类型
- S::const_reverse_iterator:逆向常迭代器类型
随机访问容器
随机访问容器支持对容器的元素进行随机访问
- s[n]:获得容器s的第n个元素
- vector和deque适用
二、应用 1:结合容器和迭代器解决序列变换和像素变换
1、常规方法实现序列变换,如取反、平方、立方
对于int整型的数组,我们可以实现如下的代码:
// 序列变换 ———— 取反
void transInv(int a[], int b[], int nNum){
for(int i = 0; i < nNum; i++){
b[i] = - a[i];
}
}
// 序列变换 ———— 平方
void transSqr(int a[], int b[], int nNum){
for(int i = 0; i < nNum; i++){
b[i] = a[i] * a[i];
}
}
// 序列变换 ———— 立方
void transCub(int a[], int b[], int nNum){
for(int i = 0; i < nNum; i++){
b[i] = a[i] * a[i] * a[i];
}
}
// 模板函数:自定义输出内容
template <typename T>
void OutPutCont(string strName, ostream& os, T begin, T end){
os<<strName<<": ";
for(; begin!=end; begin++){
os<<*begin<<"\t";
}
os<<endl;
}
代码测试:
int main()
{
const int N = 5;
int a[N] = {5, 2, 7, 0, 3};
int b[N], c[N], d[N];
transInv(a, b, N); //取反
transSqr(a, c, N); //平方
transCub(a, d, N); //立方
OutPutCont("数组a", cout, a, a + N);
OutPutCont("取反a", cout, b, b + N);
OutPutCont("平方a", cout, c, c + N);
OutPutCont("立方a", cout, d, d + N);
return 0;
}
测试结果:
2、特殊方法实现序列变换:模板函数
常规的方法不能代码的复用性不高,对于double型或者float型的数据,不能起到效果,所以我们可以模板函数来解决这个问题。
模板函数的相关知识可以参考我的上一章博客:C++ 程序设计 —— 实验三:模板_DreamWendy的博客-CSDN博客
// 序列变换 ———— 取反
template <typename T>
void transInvT(T a[], T b[], int nNum){
for(int i = 0; i < nNum; i++){
b[i] = - a[i];
}
}
// 序列变换 ———— 平方
template <typename T>
void transSqrT(T a[], T b[], int nNum){
for(int i = 0; i < nNum; i++){
b[i] = a[i] * a[i];
}
}
// 序列变换 ———— 立方
template <typename T>
void transCubT(T a[], T b[], int nNum){
for(int i = 0; i < nNum; i++){
b[i] = a[i] * a[i] * a[i];
}
}
代码测试:
int main()
{
const int N = 5;
float a[N] = {1.5, 2.70, 1.5, 0.7, 3.1};
float b[N], c[N], d[N]; //浮点型
transInvT(a, b, N); //取反
transSqrT(a, c, N); //平方
transCubT(a, d, N); //立方
OutPutCont("数组a", cout, a, a + N);
OutPutCont("取反a", cout, b, b + N);
OutPutCont("平方a", cout, c, c + N);
OutPutCont("立方a", cout, d, d + N);
return 0;
}
测试结果:
3、结合容器和迭代器解决序列变换
// 结合迭代器 ———— 取反
template <typename InputIter, typename OutputIter>
void transInvT(InputIter begInput, InputIter endInput, OutputIter begOutput){
for(; begInput != endInput; begInput++, begOutput++){
*begOutPut = ‐ (*begInput)
}
}
上面代码的缺点是对于平方、立方等操作都要写重写函数实现,复用性不好,下面进行改性:
// 取反操作
template<typename T>
T InvT(T a)
{
return -a;
}
// 平方操作
template<typename T>
T SqrT(T a)
{
return a * a;
}
// 立方操作
template<typename T>
T CubT(T a)
{
return a * a * a;
}
// 结合容器和迭代器解决序列变换
template <typename InputIter, typename OutputIter, typename MyOperator>
void transfrom(InputIter begInput, InputIter endInput, OutputIter begOutput, MyOperator op){
for(; begInput != endInput; begInput++, begOutput++){
*begOutput = op(*begInput); // 序列变换,改写为函数的形式
}
}
代码测试:
int main()
{
const int N = 5;
int a[N] = {5, 2, 7, 1, 4}; //整型数组
int b[N];
vector<double> vb(N); //vector(向量)容器
OutPutCont("数组a", cout, a, a+N); //输出数组a
transfrom(a, a+N, b, InvT<int>); //通过迭代器取反数组a
OutPutCont("通过迭代器取反数组 a", cout, b, b + N);
transfrom(a, a+N, vb.begin(), InvT<int>); //结合容器和迭代器取反数组a
OutPutCont("结合容器和迭代器取反", cout, vb.begin(), vb.end());
transfrom(a, a+N, b, SqrT<int>); //通过迭代器平方数组a
OutPutCont("通过迭代器平方数组 a", cout, b, b + N);
transfrom(a, a+N, vb.begin(), SqrT<int>); //结合容器和迭代器平方数组a
OutPutCont("结合容器和迭代器平方", cout, vb.begin(), vb.end());
transfrom(a, a+N, b, CubT<int>); //通过迭代器立方数组a
OutPutCont("通过迭代器立方数组 a", cout, b, b + N);
transfrom(a, a+N, vb.begin(), CubT<int>); //结合容器和迭代器立方数组a
OutPutCont("结合容器和迭代器立方", cout, vb.begin(), vb.end());
return 0;
}
测试结果:
上述的transform算法,顺序遍历begInput和endInput两个迭代器所指向的元素;将每个元素的值作为函数对象op的参数;将op的返回值通过迭代器begOutput顺序输出;遍历完成后begOutput迭代器指向的是输出的最后一个元素的下一个位置,transform会将该迭代器返回,并存储在vector 容器中。
4、结合容器和迭代器解决像素变换
// 像素变换 ———— 二值化
template<typename T>
class MyThreshold{
public:
MyThreshold(int n = 128):_nThreshold(n){} //参数时默认为128,列表初始化成员变量
int operator()(T val)
{
return val<_nThreshold ? 0 : 1; //_nThreshold设定阈值为n,val小于阈值返回0,否则返回1
}
int _nThreshold;
};
// 像素变换 ———— 灰度拉伸
template<typename T>
class MyGrayTrans{
public:
MyGrayTrans(int n = 128):nGrayTrans(n){}
int operator()(T val){
return val += nGrayTrans;
}
int nGrayTrans;
};
代码测试:
int main()
const int N = 5;
int a[N] = {1, 0, 5, 2, 4}; //整型数组
vector<double> vb(N); //vector(向量)容器
OutPutCont(" 数组 a ", cout, a, a+N); //输出数组a
transfrom(a, a+N, vb.begin(), MyThreshold<int>(2)); //像素变换——二值化
OutPutCont(" 二值化 ", cout, vb.begin(), vb.end());
transfrom(a, a+N, vb.begin(), MyGrayTrans<int>(2)); //像素变换——灰度拉伸
OutPutCont("灰度拉伸", cout, vb.begin(), vb.end());
return 0;
}
测试结果:
把大于某个临界灰度值的像素灰度设为灰度極大值,把小于这个值的像素灰度设为灰度極小值,从而实现二值化,测试结果把阈值设为2,小于2的值变为0,大于等于2的值变为1。
5、算法和函数对象的使用
如下示例sort排序算法:对给定区间所有元素进行排序,默认为升序,也可进行降序排序。sort函数进行排序的时间复杂度为n*log2n,比冒泡之类的排序算法效率要高,sort函数包含在头文件为#include<algorithm>的c++标准库中。
语法
sort(start,end,cmp)
参数
(1)start表示要排序数组的起始地址;
(2)end表示数组结束地址的下一位;
(3)cmp用于规定排序的方法,可不填,默认升序。既可以是函数也可以是函数对象,但是必须带有具有双目运算符的比较函数即操作符(),且返回类型为bool
功能
sort函数用于C++中,对给定区间所有元素进行排序,默认为升序,也可进行降序排序。
一般是直接对数组进行排序,例如对数组a[10]排序,sort(a,a+10)。而sort函数的强大之处在可与cmp函数结合使用,即排序方法的选择。
库函数functional中定义了类greater,函数对象类greater中的定义了调用操作符(),可用作于cmp。
// 比较函数 ———— 降序
template<typename T>
bool MyComp(T a, T b){
return a > b;
}
// 比较类
template<typename T>
class CMyComp{
public:
bool operator()(const T& x, const T& y) const{
return x > y;
}
};
测试代码:
int main(){
const int N = 5;
int a[N] = {1, 0, 5, 2, 4}; //整型数组a
int b[N] = {1, 2, 3, 7, 0}; //整型数组b
int c[N] = {0, 1, 5, 4, 0}; //整型数组c
OutPutCont("数组 a", cout, a, a+N); //输出数组a
sort(a, a+N, MyComp<int>); //调用模板函数排序
OutPutCont("排序后", cout, a, a+N); //输出排序后的数组a
OutPutCont("数组 b", cout, b, b+N); //输出数组b
sort(b, b + N, CMyComp<int>()); //调用自定义模板类排序
OutPutCont("排序后", cout, b, b+N); //输出数组b
OutPutCont("数组 c", cout, c, c+N); //输出数组b
sort(c, c+N, greater<int>()); //调用functional库中模板类排序
OutPutCont("排序后", cout, c, c+N); //输出数组b
return 0;
}
测试结果:
(二) 应用 2:用set存储学生信息,实现增删改查操作
set集合是c++ stl库中自带的一个容器,set具有以下两个特点:
- set中的元素都是排好序的
- set集合中没有重复的元素
常用操作:
begin() 返回set容器的第一个元素的地址
end() 返回set容器的最后一个元素地址
clear() 删除set容器中的所有的元素
empty() 判断set容器是否为空
max_size() 返回set容器可能包含的元素最大个数
size() 返回当前set容器中的元素个数
erase(it) 删除迭代器指针it处元素
insert() 插入某个元素
1、构建学生信息类StudentInfo
// 学生信息类
class StudentInfo{
public:
string c_strNo; //学号
string c_strName; //姓名
StudentInfo(string strNo, string strName){ //构造函数
c_strNo = strNo;
c_strName = strName;
}
// 友元函数,运算符重载————输出
friend ostream& operator<<(ostream& os, const StudentInfo& info){
os << info.c_strNo << info.c_strName;
return os;
}
// 友元函数,运算符重载————比较
friend bool operator<(const StudentInfo& info1, const StudentInfo& info2){
return info1.c_strNo<info2.c_strNo;
}
};
2、使用set容器存储学生信息
创建类型为vector的容器students,通过push_back的方法把每一个学生信息StudentInfo加入到vector容器中,再遍历vector容器将全部的学生信息存储在set容器中,这样学生信息有序排列。
// 存储学生信息
void saveStudentInfo(){
vector<StudentInfo> students; //创建vector容器,存储学生信息
students.push_back(StudentInfo("10011", "Pei Ting")); //添加学生信息
students.push_back(StudentInfo("10001", "Wang Yan"));
students.push_back(StudentInfo("10051", "Zhen Hao"));
students.push_back(StudentInfo("10037", "xin Yun"));
students.push_back(StudentInfo("10025", "Hua Li"));
// 创建set容器,存储学生信息
set<StudentInfo> StudentSet(students.begin(), students.end());
OutPutCont("Student Set", cout, StudentSet.begin(), StudentSet.end());
}
测试结果:学生信息已经排序输出了
3、对学生信息进行增删改查操作
3.1 增添学生信息
//增添学生信息————插入后仍是有序排列的
StudentSet.insert(StudentInfo("10032", "Coco Bela"));
3.2 删除学生信息
//删除学生信息
StudentSet.erase(StudentInfo("10051", "Zhen Hao"));
3.3 修改学生信息
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
set类型的迭代器有const修饰符,所以不能直接修改元素,只能先删除旧的,创建新的来实现
//修改学生姓名
bool updateStuName(set<StudentInfo>& StudentSet, string strNameOld, string strNameNew){
string strNo; //用于保存,需要修改姓名的学生的学号
bool result = false; //返回是修改成功
for(auto it = StudentSet.begin(); it != StudentSet.end(); it++){
if((*it).c_strName == strNameOld)
{
strNo = (*it).c_strNo; //保存学号
StudentSet.erase(*it); //先删除旧的学生信息
StudentSet.insert(StudentInfo(strNo,strNameNew));//再重新添加新的学生信息
result = true;
break;
}
}
return result;
}
测试代码:
updateStuName(StudentSet, "Xin Yun", "Xin Yong"); //修改学生信息
OutPutCont("Student Set:After update", cout, StudentSet.begin(), StudentSet.end());
3.4 查找学生信息
① set自带的find函数
find(val) | 在 set 容器中查找值为 val 的元素,如果成功找到,则返回指向该元素的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
// 查找学生信息
if(StudentSet.find(StudentInfo("10001", "Wang Yan")) != StudentSet.end()){
cout << "Find The Student! "<< endl;
}else{
cout << "Not Find The Student! "<< endl;
}
② 自定义查找
// 按学号查找学生系信息
template <typename T>
void searStuNo(string strNo, ostream &os, T begin, T end)
{
for(; begin !=end; begin++)
{
if((*begin).c_strNo == strNo)
{
os<<"Find The Student: "<< *begin << endl;
}
}
}
(三) 应用 3:利用map字典统计字符频数并输出
输入一个字符串,用map字典统计每个字符出现的次数并输出字符及对应的次数。
map是STL的一个关联容器,它提供一对一的hash。
- 第一个可以称为关键字(key),每个关键字只能在map中出现一次;
- 第二个可能称为该关键字的值(value);
映射与集合同属于单重关联容器,它们的主要区别在于,集合的元素类型是键本身,而映射的元素类型是由键和附加数据所构成的二元组。 在集合中按照键查找一个元素时,一般只是用来确定这个元素是否存在,而在映射中按照键查找一个元素时,除了能确定它的存在性外,还可以得到相应的附加数据。
// 统计字符串的字符频数
void strCounts(){
map<char, int> strMap; //创建字典,关键字为char类型,存储字符,键值为int类型,存储字符出现的次数
string str;
cout << "请输入字符串:";
cin >> str;
int len = str.length();
for(int i = 0; i < len; i++){
strMap[str[i]]++;
}
// 遍历输出
for(map<char, int>::iterator it = strMap.begin(); it != strMap.end(); it++){
cout << "字符:" <<it->first << "\t" << "频数:" << it->second << endl;
}
}
(四) 实验总结
本次实验,学习了解了STL——C++的一个标准模板库,实验过程中可以明显体会到STL 具有高可重用性。常用到的STL容器有vector、list、deque、map、set等等,它们有各自特点,如下:
- vector: 底层数据结构为数组 ,支持快速随机访问。
- list: 底层数据结构为双向链表,支持快速增删。
- deque: 底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问。
- set: 底层数据结构为红黑树,有序,不重复。
- map: 底层数据结构为红黑树,有序,不重复。
在实际使用过程中,选择哪容器,可根据以下选择:
- 如果需要高效的随机存取,不在乎插入和删除的效率,使用vector;
- 如果需要大量的插入和删除元素,不关心随机存取的效率,使用list;
- 如果需要随机存取,并且关心两端数据的插入和删除效率,使用deque;
- 如果打算存储数据字典,并且要求方便地根据key找到value,一对一的情况使用map,一对多的情况使用multimap;
- 如果打算查找一个元素是否存在于某集合中,唯一存在的情况使用set,不唯一存在的情况使用multiset。