C++ LeetCode 刷题经验、技巧及踩坑记录【二】
前言
记录一些小技巧以及平时不熟悉的知识。
以vector中某维元素为准对vector进行排序
见LeetCode1584题解 ,以权重为准对边进行升序排序,这也是 Kruskal 最小生成树算法的关键步骤。感觉这种方法很有用,以后也肯定能用到。
#include<algorithm>//使用sort需要include<algorithm>
//1. LeetCode官方题解
struct Edge {
int len, x, y;
Edge(int len, int x, int y) : len(len), x(x), y(y) {
}
};
vector<Edge> edges;
sort(edges.begin(), edges.end(), [](Edge a, Edge b) -> int {
return a.len < b.len; });
//2. 跟官方题解差不多
struct Edge {
int start; // 顶点1
int end; // 顶点2
int len; // 长度
};
vector<Edge> edges;
sort(edges.begin(), edges.end(), [](const auto& a, const auto& b) {
return a.len < b.len;
});
//3. labuladong
vector<vector<int>> edges; //第三位可以表示权重,因为在此题情况下,权重为int
sort(edges.begin(), edges.end(), [](auto& a, auto& b){
return a[2] < b[2];
});
struct Employee {
std::string name;
int id;
};
std::vector<Employee> employees;
// Pre-C++20的写法相当繁琐。
std::sort(employees.begin(), employees.end(), [](const auto& a, const auto& b) {
return a.id < b.id;});
切分定理
「切分定理」:
对于图的任意一种「切分」,其中权重最小的那条「横切边」一定是构成最小生成树的一条边。
优先队列 priority_queue
参考
包含头文件
#include <queue>
queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队
优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的
和队列基本操作相同:
top 访问队头元素
empty 队列是否为空
size 返回队列内元素个数
push 插入元素到队尾 (并排序)
emplace 原地构造一个元素并插入队列
pop 弹出队头元素
swap 交换内容
定义
priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆,降序。
//升序队列 小顶堆 great 小到大
priority_queue <int,vector<int>,greater<int> > pq;//升序
//降序队列 大顶堆 less 大到小 默认
priority_queue <int,vector<int>,less<int> > pq;//降序
priority_queue<int> pq;
使用自定义比较函数可以得到更广泛的应用。如LeetCode1584 Prim算法。
注意 priority_queue 与上述 sort 自定义比较函数的区别。
struct Edge{
int start;
int end;
int weight;
};
//优先级是按照权重由小到大的顺序排列
struct Cmp{
//重载操作符"()"
bool operator()(Edge& a, Edge& b){
return a.weight > b.weight; //小顶堆,升序
}
};
//新建优先级队列,以存储“横切边”
priority_queue<Edge, vector<Edge>, Cmp> pq; //这里队列q的元素为Edge,第二个参数和第三个参数是为了“实现升序而设置的”
这篇文章对优先队列说明的比较详细,可以看看。
greater 和 less
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
优先队列中大顶堆为什么用less
less:左数小于右数时,返回true,否则返回false。
在堆的调整过程中,对于大顶堆,如果当前插入的节点值大于其父节点,那么就应该向上调整。其父节点索引小于当前插入节点的索引,也就是父节点是左数,插入节点是右值,可以看到,左数小于右数时,要向上调整,也就是Compare函数应该返回true,正好是less。
优先级
C++优先队列是优先级高的在队首,定义优先级大小的方式是传入一个算子的参数比较a, b两个东西,返回true则a的优先级<b的优先级。
默认是less算子也就是返回a<b,也就是大的优先级高。
greater算子返回a>b,小的优先级高。
如果是默认的less算子,值大的优先级高,值大的排到了队头,优先队列大的先出队,也就是降序。
这里以great为例说一下用法
std::greater是用于执行比较的功能对象。它被定义为greater-than不等式比较的Function对象类。这可用于更改给定功能的功能。这也可以与各种标准算法一起使用,例如排序,优先级队列等。
头文件:
#include <functional.h>
用法:
对于顺序容器数组、vector等:
sort(arr.begin(), arr.end(), greater<int>());
示例:
// greater example
#include <iostream> // std::cout
#include <functional> // std::greater
#include <algorithm> // std::sort
int main () {
int numbers[]={
20,40,50,10,30};
std::sort (numbers, numbers+5, std::greater<int>());
for (int i=0; i<5; i++)
std::cout << numbers[i] << ' ';
std::cout << '\n';
return 0;
}
output:
50 40 30 20 10
但是在优先队列中是反过来的!
比如:
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
就是求升序。
另:以上优先队列中,在比较时,先按照pair的first元素升序,first元素相等时,再按照second元素升序
大顶堆小顶堆
吐槽一下,很多博主都没有搞清大顶堆小顶堆的概念,实在是误人子弟。
堆是一种非线性结构,可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
按照堆的特点可以把堆分为大顶堆和小顶堆
大顶堆:每个结点的值都大于或等于其左右孩子结点的值——求升序
小顶堆:每个结点的值都小于或等于其左右孩子结点的值——求降序
但是但是!在优先队列中,这玩意儿是反过来的!
排序时,构建大(小)顶堆是过程,是手段,不是结果!不能想当然的以大(小)顶堆的定义去认为是降序(升序) 。
以大顶堆求升序为例,其步骤如下:
- 先 n 个元素的无序序列,构建成大顶堆;
- 将根节点与最后一个元素交换位置,(将最大元素"沉"到数组末端);
- 交换过后可能不再满足大顶堆的条件,所以需要将剩下的 n-1 个元素重新构建成大顶堆;
- 重复第 2 步、第 3 步直到整个数组排序完成。
难理解的话可以看一下图解。
emplace_back()
在 C++11 之后,vector 容器中添加了新的方法:emplace_back() ,和 push_back() 一样的是都是在容器末尾添加一个新的元素进去,不同的是 emplace_back() 在效率上相比较于 push_back() 有了一定的提升。
在容器尾部添加一个元素,这个元素原地构造,不需要触发拷贝构造和转移构造。而且调用形式更加简洁,直接根据参数初始化临时对象的成员。
neighbors.push_back({
nx,ny}); //合法
neighbors.emplace_back(vector<int>{
nx, ny }); //合法
内存非法访问报错
检查索引是否越界,检查for循环上下限
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000029c at pc 0x00000034ebda bp 0x7ffd1385b4f0 sp 0x7ffd1385b4e8
READ of size 4 at 0x60200000029c thread T0
C++ unordered_map
unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是 哈希表 存储结构(各种题解中c++哈希表基本就用它),该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。
#include <unordered_map>
unordered_map 容器模板的定义如下所示:
template < class Key, //键值对中 键的类型
class T, //键值对中 值的类型
class Hash = hash<Key>, //容器内部存储键值对所用的哈希函数
class Pred = equal_to<Key>, //判断各个键值对键相同的规则
class Alloc = allocator< pair<const Key,T> > // 指定分配器对象的类型
> class unordered_map;
以上 5 个参数中,必须显式给前 2 个参数传值,并且除特殊情况外,最多只需要使用前 4 个参数,各自的含义和功能如表 1 所示。
参数 | 含义 |
---|---|
<key,T> | 前 2 个参数分别用于确定键值对中键和值的类型,也就是存储键值对的类型。 |
Hash = hash<Key> | 用于指明容器在存储各个键值对时要使用的哈希函数,默认使用 STL 标准库提供的 hash<key> 哈希函数。注意,默认哈希函数只适用于基本数据类型(包括 string 类型),而不适用于自定义的结构体或者类。 |
Pred = equal_to<Key> | unordered_map 容器中存储的各个键值对的键是不能相等的,而判断是否相等的规则,就由此参数指定。默认情况下,使用 STL 标准库中提供的 equal_to<key> 规则,该规则仅支持可直接用 == 运算符做比较的数据类型。 |
总的来说,当无序容器中存储键值对的键为自定义类型时,默认的哈希函数 hash 以及比较函数 equal_to 将不再适用,只能自己设计适用该类型的哈希函数和比较函数,并显式传递给 Hash 参数和 Pred 参数。
创建 unordered_map容器的方法
- 创建空unordered_map
std::unordered_map<std::string, std::string> umap;
- 创建同时初始化
std::unordered_map<std::string, std::string> umap{
{
"A","11"},
{
"B","22"},
{
"C","33"} };
- 调用 unordered_map 模板中提供的复制(拷贝)构造函数
std::unordered_map<std::string, std::string> umap2(umap);
还可以调用移动构造函数,即以右值引用的方式将临时 unordered_map 容器中存储的所有键值对,全部复制给新建容器。
//返回临时 unordered_map 容器的函数
std::unordered_map <std::string, std::string > retUmap(){
std::unordered_map<std::string, std::string>tempUmap{
{
"A","11"},
{
"B","22"},
{
"C","33"} };
return tempUmap;
}
//调用移动构造函数,创建 umap2 容器
std::unordered_map<std::string, std::string> umap2(retUmap());
- 如果不想全部拷贝,可以使用 unordered_map 类模板提供的迭代器,在现有 unordered_map 容器中选择部分区域内的键值对,为新建 unordered_map 容器初始化。
//传入 2 个迭代器,
std::unordered_map<std::string, std::string> umap2(++umap.begin(),umap.end());
通过此方式创建的 umap2 容器,其内部就包含 umap 容器中除第 1 个键值对外的所有其它键值对。
unordered_map容器的成员函数
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个键值对的正向迭代器。 |
end() | 返回指向容器中最后一个键值对之后位置的正向迭代器。 |
cbegin() | const begin() ,即该方法返回的迭代器不能用于修改容器内存储的键值对。 |
cend() | const end() ,即该方法返回的迭代器不能用于修改容器内存储的键值对。 |
empty() | 若容器为空,则返回 true;否则 false。 |
size() | 返回当前容器中存有键值对的个数。 |
max_size() | 返回容器所能容纳键值对的最大个数,不同的操作系统,其返回值亦不相同。 |
operator[key] | 该模板类中重载了 [] 运算符,其功能是可以向访问数组中元素那样,只要给定某个键值对的键 key,就可以获取该键对应的值。注意,如果当前容器中没有以 key 为键的键值对,则其会使用该键向当前容器中插入一个新键值对。 |
at(key) | 返回容器中存储的键 key 对应的值,如果 key 不存在,则会抛出 out_of_range 异常。 |
find(key) | 查找以 key 为键的键值对,如果找到,则返回一个指向该键值对的正向迭代器;反之,则返回一个指向容器中最后一个键值对之后位置的迭代器(即 end() 方法返回的迭代器)。 |
count(key) | 在容器中查找以 key 键的键值对的个数(0或1)。 |
equal_range(key) | 返回一个 pair 对象,其包含 2 个迭代器,用于表明当前容器中键为 key 的键值对所在的范围。 |
emplace() | 向容器中添加新键值对,效率比 insert() 方法高。 |
emplace_hint() | 向容器中添加新键值对,效率比 insert() 方法高。 |
insert() | 向容器中添加新键值对。 |
erase() | 删除指定键值对。 |
clear() | 清空容器,即删除容器中存储的所有键值对。 |
swap() | 交换 2 个 unordered_map 容器存储的键值对,前提是必须保证这 2 个容器的类型完全相等。 |
bucket_count() | 返回当前容器底层存储键值对时,使用桶(一个线性链表代表一个桶)的数量。 |
max_bucket_count() | 返回当前系统中,unordered_map 容器底层最多可以使用多少桶。 |
bucket_size(n) | 返回第 n 个桶中存储键值对的数量。 |
bucket(key) | 返回以 key 为键的键值对所在桶的编号。 |
load_factor() | 返回 unordered_map 容器中当前的负载因子。负载因子,指的是的当前容器中存储键值对的数量(size())和使用桶数(bucket_count())的比值,即 load_factor() = size() / bucket_count()。 |
max_load_factor() | 返回或者设置当前 unordered_map 容器的负载因子。 |
rehash(n) | 将当前容器底层使用桶的数量设置为 n。 |
reserve() | 将存储桶的数量(也就是 bucket_count() 方法的返回值)设置为至少容纳count个元(不超过最大负载因子)所需的数量,并重新整理容器。 |
hash_function() | 返回当前容器使用的哈希函数对象。 |
unordered_map 常用操作
- 插入
dict.insert(pair<string,int>("apple",2));
dict["banana"] = 6;
dict.insert(unordered_map<string, int>::value_type("orange",3)); // move insertion
dict.insert(mydict); // copy insertion
dict.insert(mydict.begin(), mydict.end()); // range insertion
dict.insert({
{
"sugar", 8 }, {
"salt", 0 } }); // initializer list insertion
- 遍历
unordered_map<string, int>::iterator iter;
for(iter=dict.begin();iter!=dict.end();iter++)
cout<<iter->first<<ends<<iter->second<<endl;
- 查找
if(dict.count("boluo")==0)
cout<<"can't find boluo!"<<endl;
if((iter=dict.find("banana"))!=dict.end())
cout<<"banana="<<iter->second<<endl;
- 访问
// 1. []
// 如果 k 匹配容器中某个元素的键,则该函数返回该映射值的引用。
// 如果 k 与容器中任何元素的键都不匹配,则该函数将使用该键插入一个新元素,并返回该映射值的引用。
cout<<dict["banana"]<<endl;
// 2. .at()
// 如果 k 匹配容器中某个元素的键,则该函数返回该映射值的引用。
// 如果 k 与容器中任何元素的键都不匹配,则该函数将抛出 out_of_range 异常。
dict.at("nanana");
- 删除
mymap.erase(mymap.begin()); // erasing by iterator
mymap.erase("France"); // erasing by key
mymap.erase(mymap.find("China"), mymap.end()); // erasing by range
mymap.clear() // clear all
C++ list
STL list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中。
list 容器中各个元素的前后顺序是靠指针来维系的,每个元素都配备了 2 个指针,分别指向它的前一个元素和后一个元素。也可以自己实现,如LeetCode146官方题解,但代码量会增加,所以在题目较复杂的时候还是直接用stl::list吧,见LeetCode460官方题解。
基于这样的存储结构,list 容器具有一些其它容器(array、vector 和 deque)所不具备的优势:在序列已知的任何位置快速插入或删除元素(时间复杂度为O(1))。并且在 list 容器中移动元素,也比其它容器的效率高。
list 容器的缺点是:不能通过位置直接访问元素。
如果需要对序列进行大量添加或删除元素的操作,而直接访问元素的需求却很少,这种情况建议使用 list 容器。
list 容器以模板类 list(T 为存储元素的类型)的形式在头文件中,并位于 std 命名空间中。
#include <list>
list容器的创建
根据不同的使用场景,有以下 5 种创建 list 容器的方式供选择。
- 创建一个没有任何元素的空 list 容器:
std::list<int> values;
和空 array 容器不同,空的 list 容器在创建之后仍可以添加元素,因此创建 list 容器的方式很常用。
- 创建一个包含 n 个元素的 list 容器:
std::list<int> values(10);
通过此方式创建 values 容器,其中包含 10 个元素,每个元素的值都为相应类型的默认值(int类型的默认值为 0)。
- 创建一个包含 n 个元素的 list 容器,并为每个元素指定初始值。例如:
std::list<int> values(10, 5);
如此就创建了一个包含 10 个元素并且值都为 5 个 values 容器。
- 在已有 list 容器的情况下,通过拷贝该容器可以创建新的 list 容器。例如:
std::list<int> value1(10);
std::list<int> value2(value1);
注意,采用此方式,必须保证新旧容器存储的元素类型一致。
- 通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 list 容器。例如:
//拷贝普通数组,创建list容器
int a[] = {
1,2,3,4,5 };
std::list<int> values(a, a+5);
//拷贝其它类型的容器,创建 list 容器
std::array<int, 5>arr{
11,12,13,14,15 };
std::list<int>values(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}
list 容器的成员函数
成员函数 | 功能 |
---|---|
begin() | 返回指向容器中第一个元素的双向迭代器。 |
end() | 返回指向容器中最后一个元素所在位置的下一个位置的双向迭代器。 |
rend() | 返回指向最后一个元素的反向双向迭代器。 |
rbegin() | 返回指向第一个元素所在位置前一个位置的反向双向迭代器。 |
cbegin() | const begin() ,不能用于修改元素。 |
cend() | const end() ,不能用于修改元素。 |
crbegin() | const rbegin() ,不能用于修改元素。 |
crend() | const rend() ,不能用于修改元素。 |
empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
size() | 返回当前容器实际包含的元素个数。 |
max_size() | 返回容器所能包含元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。 |
front() | 返回第一个元素的引用。 |
back() | 返回最后一个元素的引用。 |
assign() | 用新元素替换容器中原有内容。 |
emplace_front() | 在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。 |
push_front() | 在容器头部插入一个元素。 |
pop_front() | 删除容器头部的一个元素。 |
emplace_back() | 在容器尾部直接生成一个元素。该函数和 push_back() 的功能相同,但效率更高。 |
push_back() | 在容器尾部插入一个元素。 |
pop_back() | 删除容器尾部的一个元素。 |
emplace() | 在容器中的指定位置插入元素。该函数和 insert() 功能相同,但效率更高。 |
insert() | 在容器中的指定位置插入元素。 |
erase() | 删除容器中一个或某区域内的元素。 |
swap() | 交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。 |
resize() | 调整容器的大小。 |
clear() | 删除容器存储的所有元素。 |
splice() | 将一个 list 容器中的元素插入到另一个容器的指定位置。 |
remove(val) | 删除容器中所有等于 val 的元素。 |
remove_if() | 删除容器中满足条件的元素。 |
unique() | 删除容器中相邻的重复元素,只保留一个,对于排序列表特别有用。 |
merge() | 合并两个事先已排好序的 list 容器,并且合并之后的 list 容器依然是有序的。 |
sort() | 通过更改容器中元素的位置,将它们进行排序。 |
reverse() | 反转容器中元素的顺序。 |
C++ iterator
iterator 是一种可以遍历容器元素的数据类型。迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。C++更趋向于使用迭代器而不是数组下标操作,因为标准库为每一种标准容器(如vector、map和list等)定义了一种迭代器类型,而只有少数容器(如vector)支持数组下标操作访问容器元素。
迭代器有5种类型。
- 输入迭代器(Input Iterator):只能向前单步迭代元素,不允许修改由该迭代器所引用的元素;
- 输出迭代器(Output Iterator):只能向前单步迭代元素,对由该迭代器所引用的元素只有写权限;
- 向前迭代器(Forward Iterator):该迭代器可以在一个区间中进行读写操作,它拥有输入迭代器的所有特性和输出迭代器的部分特性,以及向前单步迭代元素的能力;
- 双向迭代器(Bidirectional Iterator):在向前迭代器的基础上增加了向后单步迭代元素的能力;
- 随机访问迭代器(Random Access Iterator):不仅综合以上4种迭代器的所有功能,还可以像指针那样进行算术计算。
这里贴一张迭代器类别对应的属性表:
容器 | 迭代器功能 |
---|---|
vector | 随机访问 |
deque | 随机访问 |
list | 双向 |
set / multiset | 双向 |
map / multimap | 双向 |
stack | 不支持迭代器 |
queue | 不支持迭代器 |
priority_queue | 不支持迭代器 |
迭代器操作
STL 中有用于操作迭代器的三个函数模板,它们是:
- advance(p, n):使迭代器 p 向前或向后移动 n 个元素。
- distance(p, q):计算两个迭代器之间的距离,即迭代器 p 经过多少次 + + 操作后和迭代器 q 相等。如果调用时 p 已经指向 q 的后面,则这个函数会陷入死循环。
- iter_swap(p, q):用于交换两个迭代器 p、q 指向的值。]
每种容器类型都定义了自己的迭代器类型,
vector<int>::iterator iter;
这条语句定义了一个名为iter的变量,它的数据类型是由vector<int>定义的iterator类型。
还有常量迭代器:
vector<int>::const_iterator citer;
通过迭代器可以读取它指向的元素,*迭代器名就表示迭代器指向的元素。如:
for(itr=v.begin();itr!=v.end();++itr)
{
cout<<*itr<<" ";
通过非常量迭代器还能修改其指向的元素。