STL
Standard template libaray标准模板库。
容器:
- 顺序容器:vector(向量容器)、deque(双端队列容器)、list(链表容器)。
- 容器适配器:stack、queue、priority queue。
- 无序关联容器:unordered_set、unordered_multiset、unordered_map、unordered_multimap。用哈希表存储,增删查接近O(1)的时间复杂度。
- 有序关联容器:set、multiset、map、multimap。用红黑树存储,增删查接近O(log2N),N是节点数量。
近容器:数组、string、bitset(位容器)。
迭代器:Iterator和const_interator,reverse_iterator和const_reverse_iterator。
函数对象:Qreater,less。类似于C的函数指针。
泛型算法:Sort,find,find_if,binary_search,for_each。
Vector
底层数据结构:动态开辟的数组,每次以原来空间大小的2倍进行扩容。
增加:
#include <vector>
int main() {
std::vector<int> vec;
// 在末尾添加元素,注意会导致容器扩容。
vec.push_back(1); // 时间复杂度:平摊常数时间 O(1)
// 在指定位置插入元素
vec.insert(vec.begin() + 1, 2); // 时间复杂度:O(n),n 是当前元素的数量,因为需要将插入位置之后的元素向后移动一位,以腾出空间插入新元素。
return 0;
}
删除:
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3};
// 删除末尾元素
vec.pop_back(); // 时间复杂度:常数时间 O(1)
// 删除指定位置的元素
vec.erase(vec.begin() + 1); // 时间复杂度:O(n),n 是当前元素的数量
return 0;
}
查询:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 通过索引访问元素
std::cout << vec[2] << std::endl; // 时间复杂度:常数时间 O(1)
// 使用 find 方法查找元素
int target = 3;
auto it = std::find(vec.begin(), vec.end(), target);
if (it != vec.end()) {
std::cout << "Element found at index: " << std::distance(vec.begin(), it) << std::endl;
} else {
std::cout << "Element not found" << std::endl;
}
// 时间复杂度:O(n),n 是当前元素的数量
// 使用 foreach 循环遍历元素
for (int element : vec) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
注意:堆容器进行连续插入或者删除操作,一定要更新迭代器,否则第一次insert或者erase完成,迭代器会失效。
其他常用方法有:Size,resizese,reverse(保留或反转),empty,clear等。
Deque
双端队列容器。底层数据结构相对复杂,是一个邻接数组结构。
它初始的时候会有2个数组专门存放对象,这2个数组的指针会被放在一个mapper数组中。对象数组不够用的时候,就会开辟新的数组,同时mapper数组也会增加新的位置用于存放新的对象数组的指针。
注意,它也是2倍扩容的,对象数组会按2-4-8-16这种方式增长,所以mapper数组也会按这个模式增长。每当mapper数组增长的时候,旧的数组指针就会被放入mapper数组的中心位置,方便从两端插入数据。
由此它能完成动态扩容与双端数据压入。
增加
Deq.push_back(10);//从末尾添加元素,O(1) 时间复杂度,vector是O(n)因为需要挨个复制元素
Deq.push_front(10);//从首部添加元素,O(1)时间复杂度
Deq.insert(10);//O(n),vec.insert是O(1)的时间复杂度
删除
Deq.pop_back();//从末尾删除元素,O(1)
Deq.pop_front();//从首部删除元素,O(1)
Deq.erase(it);//从it指向的位置删除元素,O(n)
搜索
Iterator //连续的insert和erase就一定要考虑迭代器失效的问题
List
链表容器,底层数据结构是双向的循环链表。
增加
List.push_back和List.push_front和List.insert都是O(1)的时间复杂度,因为insert不需要依次复制数据的过程。
不过List的insert方法千需要query查询,查询效率比较慢,因为分布在零散的空间中。
删除
List.pop_back和List.pop_front和List.erase。
查询
也是通过interator迭代器,时间复杂度是O(n)。只要是通过迭代器一定要考虑迭代器失效的问题。
Vector、deuqe和list对比
存储空间连续性:vector连续。list不连续,deque间断连续。
扩容:vector动态2倍扩容,需要复制原元素需要申请空间。List链表扩容,不需要复制原元素。Deque的mapper数组是连续空间上的2倍扩容,与vector扩容一样需要复制需要申请空间,对象数组仅仅是数量上的2倍扩容。
初始:vector和list初始不占空间,vector除非reverse。Deque初始就申请了对象数组空间。
删除、插入元素时间复杂度:List前中后全O(1)但是要考虑查询的速度,vector只有后O(1),deque只有中间不是O(1)。但是从效率上来说,如果deque用了n(n>1)个数组,效率回避vector慢。
查询数据时间复杂度:List是O(n),vector和deque都是O(1)。
内存使用效率:list最高,deque其次,vector最低。主要是看对内存连续性的要求。
迭代器
Interator:普通正向迭代器。
Const_interator:常量正向迭代器,只能读不能修改对象。
Reverse_iterator:普通反向迭代器,通常配合rbegin和rend方法(返回的是首元素前去位置的迭代器表示)使用,++的时候会倒序向前搜索容器。
Const_reverse_interator:常量反向迭代器。
无序关联容器
关联容器:与顺序容器相对,顺序容器通过元素在容器的位置来进行访问的。关联容器则是通过key(关键字)访问,关联容器要么存储key-value对,要么只存储key。比如vec[1]是顺序容器访问元素,Map["Bob"]是关联容器访问。
底层存储结构:链式哈希表。增删改O(1)。
Set:集合,只存储key。Map:映射表,存储kv对。
无序关联容器包括:unordered_set单重集合,unordered_multiset多重集合,unordered_map单重映射表,unordered_multimap多重映射表。单重就是不允许key重复,多重就是允许key重复。
unordered_multimap和unordered_multiset对比:
#include <iostream>
#include <unordered_map>
#include <unordered_set>
int main() {
// std::unordered_multimap 示例
std::unordered_multimap<int, std::string> myMultimap;
myMultimap.insert({1, "apple"});
myMultimap.insert({2, "banana"});
myMultimap.insert({2, "orange"});
myMultimap.insert({3, "apple"});
// 增加元素
myMultimap.insert({4, "grape"});
// 删除特定键值对
auto range = myMultimap.equal_range(2);
for (auto it = range.first; it != range.second; ) {
if (it->second == "orange") {
it = myMultimap.erase(it);
} else {
++it;
}
}
// 修改值
for (auto& pair : myMultimap) {
if (pair.second == "apple") {
pair.second = "kiwi";
}
}
// 查找特定键对应的所有值
auto range2 = myMultimap.equal_range(1);
std::cout << "Values for key 1: ";
for (auto it = range2.first; it != range2.second; ++it) {
std::cout << it->second << " ";
}
std::cout << std::endl;
// std::unordered_multiset 示例
std::unordered_multiset<std::string> myMultiset;
myMultiset.insert("apple");
myMultiset.insert("banana");
myMultiset.insert("orange");
myMultiset.insert("apple");
// 增加元素
myMultiset.insert("grape");
// 删除特定值
for (auto it = myMultiset.begin(); it != myMultiset.end(); ) {
if (*it == "orange") {
it = myMultiset.erase(it);
} else {
++it;
}
}
// 查找特定值的个数
int count = myMultiset.count("apple");
std::cout << "Count of 'apple': " << count << std::endl;
return 0;
}
有序关联容器
C++的有序关联容器(如std::set和std::map)使用红黑树(Red-Black Tree)作为底层数据结构来存储元素。由于红黑树的自平衡性质,它保证了在最坏情况下的查找、插入和删除操作的时间复杂度为O(log N),其中N是树中元素的个数。
对比无序关联容器,它最大的特点是:插入元素之后,对自动排序。
如果容器容纳的对象是自定义类对象,那么需要这个类重载<符号,否则有序关联容器不知道排序的依据是什么。如果是map,只需要重载key类型的<即可。
函数对象
STL(Standard Template Library)提供了许多函数对象(Function Objects),也称为函数符(Functors),用于在算法和容器中执行操作。
什么是函数对象?
在下面的案例中,Multiplier是一个类,但它使用形式可以像函数一样,都是名字+(参数),这个就是函数类。
#include <iostream>
class Multiplier {
public:
Multiplier(int factor) : factor_(factor) {}
int operator()(int number) const {
return number * factor_;
}
private:
int factor_;
};
int main() {
Multiplier multiplyByTwo(2);
// 使用函数对象进行乘法操作
int result = multiplyByTwo(5);
std::cout << "Result: " << result << std::endl;
return 0;
}
为什么要使用函数对象?通过函数指针调用对象,效率是很低的,因为会有函数调用开销。但是函数对象可以内联。同时函数对象可以添加一些属性,记录函数对象的调用信息。
函数对象的使用:实例化一个函数对象,然后传入给能接收函数对象的函数就行。这个函数可能来自于函数模板,会自动推导函数对象的类型。
#include <algorithm>
#include <vector>
int main() {
std::vector<int> numbers = {4, 2, 7, 1, 5};
// 使用std::less函数对象进行升序排序
std::sort(numbers.begin(), numbers.end(), std::less<int>());
// 输出排序结果
for (const auto& num : numbers) {
std::cout << num << " ";
}
return 0;
}
函数类的使用
把类模板实例化成模板类再传给类型参数使用。
#include <iostream>
#include <set>
int main() {
// 使用 std::less 函数对象进行从大到小排序
std::set<int, std::less<int>> mySet = {5, 2, 8, 1, 9};
// 输出排序后的元素
for (const auto& num : mySet) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
容器适配器
C++容器适配器(Container Adapters)是一种特殊类型的容器,它们提供了一种不同于标准容器的接口和功能。容器适配器本质上是基于现有的标准容器实现的包装器,通过限制或修改标准容器的接口,提供了特定的功能和行为。
C++标准库提供了三种常见的容器适配器:
std::stack:栈适配器,基于std::deque、std::list或std::vector实现,提供了后进先出(LIFO)的数据结构。它主要支持push、pop、top等操作。
std::queue:队列适配器,基于std::deque、std::list或std::vector实现,提供了先进先出(FIFO)的数据结构。它主要支持push、pop、front、back等操作。
std::priority_queue:优先队列适配器,基于std::vector实现,默认情况下使用std::less作为比较函数对象,提供了按照优先级排序的数据结构。它主要支持push、pop、top等操作。
#include <iostream>
#include <stack>
int main() {
std::stack<int> myStack;
myStack.push(5);
myStack.push(2);
myStack.push(8);
myStack.push(1);
myStack.push(9);
while (!myStack.empty()) {
std::cout << myStack.top() << " ";
myStack.pop();
}
std::cout << std::endl;
return 0;
}
#include <iostream>
#include <queue>
int main() {
std::queue<int> myQueue;
myQueue.push(5);
myQueue.push(2);
myQueue.push(8);
myQueue.push(1);
myQueue.push(9);
while (!myQueue.empty()) {
std::cout << myQueue.front() << " ";
myQueue.pop();
}
std::cout << std::endl;
return 0;
}
#include <iostream>
#include <queue>
int main() {
std::priority_queue<int> myPriorityQueue;
myPriorityQueue.push(5);
myPriorityQueue.push(2);
myPriorityQueue.push(8);
myPriorityQueue.push(1);
myPriorityQueue.push(9);
while (!myPriorityQueue.empty()) {
std::cout << myPriorityQueue.top() << " ";
myPriorityQueue.pop();
}
std::cout << std::endl;
return 0;
}
泛型算法
泛型算法 = template + 迭代器 + 函数对象。
包括Sort,find,find_if,binary_search,for_each等。
泛型算法的特点:1、泛型算法接收的都是迭代器。2、泛型算法的参数还可以接收函数对象。
示例代码,演示如何使用 std::vector、std::sort 和 std::binary_search 进行排序和二分查找。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
// 创建一个 vector 并初始化
std::vector<int> vec = {5, 2, 8, 1, 9, 3};
// 使用 sort 泛型算法对 vector 进行排序
std::sort(vec.begin(), vec.end());
// 打印排序后的 vector
std::cout << "排序后的 vector: ";
for (const auto& element : vec) {
std::cout << element << " ";
}
std::cout << std::endl;
// 使用 binary_search 泛型算法查找元素
int target = 8;
bool found = std::binary_search(vec.begin(), vec.end(), target);
// 输出查找结果
if (found) {
std::cout << "找到了元素 " << target << std::endl;
} else {
std::cout << "未找到元素 " << target << std::endl;
}
return 0;
}
绑定器
绑定器+二元函数对象(需要两个参数的函数对象) = 一元函数对象。
绑定器的作用是可以把二元函数对象的其中一个参数给绑定给某个值,这样子传参就只需要传入一个参数。
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector<int> numbers = {80, 70, 60, 50, 40, 30};
auto it = std::find_if(numbers.begin(), numbers.end(), std::bind(std::less<int>(), std::placeholders::_1, 49));
if (it != numbers.end()) {
std::cout << "找到了第一个小于49的数:" << *it << std::endl;
} else {
std::cout << "未找到小于49的数" << std::endl;
}
return 0;
}
在上述代码中,我们使用了 std::bind 函数将 std::less<int>() 函数对象与 std::placeholders::_1(第一个参数)和常数值 49 绑定。std::less<int>() 是一个函数对象,用于比较两个值的大小。
然后,我们使用 std::find_if 算法在 numbers 容器中查找第一个小于49的元素。如果找到了符合条件的数,我们输出该数的值;否则,输出未找到。
Lambda表达式
Lambda表达式是C++11的语法,它用于生成一个函数对象。
#include <iostream>
int main() {
int x = 5;
int y = 10;
// 空捕获列表
auto lambda1 = []() {
std::cout << "Empty capture list" << std::endl;
};
lambda1();
// 值捕获列表
auto lambda2 = [x, y]() {
std::cout << "Value capture list: x = " << x << ", y = " << y << std::endl;
};
lambda2();
// 引用捕获列表
auto lambda3 = [&x, &y]() {
x++;
y++;
std::cout << "Reference capture list: x = " << x << ", y = " << y << std::endl;
};
lambda3();
std::cout << "After lambda3: x = " << x << ", y = " << y << std::endl;
return 0;
}
Empty capture list
Value capture list: x = 5, y = 10
Reference capture list: x = 6, y = 11
After lambda3: x = 6, y = 11