标准模板库--STL库

基本概念

STL六大组件

容器
随机访问

可以随机访问即是无需遍历容器,通过下标 / 索引直接定位到容器中任意位置的元素,访问耗时恒定。

换句话说,可以随机访问要么元素本身连续(vector,string,array),要么有一个连续的索引表(deque)

算法
迭代器
迭代器头文件--#include <iterator>
next函数

迭代器中,某些容器可以通过it+数字来快速访问当前下标的元素,如vector、deque、array、string,这是因为这些容器内存是连续的,像数组一样,支持快速跳转。

复制代码
vector<int> v;
it = v.begin();
it = it + 3; //现在it相当是v容器中第四个元素的迭代器

但是某些容器不支持这样操作,如set、map等,其所支持的访问如下:

复制代码
#include <iostream>
#include <set>
#include <iterator>   // 用于 next
using namespace std;

int main() {
    set<int> s = {10, 20, 30, 40, 50};

    // 删除第 3 个元素
    auto it = next(s.begin(), 2);
    s.erase(it);

    for (int x : s) cout << x << " ";
    // 输出:10 20 40 50
    return 0;
}

同时next函数几乎支持所有STL库的容器的迭代。当你只需要最后一个元素时,也可以直接使用prev(s.end())

复制代码
std::prev(s.end()) 的意思就是 "指向容器中最后一个元素的迭代器"

string容器--#include<string>

本质概念
构造函数
复制代码
string s1;

const char* s = " Hello,world";
string s2(s);
cout << "s2 = " << s2 << endl;

string s3(s2);
cout << "s3 = " << s3 << endl;

string s4(10, 'a');
cout << "s4 = " << s4 << endl;

//s2 = Hello,world
//s3 = Hello,world
//s4 = aaaaaaaaaa        //10个a
赋值操作
追加操作
查找和替换操作

find和rfind查找后都是返回数组中元素的下标,当找不到字符串时返回-1

其中find是从左往右(第一位到最后一位),而rfind是从右往左(最后一位到第一位),但是都是返回需要查找的字符串符合要求的左边字符的下标,如下所示

比较操作

比较字符串的AC码,等于返回0,大于返回1,小于返回-1

字符存取操作
插入和删除操作
子串获取操作

vector容器--#include<vector>

构造函数
赋值操作
容量和大小
迭代器--iterator

vector<T>::iterator itBegin = v.begin()

vector<T>::iterator itEnd = v.end()

复制代码
vector<string> v;

//iterator为vector作用域下的迭代器
//begin为起始迭代器,指向容器第一个元素
vector<string>::iterator itBegin = v.begin();
//begin为结束迭代器,指向容器最后一个元素的下一个位置
vector<string>::iterator itEnd = v.end();
遍历方式
while循环
复制代码
vector<string>::iterator itBegin = v.begin();
vector<string>::iterator itEnd = v.end();
while (itBegin != itEnd) {
	cout << *itBegin << endl;
	itBegin++;
}
for循环
复制代码
for (vector<string>::iterator it = v.begin();it != v.end(); it++) {
	cout << *it << endl;
}

对于自定义的数据类型,比如类或结构体等,*it表示的数据类型和赋值给vector容器的数据类型相同。

STL提供的遍历算法
复制代码
void myPrint(string val) {
	cout << val << endl;
}

for_each(v.begin(), v.end(), myPrint);

这个方法需要自定义一个函数,同时包含头文件<algorithm>

在自定义函数中,输出的数据与前面的输出不同,不使用指针传输,因此for_each内部的传输自带指针

复制代码
_EXPORT_STD template <class _InIt, class _Fn>
_CONSTEXPR20 _Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) {
    _STD _Adl_verify_range(_First, _Last);
    auto _UFirst      = _STD _Get_unwrapped(_First);
    const auto _ULast = _STD _Get_unwrapped(_Last);
    for (; _UFirst != _ULast; ++_UFirst) {
        _Func(*_UFirst);
    }

    return _Func;
}

可以看出for_each的内部也是使用的for循环输出指针,并且使用的是模板传入,以适配更多的数据类型。

const修饰迭加器

当你不想要外部修改容器中的值时,那么就需要使用const_iterator迭加器

如上述代码,除了将迭加器修改为const_iterator以外,如果使用函数传入实参,也需要在传入时使用const修饰。

vector容器嵌套容器
复制代码
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void test() {
	vector<vector<int>> v;
	
	vector<int> v1;
	vector<int> v2;

	v1.push_back(10);
	v1.push_back(20);
	v2.push_back(30);
	v2.push_back(40);

	v.push_back(v1);
	v.push_back(v2);

	for (vector<vector<int>>::iterator it = v.begin();it != v.end(); it++) {
		//*it=vector<int>
		for (vector<int>::iterator its = (*it).begin(); its != (*it).end(); its++) {
			cout << *its << " ";
		}
		cout << endl;
	}
}

int main() {
	test();
	return 0;
}
插入和删除操作

insert和erase的第一个元素均为迭加器

第一个元素为v.begin(),第二和三个元素分别为v.begin()+1和v.begin()+2

最后一个元素的后一位为v.end(),因此最后一位元素为v.end()-1

数据存取操作
互换操作

通过vect<int>(v).swap(v);可以收缩内存

复制代码
#include <iostream>
#include <vector>

using namespace std;

int main() {
	vector<int> v;
	for (int i = 0;i < 10000;i++) {
		v.push_back(i);
	}
	cout << "v的容量为:" << v.capacity() << endl;
	cout << "v的大小为:" << v.size() << endl;

	v.resize(3);
	cout << "resize后的v的容量为:" << v.capacity() << endl;
	cout << "resize后的v的大小为:" << v.size() << endl;

	vector<int>(v).swap(v);
	cout << "swap后的v的容量为:" << v.capacity() << endl;
	cout << "swap后的v的大小为:" << v.size() << endl;

	return 0;
}

可以看出使用swap后vector所占的容量减少,大大节省了内存空间。其本质如下:

vector<int> (v)--匿名对象,其可以通过v的大小初始化一个新的vector容器,这个容器的大小和容量均为3

调用swap后便将这两个容器互换,同时删除新建的vector容器,而之前的vector容器由于互换导致其大小和容量均变为了3,从而节省了内存空间

预留空间操作

vector 动态扩容时,若初始预留空间远不足10000个数据,会在填满时反复 "重新分配更大内存->复制原有元素->释放旧空间"。这会导致容器首地址频繁改变,使得之前指向容器的迭代器、指针或引用会全部失效,同时多次复制元素会带来显著的性能开销。

因此可以使用reserve预留出所需的空间,从而避免容器首地址的频繁改变,并减少多次复制元素带来的性能开销。

deque容器--#include<deque>

功能

双端数组,可以对头端进行插入删除操作

deque与vector根本的不同在于对于数据的储存方式不同。

vector储存数据是将所有数据放置在一片连续的内存空间中,同时预留一部分容量,当其预留的容量仍无法完全储存当前数据时,开辟一个新的连续内存空间,直至可以将其完全储存。

而deque储存数据是将多个小的不连续的内存空间结合在一起,储存数据,保存每段内存空间的首地址以实现连续,当有无法储存的数据时,就开辟一个新的、与上述内存空间不连续的内存空间,因此可以实现首插入和尾插入。

因此我们也将deque的储存方式称为局部连续(缓冲区),整体分段(中控器)

和vector容器的区别
特性 std::vector std::deque
内存结构 一段连续的内存块 分段连续内存(多缓冲区 + 中控器)
随机访问 极快(O (1),直接指针偏移) 快(O (1),但需计算段地址)
插入 / 删除(头) 极慢(需移动所有后续元素) 极快
插入 / 删除(尾) 极快(预留空间足够时) 极快
插入 / 删除(中间) 慢(需移动后续元素) 慢(需移动元素)
迭代器失效 扩容时全部失效;中间操作后后续失效 中间操作全部失效;头尾操作可能失效
内存释放 只增不减,需 swap 手动收缩 删除元素可自动释放部分缓冲区
缓存友好性 高(连续内存,CPU 缓存命中高) 较低(分段内存,缓存跳转多)
迭代器
构造函数
赋值操作
容器大小

这里出现一个和vector的区别,deque没有容量的限制,这是因为deque对于数据的储存方式是不连续的,某种程度来说,其的容量是整个内存的容量。

插入和删除
数据存取操作

list容器--#include<list>

概念

将数据进行链式储存

链表

链表(list)是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的

链表的组成:链表由一系列结点组成

结点的组成:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域
list容器和deque容器最大的区别也在于数据的存储

deque容器的数据存储是缓冲区中元素局部连续,中控区的指针整体分散的。

list容器的元素是完全分散的,每一个元素都是一个新的结点,因此内存还需要记录每一个元素在内存中的指针,同时还为了区分每个元素的先后顺序,每一个结点除了储存自身的指针外,还会储存指向下一个元素的指针。

因此list的内存开销非常大,但是list可以对任意位置进行极快的插入和删除

特性 std::deque std::list
内存结构 分段连续内存(多缓冲区 + 中控器) 双向链表(每个节点独立存储,含前后指针)
随机访问 支持(O (1),需计算段地址) 不支持(仅支持双向遍历,O (n))
插入 / 删除(头) 极快 极快(O (1))
插入 / 删除(尾) 极快 极快(O (1))
插入 / 删除(中间) 慢(需移动后续元素) 极快(O (1),只需调整指针)
迭代器类型 随机访问迭代器 双向迭代器
迭代器失效 中间操作全部失效;头尾操作可能失效 仅失效指向被删除节点的迭代器
内存开销 较低(中控器 + 缓冲区) 较高(每个节点需额外存储前后指针)
缓存友好性 中等(分段连续,有一定缓存命中) 低(节点分散,缓存跳转多)

选vector:优先考虑内存效率和随机访问速度,且主要在尾部操作;

选list:优先考虑已知位置的插入 / 删除速度,且不需要随机访问;

选deque:需要头尾都快速插入 / 删除,且仍需随机访问,是两者的折中。

构造函数
赋值和交换操作
大小操作
插入和删除操作
数据存取操作
反转和排序操作

这里的sort()是list容器的成员函数,而非下面的算法中的函数,因此不需要包含算法的头文件,直接在list容器后面使用.进行调用即可。

list的sort函数一共有两个版本,默认是进行升序排列(l.sort()),其次是在sort函数中传递一个bool型的函数用以判断,传递的这个函数称为仿函数。

复制代码
#include <iostream>
#include <list>
#include <string>

using namespace std;

class Person {
public:
	string name;
	int age;
	int heigh;

	Person(string name, int age, int heigh):name(name), age(age), heigh(heigh){
	}
};

bool compare(Person& p1, Person& p2) {
	//按年龄大小升序排列,如果年龄相同则按身高升序排列
	if (p1.age != p2.age) {
		return p1.age < p2.age;
	}
	else {
		return p1.heigh < p2.heigh;
	}
}

int main() {
	Person p1 = { "a", 13, 166};
	Person p2 = { "b", 31, 175};
	Person p3 = { "c", 20, 174};
	Person p4 = { "d", 18, 189};
	Person p5 = { "e", 18, 179};

	list<Person> l;
	l.push_back(p1);
	l.push_back(p2);
	l.push_back(p3);
	l.push_back(p4);
	l.push_back(p5);
	l.sort(compare);
	for (list<Person>::iterator it = l.begin(); it != l.end(); it++) {
		cout << "姓名:" << (*it).name << "\t年龄:" << (*it).age << "\t身高:" << (*it).heigh << endl;
	}

	return 0;
}

set/multiset容器--#include<set>

概念

所有的元素都会在自动插入时自动被排序

set/multiset容器的区别:

构造函数
赋值操作
大小和交换操作
插入和删除操作

与前面不同的是,set/multiset只有insert函数,但是没有push、pop等函数

insert返回的pair对组

insert返回的对组是set容器判断插入的数据是否重复的重要依据,下述是set容器中insert的源码

复制代码
template <bool _Multi2 = _Multi, enable_if_t<!_Multi2, int> = 0>
pair<iterator, bool> insert(value_type&& _Val) {
    const auto _Result = _Emplace(_STD move(_Val));
    return {iterator(_Result.first, _Get_scary()), _Result.second};
}

该对组第一个元素是set的迭代器,第二个元素是bool类型的返回值,该返回值为ture则表示插入的数据没有重复,否则则是重复,不再插入

输出pair对组直接在对组后面使用first和second来实现即可

创建pair对组
查找和统计操作

由于set/multiset容器的特性,count统计出的元素个数只能为0或1

set容器的排序

由于set容器也是无法随机访问的容器,因此无法使用算法头文件中sort进行排序,但其内部有自己的排序成员函数。

内置类型指定排序规则
自定义数据类型指定排序规则

如果要给set容器中传入自定义的数据类型,那么就一定需要进行set函数排序的重新定义,因为set容器需要对传入的元素进行排序,但是默认排序无法适用于自定义的数据类型。

map/multimap容器--#include<map>

概念

map/multimap容器的性能较快,但是所需的内存开销也非常的大

构造函数
赋值操作
大小和交换操作
插入和删除操作
查找和统计操作
map容器的排序
复制代码
#include <iostream>
#include <map>
#include <string>
using namespace std;
class Person {
public:
	string name;
	int age;

	Person(string name, int age) :name(name), age(age) {}
};

class mycompare {
public:
	bool operator() (const Person& p1, const Person& p2) const {
		return p1.age > p2.age;
	}
};

int main() {
	map<Person, int, mycompare> s;
	Person p1 = { "a", 21 };
	Person p2 = { "b", 23 };
	Person p3 = { "c", 25 };
	Person p4 = { "d", 20 };
	s.insert(pair<Person, int>(p1, 1));
	s.insert(pair<Person, int>(p2, 2));
	s.insert(pair<Person, int>(p3, 3));
	s.insert(pair<Person, int>(p4, 4));

	for (map<Person, int, mycompare>::iterator it = s.begin(); it != s.end(); it++) {
		cout << "姓名:" << (*it).first.name << "\t年龄:" << (*it).first.age << endl;
	}
	return 0;
}

stack容器--#include<stack>

概念
构造函数
赋值操作
数据存取操作
大小操作

queue容器--#include<queue>

概念
构造函数
赋值操作
数据存取操作
大小操作

函数对象(仿函数)

概念
特点
复制代码
#include <iostream>
#include <string>

using namespace std;

class myAdd {
public:
	//函数对象超出普通函数的概念,函数对象可以有自己的状态
	int v1;
	int v2;

	//函数对象在使用时,可以像普通函数那样调用,可以有参数,可以有返回值
	int operator()(int v1, int v2) {
		return v1 + v2;
	}
};

int main() {
	myAdd a(20, 20);
	//函数对象可以作为参数传递
	cout << a(10, 20) << endl;
	cout << a.v1 << endl;
	return 0;
}
谓词--Pred
概念
内建函数对象
用法
算数仿函数
关系仿函数

前面所有实现自定义排序时,使用的仿函数都是关系仿函数

但是如果你使用的是自定义的数据类型,比如class、struct等,那么在必须自己重载所要用到的符号,否则就自己重载一个关系仿函数

复制代码
class Person {
public:
    string name;
    int age;

    Person(string name, int age) : name(name), age(age) {}

    bool operator>(const Person& other) const {
        // 按年龄降序比较
        return this->age > other.age;
    }
};

int main() {
    set<Person, greater<Person>> s;

    s.insert(Person("a", 21));
    s.insert(Person("b", 23));
    s.insert(Person("c", 20));

    for (const auto& p : s) {
        cout << p.name << " " << p.age << endl;
    }
    return 0;
}
逻辑仿函数

算法--#include<algorithm>

算法需要包含#include <algorithm>

遍历算法
遍历容器--for_each
搬运容器--transform

在搬运容器中,需要对目标容器进行resize赋予容量,否则程序会崩溃,一般使用size()函数进行开辟空间。

同时在搬运自定义数据时,直接使用resize进行赋值会导致程序发生错误

这是因为使用resize操作强制要求元素必须有默认无参构造函数。因此要么再写一个默认的无参构造函数,如下:

复制代码
Person() = default;

或者使用back_inserter()来进行传递,这个函数会自动调用push_back插入元素,但是需要包含头文件#include <iterator>

复制代码
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>

using namespace std;

class Person {
public:
	string name;
	int age;
    //Person() = default; //默认无参构造函数
	Person(string name, int age):name(name), age(age){}
};

class trans {
public:
	string name;
	int age;

	//姓名+_new,年龄+1
	Person operator()(const Person& p) {
		return Person(p.name + "_new", p.age + 1);
	}
};

int main() {
	Person p1("aaa", 11);
	Person p2("bbb", 22);
	vector<Person> v;
	v.push_back(p1);
	v.push_back(p2);
	vector<Person> v1;

	transform(v.begin(), v.end(), back_inserter(v1), trans());

	return 0;
}
查找算法

自定义类型的容器使用查找算法时,都需要重载==运算符,如下find函数实例。

这个不是绝对的,当只需要对比一部分,或者需要一定限制条件时,也可以直接使用谓词函数或匿名函数,如下find_if实例。

查找元素--find

当查找自定义数据类型时,需要重载==符号,否则无法找到元素

复制代码
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

class Person {
public:
	string name;
	int age;

	Person(string name, int age):name(name), age(age){}

	//重载==使得find函数内部可以对Person类进行对比
	bool operator==(const Person& p) {
		if ((this->name == p.name) && (this->age == p.age)) {
			return true;
		}
		return false;
	}
};

int main() {
	Person p1("aaa", 11);
	vector<Person> v;
	v.push_back(p1);

	vector<Person>::iterator it = find(v.begin(), v.end(), p1);
	if (it == v.end()) {
		cout << "没有找到" << endl;
	}
	else {
		cout << "找到 姓名:" << (*it).name << "\t年龄:" << (*it).age << endl;
	}
	return 0;
}
按条件查找元素--find_if

这个同上也需要重载==运算符,如果不重载运算符,则可以传递函数对象。同时也可以使用匿名函数

复制代码
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

class Person {
public:
	string name;
	int age;

	Person(string name, int age):name(name), age(age){}
};

class iffind{
public:
	bool operator()(const Person& p) {
		return p.age > 20;
	}
};

int main() {
	Person p1("aaa", 11);
	Person p2("bbb", 22);
	vector<Person> v;
	v.push_back(p1);
	v.push_back(p2);

	vector<Person>::iterator it = find_if(v.begin(), v.end(), iffind());
	if (it == v.end()) {
		cout << "没有找到" << endl;
	}
	else {
		cout << "找到 姓名:" << (*it).name << "\t年龄:" << (*it).age << endl;
	}
	return 0;
}

//匿名函数
find_if(v.begin(), v.end(), [](const Person& p) { return p.age > 20; });
查找相邻重复元素--adjacent_find

与单纯的find查找元素的方法来说,其速度更快,但是无序序列(stack、queue)无法使用,同时其只能返回bool类型,无法返回元素的位置。

一般使用binary_search的容器有vector、deque、array,而list是双向迭代器,无法直接跳转到中间位置,会导致其效率变得很低。

而如set、map等容器时,其内部为红黑数,本身便是有序的,因此直接使用专门的find成员函数(不是上述的find函数)效率更高、兼容性更好。

统计元素个数--count
按条件统计元素个数--count_if
排序算法
排序--sort(支持随机访问的容器)

对于所有支持随机访问的迭代器的容器,都可以使用sort完成升序操作

后面可以不加谓词,默认为升序排列,若要使用自定义类型的排序,则需要重载谓词或者直接使用functional库中的内建函数对象

打乱顺序--random_shuffle

这一函数现在使用较少,一般都直接使用shuffle进行打乱

复制代码
#include <iostream>
#include <vector>
#include <algorithm> // shuffle 头文件
#include <random>    // 随机数引擎头文件
#include <ctime>     // time(0) 头文件

using namespace std;

int main() {
    vector<int> v = {1, 2, 3, 4, 5};
    
    // 初始化随机数引擎(用当前时间作为种子)
    mt19937 rng(time(0));
    
    // 调用 shuffle,传入随机数引擎
    shuffle(v.begin(), v.end(), rng);
    
    // 每次运行顺序都不同
    for (int num : v) {
        cout << num << " ";
    }
    cout << endl;
    
    return 0;
}
容器元素合并--merge

合并到的容器和之前一样,一定要提前使用resize分配内存。

合并的两个容器可以是不一样的类型,但是其数据类型必须相同,同时排序方式也必须相同

针对于不同的数据类型,假如一个是vector<int>,另一个为deque<Person>,这个Person是一个类,如果要实现这两个容器的合并,必须满足三个条件,一是Person中必须含有int的成员变量,二是必须写一个仅传入int类型所运行的构造函数,三是重载排序运算符使得两个容器的排序方式相同

复制代码
class Person {
public:

    string name;
    // 条件1 包含int数据类型
    int age;

    // 条件2 支持从 int 隐式构造(为了让 int 能转为 Person)
    Person(int age) : name("Unknown"), age(age) {}
    Person(string name, int age) : name(name), age(age) {}

    // 条件3 支持按 age 比较(让 int 和 Person 能按同一规则排序)
    bool operator<(const Person& other) const {
        return this->age < other.age;
    }
};
反转指定范围的元素-reverse
拷贝和替换算法
元素拷贝--copy

一般情况都不需要重载=运算符,但是当你的原自定义类型中有成员变量是new创建是,便需要重载=运算符。

通过前面的知识可以知道,new创建的成员变量在析构函数中需要释放,但浅拷贝会完全拷贝这变量的所有信息,导致析构函数new区的重复释放,使得程序崩溃,因此这时就需要重载=运算符使得其为深拷贝。

元素替换--replace

replace相比于前面的copy函数,由于其内部存在一个==的对比,所有需要重载==运算符。

指定范围满足条件替换元素--replace_if

replace_if可以很好的替代replace,当replace需要重载==运算符时,直接使用replace_if在谓词中写出函数即可。

互换两个容器--swap

只能互换相同容器的相同类型。

算术生成算法

使用该算法时,不仅需要包含#include <algorithm>头文件,还需要包含#include <numeric>头文件

计算容器元素累计总和--accumlate

当需要类型自定义数据类型时,除了在类中重载+运算符,也可以直接使用匿名函数,如下

复制代码
#include <iostream>
#include <vector>
#include <numeric>
#include <string>

using namespace std;

class Person {
public:
    string name;
    int age;
    Person(string name, int age) : name(name), age(age) {}
};

int main() {
    vector<Person> v = {Person("a", 21), Person("b", 22), Person("c", 33)};

    int total_age = accumulate(v.begin(), v.end(), 0,
        [](int sum, const Person& p) {
            return sum + p.age; // 只累加 age
        });

    cout << "所有人的年龄总和:" << total_age << endl;

    return 0;
}
向容器中添加元素--fill

fill只能把同一个固定值填充到整个区间,无法填充不同的值。

集合算法
需求 推荐算法
找两个有序序列的共同元素 std::set_intersection
合并两个有序序列,去重 std::set_union
找 "在第一个序列有、第二个没有" 的元素 std::set_difference
求两个容器的交集--set_intersection
求两个容器的并集--set_union
求两个容器的差集--set_difference
相关推荐
枫叶丹42 小时前
【HarmonyOS 6.0】ArkWeb嵌套滚动快速调度策略
开发语言·华为·harmonyos
努力学习的小廉2 小时前
Python 零基础入门——基础语法(二)
android·开发语言·python
~plus~2 小时前
C# 内存管理深度剖析:从 Span<T> 到 Memory<T> 再到 ArrayPool
开发语言·c#
计算机安禾2 小时前
【数据结构与算法】第43篇:Trie树(前缀树/字典树)
c语言·开发语言·矩阵·排序算法·深度优先·图论·宽度优先
cici158742 小时前
C#与西门子S7-1200通讯实例
开发语言·c#
~plus~2 小时前
C# 异步编程深度剖析:从 async/await 到 ValueTask
开发语言·c#
回忆2012初秋2 小时前
C# 大文件分片上传完整实现指南
开发语言·c#
菜_小_白2 小时前
RTP协议收发组件开发
linux·网络·c++