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];
 });

more…

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元素升序

参考


大顶堆小顶堆

吐槽一下,很多博主都没有搞清大顶堆小顶堆的概念,实在是误人子弟。

堆是一种非线性结构,可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

按照堆的特点可以把堆分为大顶堆和小顶堆

大顶堆:每个结点的值都大于或等于其左右孩子结点的值——求升序
小顶堆:每个结点的值都小于或等于其左右孩子结点的值——求降序

但是但是!在优先队列中,这玩意儿是反过来的!

排序时,构建大(小)顶堆是过程,是手段,不是结果!不能想当然的以大(小)顶堆的定义去认为是降序(升序)

以大顶堆求升序为例,其步骤如下:

  1. 先 n 个元素的无序序列,构建成大顶堆;
  2. 将根节点与最后一个元素交换位置,(将最大元素"沉"到数组末端);
  3. 交换过后可能不再满足大顶堆的条件,所以需要将剩下的 n-1 个元素重新构建成大顶堆;
  4. 重复第 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 所示。

表 1 unordered_map 容器模板类的常用参数
参数 含义
<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容器的方法

  1. 创建空unordered_map
std::unordered_map<std::string, std::string> umap;
  1. 创建同时初始化
std::unordered_map<std::string, std::string> umap{
    
    
    {
    
    "A","11"},
    {
    
    "B","22"},
    {
    
    "C","33"} };
  1. 调用 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());
  1. 如果不想全部拷贝,可以使用 unordered_map 类模板提供的迭代器,在现有 unordered_map 容器中选择部分区域内的键值对,为新建 unordered_map 容器初始化。
//传入 2 个迭代器,
std::unordered_map<std::string, std::string> umap2(++umap.begin(),umap.end());

通过此方式创建的 umap2 容器,其内部就包含 umap 容器中除第 1 个键值对外的所有其它键值对。

unordered_map容器的成员函数

表 2 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 常用操作

  1. 插入
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
  1. 遍历
unordered_map<string, int>::iterator iter;
for(iter=dict.begin();iter!=dict.end();iter++)
	cout<<iter->first<<ends<<iter->second<<endl;
  1. 查找
if(dict.count("boluo")==0)
	cout<<"can't find boluo!"<<endl;

if((iter=dict.find("banana"))!=dict.end())
	cout<<"banana="<<iter->second<<endl;
  1. 访问
// 1. []
// 如果 k 匹配容器中某个元素的键,则该函数返回该映射值的引用。
// 如果 k 与容器中任何元素的键都不匹配,则该函数将使用该键插入一个新元素,并返回该映射值的引用。
cout<<dict["banana"]<<endl;

// 2. .at()
// 如果 k 匹配容器中某个元素的键,则该函数返回该映射值的引用。
// 如果 k 与容器中任何元素的键都不匹配,则该函数将抛出 out_of_range 异常。
dict.at("nanana");
  1. 删除
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 容器的方式供选择。

  1. 创建一个没有任何元素的空 list 容器:
std::list<int> values;

和空 array 容器不同,空的 list 容器在创建之后仍可以添加元素,因此创建 list 容器的方式很常用。

  1. 创建一个包含 n 个元素的 list 容器:
std::list<int> values(10);

通过此方式创建 values 容器,其中包含 10 个元素,每个元素的值都为相应类型的默认值(int类型的默认值为 0)。

  1. 创建一个包含 n 个元素的 list 容器,并为每个元素指定初始值。例如:
std::list<int> values(10, 5);

如此就创建了一个包含 10 个元素并且值都为 5 个 values 容器。

  1. 在已有 list 容器的情况下,通过拷贝该容器可以创建新的 list 容器。例如:
std::list<int> value1(10);
std::list<int> value2(value1);

注意,采用此方式,必须保证新旧容器存储的元素类型一致。

  1. 通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 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 容器的成员函数

表 3 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种类型。

  1. 输入迭代器(Input Iterator):只能向前单步迭代元素,不允许修改由该迭代器所引用的元素;
  2. 输出迭代器(Output Iterator):只能向前单步迭代元素,对由该迭代器所引用的元素只有写权限;
  3. 向前迭代器(Forward Iterator):该迭代器可以在一个区间中进行读写操作,它拥有输入迭代器的所有特性和输出迭代器的部分特性,以及向前单步迭代元素的能力;
  4. 双向迭代器(Bidirectional Iterator):在向前迭代器的基础上增加了向后单步迭代元素的能力;
  5. 随机访问迭代器(Random Access Iterator):不仅综合以上4种迭代器的所有功能,还可以像指针那样进行算术计算。

这里贴一张迭代器类别对应的属性表
在这里插入图片描述

表4 不同容器支持的迭代器类型
容器 迭代器功能
vector 随机访问
deque 随机访问
list 双向
set / multiset 双向
map / multimap 双向
stack 不支持迭代器
queue 不支持迭代器
priority_queue 不支持迭代器

迭代器操作

STL 中有用于操作迭代器的三个函数模板,它们是:

  1. advance(p, n):使迭代器 p 向前或向后移动 n 个元素。
  2. distance(p, q):计算两个迭代器之间的距离,即迭代器 p 经过多少次 + + 操作后和迭代器 q 相等。如果调用时 p 已经指向 q 的后面,则这个函数会陷入死循环。
  3. 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<<" "; 

通过非常量迭代器还能修改其指向的元素。

参考 更多


猜你喜欢

转载自blog.csdn.net/m0_50910915/article/details/129905829