写在前面
1.之前存书签和leetcode笔记太乱了
2.链接不一定最佳,这东西能看就行
3. 不可告人的链接
https://www.book123.info/detail/9787302609759
4. 超级刷题攻略
5. 刷题方法论
根据 超级刷题攻略的精记录 快速过的题(简单题 or重复题类型3道以上or有很快有思路题且对)不在列
数组
二分法
- 需要排序
- 注意边界也有合并之说
快慢指针
- 边界问题 使用缓慢移动slow比快速移动quick好判断边界问题 for循环
滑动窗口
https://leetcode.cn/problems/minimum-size-subarray-sum/ 也一样 如果不在for循环里体现边界就需要额外的处理 用if加continue方法比较好
链表
删除
- 区分头节点
- 用next处理更省事
- 用虚拟头节点
其他
哈希表
什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
计数记重
- 使用数组也可以
字符
-反转 删除 各种 不少双指
- 反转单词也可stack
-kpm暂时不看
双指针
栈
- 对称问题
- 单调队列 优先队列
二叉树
- 有时候需要传入两个数root 比如对称问题
- 迭代思路三大步
- 树的构造前序
- 二叉搜索 中序遍历为递增 递归stack版本可以用指针
- 叶子节点判断方式cur->leftnullptr&&cur->rightnullptr
- 经典https://programmercarl.com/0112.%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C.html#%E6%80%9D%E8%B7%AF
- 什么时候要返回值什么时候不要https://programmercarl.com/0236.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88.html#%E6%80%9D%E8%B7%AF
- 使用形参可以免受回溯之苦,递归可以先进再出也可以先出再进
贪心
-局部全局最优 各题有不同的解法
回溯
为了控排列和组合的区别:每层都是从0开始搜索而不是startIndex
为了控ans里不能有重复(数组里有重复的数字)需要used数组记录path里都放了哪些元素了
*后俩发现 used数组 首先要能支持排序 其次 对于组合问题 一般是树层去重i>0 used[i-1]=false nums[i-1]==nums,而枝去重 在startindex一般做,如果可以重复使用 startindex不++。对于排列问题 startindex不用啦且 需要用used 来continue 如果 有成员 重复且结果不能重复还要额外层去重。
至于电话问题是个个例。
for循环的层数如何写出来,此时又是回溯法登场的时候了。
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new
window),216.组合总和III (opens new window)。如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合
注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我在讲解排列的时候会重点介绍。
大家此时可以感受出排列问题的不同:为了控排列和组合的区别:每层都是从0开始搜索而不是startIndex
为了控ans里不能有重复(数组里有重复的数字)需要used数组记录path里都放了哪些元素了这道题目和46.全排列 (opens new window)的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列。
这里又涉及到去重了。
在40.组合总和II (opens new window)、90.子集II (opens new
window)我们分别详细讲解了组合问题和子集问题如何去重。那么排列问题其实也是一样的套路。
还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
- 中止条件深度可以由控sum也可以由题目给出深度控制,画一个例子好看
- backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次
- -切割问题类似 本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。
- -如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
-这也是需要注意的点,unordered_set uset; 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!
以及使用set的时候要注意是否会影响遍历,数组靠谱-首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
但排列问题需要一个used数组,标记已经选择的元素
-去重是题目中有重复数字还是结果不可以重复
可以用这道题 用我这段代码 修改 i=0 或者i=start 来验证
为了控排列和组合的区别:每层都是从0开始搜索而不是startIndex
数组中有重复还去重则使用
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
为了控ans里不能有重复(数组里有重复的数字)需要used数组记录path里都放了哪些元素了 同时continue 要最前
数字可以无限制重复被选取的话就每一次startindex不加
class Solution {
public:
vector <vector <int >> ans;
vector <int> ans_t;
void backtracking (const vector<int>& nums,int & sum, vector<int> & dir,int &start,int & k,int target )
{
if (k==4)
{
if (sum==target)
{
ans.emplace_back(ans_t);
return;
}
else
{
return;
}
}
for (int i =start; i<nums.size();++i)
{
if (i>0 && dir[i-1]==0 &&nums[i]==nums[i-1] )
{
continue;
}
dir [i]=1;
k+=1;
sum+=nums[i];
ans_t.emplace_back(nums[i]);
i=i+1;
backtracking( nums,sum,dir,i,k,target);
i=i-1;
k-=1;
sum-=nums[i];
dir [i]=0;
ans_t.pop_back();
}
}
vector<vector<int>> fourSum(vector<int>& nums, int target) {
int size = nums.size();
if (size<4)
{
return ans;
}
int k=0;
int sum=0;
int start=0;
sort(nums.begin(),nums.end());
vector <int> dir (size,0);
backtracking ( nums,sum,dir,start,k,target);
return ans;
}
}; return ans;
}
int k=0;
int sum=0;
int start=0;
sort(nums.begin(),nums.end());
vector <int> dir (size,0);
backtracking ( nums,sum,dir,start,k,target);
return ans;
}
};
动态规划
步骤: 1 确定可以使用后,确定dp含义 2确定递推式分类讨论 3初始化 4确定遍历范围注意特别点 5找出返回点
背包问题 https://leetcode.cn/problems/last-stone-weight-ii/solutions/805162/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-5lfv/
问题转化的时候 留意一下可以预先return掉一些情况
背包最大值和装到特定值背包的方式递推式有一点差别
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
背包的数量问题dp[j] += dp[j - nums[i]];
打劫问题
分类讨论
股票问题
多维动态规划
子序列问题
经常要考虑到dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。
更细问题 上升重复子序列 回文
回文问题
图
bfs dfs 对于岛屿问题都可以 标注类型
否则 dfs适用于数量 bfs适用于最短
如果是为了标注的话不需要回溯
CPP 八股
大部分参见 以及gpt回答
智能指针
#include <memory>
unique_ptr<int>z ((int*)malloc(sizeof(int)));
unique_ptr<int>x (new int (5));
P2 = move(x);
auto y =make_shared<int>(5);
#include <string>
#include <memory>
#include <iostream>
using namespace std;
int main()
{
auto a =make_shared<int> (5);
shared_ptr<int> b(new int(5));
int* c = (int*)malloc(5 * sizeof(int));
c[3]=1;
if (c != nullptr) {
cout << "Value of c: " << c[3] << endl;
free(c);
}
else
{
cerr << "Memory allocation failed." << endl;
}
int * x= new int [5];
delete [] x;
}
当我们使用 new 时,我们将 new 出的资源指针赋给 share_ptr 的 ptr, 然后 share_ptr 本身还需要再次在堆上申请一块单独的内存作为它的管理区,存放引用计数、用户自定的函数等,因此创建 shared_ptr 时需要在堆上申请两次。
new delete
int* c= new int (5);
delete c;
int* a=new int[5];
delete [] a;
11
volatile
In C++, the volatile keyword is used to indicate that a variable’s value might be changed by external sources that are beyond the control or understanding of the compiler.
When you declare a variable as volatile, you are essentially telling the compiler that it should not perform certain optimizations on that variable.
auto decltype
编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。
constexpr:
常量表示式对编译器来说是优化的机会,编译器时常在编译期执行它们并且将值存入程序中。同样地,在许多场合下,C++ 标准要求使用常量表示式。例如在数组大小的定义上,以及枚举值(enumerator values)都要求必须是常量表示式。常量表示式不能含有函数调用或是对象构造函数。所以像是以下的例子是不合法的:
static 成员
// C++ program to demonstrate static
// variables inside a class
#include
using namespace std;
class GfG
{
public:
static int i;
GfG() {
};
};
int GfG::i = 1; // initial
int main()
{
GfG obj1;
GfG obj2;
obj1.i =2; // error
obj2.i = 3; // error
GfG::i = 10; // assignment
// prints value of i
cout << obj1.i<<" "<<obj2.i; // 10
}
类中的const
#include <iostream>
using namespace std;
class A
{
public:
int var;
A(int tmp) : var(tmp) {}
void c_fun(int tmp) const // const 成员函数
{
var = tmp; // error: assignment of member 'A::var' in read-only object. 在 const 成员函数中,不能修改任何类成员变量。
fun(tmp); // error: passing 'const A' as 'this' argument discards qualifiers. const 成员函数不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
}
void fun(int tmp)
{
var = tmp;
}
};
int main()
{
return 0;
}
区别系列
const define
编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。
安全性:define 定义的宏常量没有数据类型,只是进行简单的代码替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。
存储空间:define 定义的宏定义只是作为代码替换的表达式而已,宏定义本身不占用内存空间,define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,实际使用宏定义替换代码时占用的是代码段的空间;const 定义的常量占用静态存储区的只读空间,程序运行过程中常量只有一份。
调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;const 定义的常量可以进行调试。
define 可以接受参数构造非常复杂的表达式,const 不能接受参数。
inline define
编译期间,虚函数实在运行期间 不可以inline virtual
内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销,在编译后的代码段中可以看到内联函数的定义。宏定义编写较为复杂,常需要增加一些括号来避免歧义。宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查,因此在实际使用宏时非常容易出错。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。
内联函数可以进行调试,宏定义的“函数”无法调试。
由于类的成员函数全部为内联函数,通过内联函数,可以访问类的数据成员,而宏不能访问类的数据成员。
在 inline 函数传递参数只计算一次,而在使用宏定义的情况下,每次在程序中使用宏时都会计算表达式参数,因此宏会对表达式参数计算多次。
define typedef
二者之间的区别:
#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。
typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,如果在 .cpp 文件中定义了宏,则在整个文件中都可以使用该宏,如果在 .h 文件中定义了宏,则只要包含该头文件都可以使用;而 typedef 有自己的作用域,如果在函数之外定义了类型,则在整个文件中都可以使用该类型定义,如果在函数内部定义了该类型,则只能在函数内部使用该类型。
指针的操作:typedef 和 #define 在处理指针时不完全一样。比如以下程序:
struct class
C++ 中为了兼容 C 语言而保留了 C 语言的 struct 关键字,并且加以扩充。在 C 语言中,struct 只能包含成员变量,不能包含成员函数。而在 C++ 中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
C++ 中的 struct 和 class 基本是通用的,唯有几个细节不同:
class 中类中的成员默认都是 private 属性的;而在 struct 中结构体中的成员默认都是 public 属性的。
class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
class 可以用于定义模板参数,struct 不能用于定义模板参数。
struct 可以具有构造函数、析构函数以及实现动态多态的特性,与 class 没有本质区别。构造函数用于对象的初始化,析构函数用于对象的清理和释放资源,而动态多态性通常通过虚函数来实现。
###重载、重写、隐藏的区别
重载
class A
{
public:
void fun(int tmp);
void fun(float tmp); // 重载 参数类型不同(相对于上一个函数)
void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
};
隐藏
```cpp
#include <iostream>
using namespace std;
class Base
{
public:
void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
};
class Derive : public Base
{
public:
void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};
int main()
{
Derive ex;
ex.fun(1); // Derive::fun(int tmp)
ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
return 0;
}
重写
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; }
};
class Derived : public Base
{
public:
virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
Base *p = new Derived();
p->fun(3); // Derived::fun(int) : 3
return 0;
}
隐藏是发生在编译时,即在编译时由编译器实现隐藏,而重写一般发生运行时,即运行时会查找类的虚函数表,决定调用函数接口。
构造
GPT A:
在C++中,类的拷贝构造函数用于创建一个对象作为另一个对象的副本。拷贝构造函数通常接受一个对同类型对象的引用作为参数。它的一般形式如下:
cpp
class ClassName {
public:
ClassName(const ClassName &other) {
// 构造函数体:复制other的数据到新创建的对象
}
};
这里,ClassName是类的名字,other是对另一个ClassName类型对象的引用。在拷贝构造函数体内部,您应该复制other对象的所有属性到新创建的对象。
例如,假设有一个简单的Point类,它有两个成员变量x和y:
cpp
class Point {
public:
int x, y;
// 默认构造函数
Point() : x(0), y(0) {}
// 带参数的构造函数
Point(int x, int y) : x(x), y(y) {}
// 拷贝构造函数
Point(const Point &other) : x(other.x), y(other.y) {
// 在这里,我们直接复制了other对象的x和y属性
}
};
在上述例子中,拷贝构造函数接受一个Point类型的对象引用other,并将其成员x和y的值复制到新创建的对象中。
通常,如果您没有为类定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,它会逐成员地复制对象(浅拷贝)。但是,如果类中包含指针或动态分配的资源,您应该显式定义拷贝构造函数以实现深拷贝,以避免潜在的内存问题。
l
加上 explicit 会不会好一些
在C++中,给拷贝构造函数添加explicit关键字通常是不合适的,也是非标准的。explicit关键字主要用于防止构造函数的隐式转换。它通常用于单参数的构造函数或者除了第一个参数外,其余参数都有默认值的构造函数。其目的是防止编译器自动地将单个值转换为该类型的对象。
拷贝构造函数的典型声明是接受一个同类对象的引用作为参数。它的目的是用一个已存在的对象来初始化新的对象。在这种情况下,没有进行类型转换,因此使用explicit是不必要的,也不符合其设计意图。
例如:
cpp
class ClassName {
public:
explicit ClassName(int a) {
// 这里可以使用explicit,因为存在从int到ClassName的隐式转换
}
ClassName(const ClassName &other) {
// 这里使用explicit是不合适的,也通常不被允许
}
};
总结来说,对于拷贝构造函数而言,添加explicit关键字不仅不会带来好处,而且不符合C++的标准用法。拷贝构造函数的目的是从同类型的另一个对象创建对象,这种场景下不涉及隐式类型转换。
继承
https://zhuanlan.zhihu.com/p/70758317
QKF Q:
private和protected在继承中有什么用 我指的是继承 我都用 public 继承不就好啦?
GPT A:
在C++中,使用private、protected和public进行继承,会影响派生类如何访问基类的成员。这三种继承方式具有不同的含义和用途:
public 继承:
这是最常用的继承方式。它意味着“派生类是基类的一种”。public继承保持了基类成员的访问级别:基类的public成员在派生类中仍然是public的,protected成员仍然是protected的。
public继承通常用于表示“is-a”(是一种)关系。例如,如果你有一个Animal类,一个Dog类可以公开继承Animal,因为狗是一种动物。
protected 继承:
在protected继承下,基类的public和protected成员在派生类中变成protected。这意味着它们对派生类的子类是可见的,但对外部代码不可见。
这种继承方式较少使用,但它可以在特定场景下用来限制对某些成员的访问,同时允许进一步派生。
private 继承:
在private继承中,基类的public和protected成员在派生类中变成private。这意味着它们只能在派生类内部访问,即使是派生类的子类也无法访问。
private继承可以被看作是一种实现细节的复用,而不是表示“is-a”关系。它更像是一种强化的组合。
总结一下,虽然总是使用public继承可能看起来简单直接,但它并不总是最佳选择。选择继承类型取决于你想要表达的关系类型和你对基类成员的访问需求。恰当地使用protected和private继承可以提供更精确的控制,有助于更好地封装代码和维护类的层次结构。
constexpr
constexpr 是 C++11 引入的关键字,用于声明能够在编译期求值的常量表达式。它可以用于变量、函数以及构造函数,使得这些表达式能够在编译时就计算得到结果。以下是一些使用 constexpr 的常见情况:
编译期计算的常量: 当你希望某个变量在编译期就能够得到计算结果时,你可以使用 constexpr。这样做可以提高程序的性能,因为这些值可以在编译时被计算,而不是在运行时。
cpp
constexpr int square(int x) {
return x * x;
}
int main() {
constexpr int result = square(5); // 在编译期计算
// ...
return 0;
}
//数组的大小: 在 C++11 之前,数组的大小必须是常量表达式。使用 constexpr 可以在 C++11 及以后版本中使用非常量表达式来定义数组的大小。
cpp
const int size = 5;
int array[size]; // 合法,size 是常量表达式
int main() {
constexpr int newSize = 10;
int newArray[newSize]; // 合法,newSize 是常量表达式
// ...
return 0;
}
//构造函数: 在 C++11 中,构造函数也可以声明为 constexpr,表示该构造函数可以在编译期执行。这对于在编译期初始化对象是有用的。
cpp
class MyClass {
public:
constexpr MyClass(int val) : value(val) {}
int getValue() const { return value; }
private:
int value;
};
int main() {
constexpr MyClass obj(42); // 在编译期初始化对象
// ...
return 0;
}
多态系列
https://leetcode.cn/leetbook/read/cmian-shi-tu-po/vwz3sj/
细节https://leetcode.cn/leetbook/read/cmian-shi-tu-po/vwcp3h/
构造函数不能为虚函数:构造函数不能定义为虚函数。构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
静 动区别
编译时多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。实际在编译器内部看来不管是重载还是模板,编译器内部都会生成不同的函数,在代码段中分别装有两个函数的不同实现。
运行时多态:运行时多态也称动态绑定,在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。
编译时多态和运行时多态的区别:
时期不同:编译时多态发生在程序编译过程中,运行时多态发生在程序的运行过程中;
实现方式不同:编译时多态运用泛型编程来实现,运行时多态借助虚函数表来实现。
左值右值
左值:指表达式结束后依然存在的持久对象。可以取地址,可以通过内置(不包含重载) & 来获取地址,我们可以将一个右值赋给左值。
右值:表达式结束就不再存在的临时对象。不可取地址,不可以通过内置(不包含重载) & 来获取地址。由于右值不可取地址,因此我们不能将任何值赋给右值。
作者:LeetCode
链接:https://leetcode.cn/leetbook/read/cmian-shi-tu-po/vd00s1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
STL
好的总集合
https://blog.csdn.net/qq_50285142/article/details/114026148
https://blog.csdn.net/qq_37529913/article/details/120052137
哪些有 pop_back
哪些stl有pop back
在 C++ 的标准模板库(STL)中,有几个容器支持 pop_back 函数。这个函数用于移除容器中的最后一个元素。支持 pop_back 的 STL 容器包括:
std::vector: 动态数组,可以在尾部快速添加或移除元素。
std::deque: 双端队列,支持在头部和尾部快速添加或移除元素。
std::list: 双向链表,允许在任何位置快速插入和删除元素。
std::string: 字符串类,其实是一个字符容器,支持快速在尾部添加或移除字符。
这些容器使用 pop_back 来移除其尾部的最后一个元素。这通常是一个快速操作,不需要移动或复制容器中的其它元素。不过,值得注意的是,使用 pop_back 之前应确保容器不是空的,否则可能导致未定义行为。对于 std::vector 和 std::string,在调用 pop_back 之前可以使用 empty() 函数来检查容器是否为空。对于 std::list 和 std::deque,同样适用。
关于find count begin end
和gpt的几个问答
其实还是用关联是和顺序储存来记忆比较好
没有 find 成员函数的容器:
std::vector: 动态数组,支持快速随机访问,但没有专用的 find 成员函数。
std::list: 双向链表,同样没有 find 成员函数。
std::forward_list: 单向链表,没有 find 成员函数。
std::deque: 双端队列,也没有 find 成员函数。
std::array: 固定大小数组,没有 find 成员函数。
对于这些容器,你可以使用非成员函数 std::find 来进行元素查找。
没有 count 成员函数的容器:
std::vector: 不提供 count 成员函数。
std::list: 同样不提供 count 成员函数。
std::forward_list: 不提供 count 成员函数。
std::deque: 不提供 count 成员函数。
std::array: 不提供 count 成员函数。
对于这些容器,你可以使用非成员函数 std::count 来计算特定值的出现次数。
没有 begin 和 end 成员函数的容器:
从C++11开始,所有标准容器类型(std::vector、std::list、std::forward_list、std::deque、std::set、std::map、std::multiset、std::multimap、std::array、std::unordered_set、std::unordered_map、std::unordered_multiset、std::unordered_multimap)都提供了 begin 和 end 成员函数。
哪一些没有insert
在 C++ 标准模板库(STL)中,不同类型的容器对 insert 成员函数的支持也各不相同。以下是没有 insert 成员函数的容器:
std::array:
由于 std::array 的大小是固定的,因此它不支持 insert 操作。std::array 在定义时就确定了大小,无法在运行时改变。
普通数组(C风格数组):
普通数组也是固定大小的,因此无法支持 insert 操作。一旦定义,其大小和存储空间就固定下来了。
适配器类容器:
std::stack:作为栈,它只支持从顶部添加或删除元素的操作,没有 insert 成员函数。
std::queue:作为队列,它只支持从一端添加元素,从另一端删除元素,没有 insert 成员函数。
std::priority_queue:作为优先队列,它允许插入元素和删除最高(或最低)优先级的元素,但不提供任意位置的 insert 功能。
这些容器或者因为大小固定(如 std::array 和普通数组),或者因为它们的设计目的(如栈、队列和优先队列)不适合或不需要提供 insert 功能。对于需要动态大小和任意位置插入元素的场景,通常会使用如 std::vector、std::list、std::deque、std::set、std::map 等容器,这些容器提供了灵活的 insert 操作。
string
vector
关于列表返回值初始化
在 C++ 中,大括号 {} 作为初始化列表(Initializer List)确实可以用于初始化不同的类型,包括数组、集合(如 std::set)、向量(如 std::vector)等。然而,它们在不同上下文中的具体意义可能会有所不同。具体类型取决于以下因素:
上下文:编译器会根据上下文来决定 {} 应该如何解释。例如,在一个预期返回 std::vector 的函数中,{} 会被解释为一个空的 std::vector。如果上下文预期是一个数组,那么 {} 就会被解释为一个空的数组。
类型推断:编译器利用类型推断来确定 {} 应该代表什么类型。例如,如果一个变量被声明为 std::vector, 那么使用 {} 初始化它时,编译器会理解为你想要一个空的 std::vector。
构造函数的存在:对于像 std::set 这样的容器,如果使用 {} 初始化,编译器会查找是否有接受初始化列表的构造函数。std::set 有一个接受初始化列表的构造函数,所以你可以用 {} 来初始化一个 std::set,但它将被解释为一个具有初始化元素的集合。
在您的例子中,return {}; 出现在一个返回类型为 vector 的函数中。因此,编译器理解这意味着返回一个空的 vector。如果这个语句出现在另一个预期返回不同类型的函数中,{} 的含义可能会有所不同。例如,在一个返回 std::array<int, 5> 的函数中,return {}; 会返回一个元素都为零的数组。
关于初始化
- 默认初始化
cpp
std::vector vec; // Creates an empty vector of integers. - 填充固定值
cpp
std::vector vec(10, 5); // Creates a vector of 10 integers, each initialized to 5. - 列表初始化(C++11及以上)
cpp
std::vector vec = {1, 2, 3, 4, 5}; // Initializes vector with a given list of integers. - 用数组初始化
cpp
int arr[] = {1, 2, 3, 4, 5};
std::vector vec(arr, arr + sizeof(arr) / sizeof(arr[0])); // Initializes vector with an array. - 复制另一个vector
cpp
std::vector vec1 = {1, 2, 3, 4, 5};
std::vector vec2(vec1); // Copy constructor.
std::vector vec3 = vec1; // Copy assignment. - 使用迭代器范围
cpp
std::vector vec1 = {1, 2, 3, 4, 5};
std::vector vec2(vec1.begin(), vec1.end()); // Initializes with elements from vec1. - 使用移动构造函数(C++11及以上)
cpp
std::vector vec1 = {1, 2, 3, 4, 5};
std::vector vec2(std::move(vec1)); // vec1 is now empty, and its contents have been moved to vec2. - 初始化为空并后续赋值
cpp
std::vector vec;
vec = {1, 2, 3, 4, 5}; // Assigning values after declaration.
链表
- 要注意头和尾的处理
- 链表迭代 用单个指针的话 可以用cur->next 和再next来判断
栈
- 发现倒叙的东西可以试一试stack的方法
https://leetcode.cn/problems/SLwz0R/solutions/1417848/shan-chu-lian-biao-de-dao-shu-di-n-ge-ji-ydte/
priority_queue
- 很好的一个练习https://leetcode.cn/problems/top-k-frequent-elements/ 同时对比这里没有加()https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/
在C++中,priority_queue 的模板参数是一个类型,而不是一个对象。在你的代码中,com 是一个类型,而不是对象,因此你不应该在使用 priority_queue 的时候加上括号。
正确的方式是:
cpp
priority_queue<pair<int, int>, vector<pair<int, int>>, com> hel;
这里,com 是作为类型传递给 priority_queue 模板的,而不是作为一个对象。不同于某些情况下需要创建对象并使用括号调用构造函数,priority_queue 在这里只需要 com 作为类型名即可。
- 如果自定义则必须实现一个underlying container
is it necessary to offer a underlying container for priority_queue?
Yes, it is necessary to specify an underlying container for std::priority_queue in C++. The third template parameter of std::priority_queue is the underlying container type, and it must be specified.
The reason for this is that std::priority_queue is implemented as an adapter around a container, and the choice of container affects the efficiency of certain operations. By default, std::priority_queue uses std::vector as the underlying container.
Here’s the typical declaration of std::priority_queue:
cpp
template<
class T,
class Container = std::vector,
class Compare = std::less
class priority_queue;
T: Type of the elements.
Container: Type of the underlying container (default is std::vector).
Compare: Type of the comparison function (default is std::less).
If you don’t explicitly provide the underlying container type, it defaults to std::vector. However, you still need to specify the type of elements (T) and, if necessary, the type of the comparison function (Compare).
For example, if you want to use a std::deque as the underlying container:
cpp
std::priority_queue<int, std::deque> myPriorityQueue;
So, while you can rely on the default (std::vector), it’s a good practice to be explicit about your choices to avoid any confusion or unintentional changes in the future.
单调栈、优先队列和单调队列是不同的概念,它们在数据结构和算法中有不同的应用和特性。
单调栈(Monotonic Stack):
单调栈是一种特殊的栈数据结构,其元素按照单调递增或单调递减的顺序排列。
主要用于解决一些与区间最值、下一个更大元素、下一个更小元素等问题。
在处理元素时,可以维护一个单调递增栈或单调递减栈,以便在栈中保存有用的信息。
优先队列(Priority Queue):
优先队列是一种抽象数据类型,支持插入和删除最大/最小元素的操作,并确保这些操作的时间复杂度较低。
常见实现包括二叉堆、斐波那契堆等。C++中的 std::priority_queue 是一个基于二叉堆的优先队列。
主要用于解决一些与优先级相关的问题,如最小(大)堆问题。
单调队列(Monotonic Queue):
单调队列是一种特殊的队列数据结构,其元素满足单调递增或单调递减的顺序。
常用于解决一些与窗口最值、滑动窗口等问题。
在处理元素时,可以维护一个单调递增队列或单调递减队列。
尽管这些数据结构在名称上有相似之处,但它们有不同的应用场景和解决不同类型问题的能力。选择使用哪种数据结构取决于问题的性质和要求。
map
multimap 初始化要注意 得用insert 没办法用键值的形式
我的问题是为什么再定义myMap的时候不写成map<pair<int ,int >>呢
当定义 std::map 时,你指定键和值的类型,而不需要使用 std::pair 作为键的类型。这是因为 std::map 会自动将键值对作为元素进行存储。在 std::map 中,键是唯一的,因此你只需指定键和值的类型。
如果你有一个 std::map 的键和值都是 int 类型的情况,你可以直接使用 std::map<int, int>,而无需使用 std::pair<int, int> 作为键的类型。这样的写法更为简洁,符合 std::map 的设计。
以下是一个例子:
cpp
#include
#include
int main() {
// 直接指定键和值的类型
std::map<int, int> myMap;
// 插入键值对
myMap.insert(std::make_pair(42, 100));
myMap.insert(std::make_pair(23, 200));
// 输出 map 的内容
for (const auto& pair : myMap) {
std::cout << "Key: " << pair.first << ", Value: " << pair.second << std::endl;
}
return 0;
}
在这个例子中,我们直接定义了 std::map<int, int>,并插入了一些键值对。这样的语法更为简洁,更容易理解。 std::pair 在这里主要用于 make_pair 用来生成临时键值对对象。
l
如果 vector内存放的是pair 可以写成 vector <int,int > 吗
在C++中,std::vector 是一个容器,而 std::pair 是一个模板类,表示两个值的组合。如果你想要在 std::vector 中存放 std::pair 对象,你需要使用 std::pair 的模板参数,而不能直接使用 vector<int, int>。
以下是一个示例,演示如何在 std::vector 中存放 std::pair:
cpp
#include
#include
#include
int main() {
// 使用 std::pair 存放在 std::vector 中
std::vector<std::pair<int, int>> myVector;
// 插入 std::pair 对象
myVector.push_back(std::make_pair(1, 10));
myVector.push_back(std::make_pair(2, 20));
myVector.push_back(std::make_pair(3, 30));
// 输出 vector 的内容
for (const auto& pair : myVector) {
std::cout << "Pair: {" << pair.first << ", " << pair.second << "}\n";
}
return 0;
}
在这个例子中,我们使用 std::pair<int, int> 作为 std::vector 的元素类型。每个元素都是一个包含两个整数的 std::pair 对象。
总的来说,如果你想在 std::vector 中存放 std::pair,就需要指定 std::pair 的模板参数。直接使用 vector<int, int> 是不合法的,因为 vector 的模板参数应该是元素类型,而不是元素的模板参数。
l
总结 map可以写成 map<int,int> 而其他数据容器要写成 比如 vector <pair<int,int>>
是的,你总结得很正确。在C++中,不同的数据容器有不同的元素存储方式,因此在定义它们时需要使用不同的语法。
std::map: 用于键值对存储,键和值的类型是分开指定的。例如,std::map<int, int> 表示键和值都是整数类型。
cpp
std::map<int, int> myMap;
std::vector: 用于存储一系列元素,如果你想存储 std::pair 对象,需要指定 std::pair 的模板参数。例如,std::vector<std::pair<int, int>> 表示存储整数对的向量。
cpp
std::vector<std::pair<int, int>> myVector;
其他容器如 std::list、std::set 等也遵循类似的规则。你需要根据元素的类型来选择正确的语法。这种灵活性使得C++容器能够满足不同的需求,同时提供了清晰的语法,使代码更易于理解。
queue
std::stack、std::queue 和 std::priority_queue:这些都是容器适配器,通常不提供 begin 和 end 方法,因为它们提供了特定的数据结构行为(如栈、队列等)。
题目算法
Debug (特指tmd刷题)
- 可以删除一半一半的来看
- 正常debug
- 常见错误
递归
- 终末条件
- 迭代体输入输出的东西
树
- 见我的leet书留言
- 有时候题目本身给出的函数不能迭代,自己再建立一个
- 有时候要考虑一下直接使用返回值
- 树的路径系列问题 树的路径系列问题
DP
递归一般是自顶向下,依赖于子问题优化函数的结果,只有子问题完全求出,也就是子问题的递归返回结果,原问题才能求解。多使用递归,从最大开始到开始。使用n和最小限制对比来退出。
迭代法,从最小的子问题开始,自下而上求解。每次求新的问题时,子问题的解已经计算出来了。多使用循环来处理从最小问题(计数开始)
迭代:有辅助数组或者变量法
双指针
- 关键是怎么找到移动条件
排序
https://blog.csdn.net/guorui_java/article/details/122972781
自己书写好记忆版
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int size=nums.size();
for(int i=1;i<size;++i)// index wait to com
{
if (nums[i]<nums[i-1])
{
int temp=nums[i];
int j=i;
for(;(j-1)>=0&&temp<nums[j-1];--j)
{
nums[j]=nums[j-1];
}
nums[j]=temp;
}
}
return nums;
}
};
///
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int size=nums.size();
int minindex;
for (int i=0;i<size;++i)
{
minindex=i;
for (int j=i;j<size;++j)
{
if (nums[minindex]>nums[j])
{
minindex=j;
}
}
swap(nums[minindex],nums[i]);
}
return nums;
}
};
//
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int size=nums.size();
for (int i=0;i<size-1;++i)
{
bool flag =false;
for (int j=0;j<size-i-1;++j)
{
if (nums[j]>nums[j+1])
{
swap(nums[j],nums[j+1]);
flag=true;
}
}
if (false==flag)
return nums;
}
return nums;
}
};
//
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int size=nums.size();
for(int gap=size;gap>0;gap/=2)
{
for(int i=gap;i<size;++i)// index wait to com
{
if (nums[i]<nums[i-1])
{
int temp=nums[i];
int j=i;
for(;(j-gap)>=0&&temp<nums[j-gap];j-=gap)
{
nums[j]=nums[j-gap];
}
nums[j]=temp;
}
}
}
return nums;
}
};class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int size =nums.size();
for (int gap=size-1;gap>0;gap/=2)
{
for (int i=gap;i<size;++i)
{
if (nums[i]<nums[i-gap])
{
int temp =nums[i];
int j=i;
for (;j-gap>=0&&temp<nums[j-gap];j-=gap)
{
nums[j]=nums[j-gap];
}
nums[j]=temp;
}
}
}
return nums;
}
};
//
class Solution {
vector<int> temp; // 声明一个辅助数组 temp,用于在归并排序中暂存排序结果
public:
void mergeSort(vector<int>& nums, int l, int r); // 归并排序函数的声明
vector<int> sortArray(vector<int>& nums) {
temp.resize(nums.size(), 0); // 初始化辅助数组 temp,大小与输入数组相同
mergeSort(nums, 0, nums.size() - 1); // 调用归并排序函数对整个数组进行排序
return nums; // 返回排序后的数组
}
};
void Solution::mergeSort(vector<int>& nums, int l, int r) {
if (l >= r)
return; // 如果当前子数组的长度为 1 或 0,则已经有序,无需继续分解
int mid = l + (r - l) / 2; // 计算数组的中间位置
// 递归地对左右两部分进行归并排序
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
int i = l, j = mid + 1; // 设置两个指针 i 和 j 分别指向左右两部分的起始位置
int cnt = 0; // 计数器,用于记录辅助数组 temp 中已经放置了多少个元素
// 归并过程:比较左右两部分的元素,将较小的元素放入辅助数组 temp 中
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) {
temp[cnt] = nums[i]; // 将左部分的元素放入辅助数组 temp
cnt++;
i++;
} else {
temp[cnt] = nums[j]; // 将右部分的元素放入辅助数组 temp
cnt++;
j++;
}
}
// 将剩余的左部分元素放入辅助数组 temp
while (i <= mid) {
temp[cnt] = nums[i];
cnt++;
i++;
}
// 将剩余的右部分元素放入辅助数组 temp
while (j <= r) {
temp[cnt] = nums[j];
cnt++;
j++;
}
// 将辅助数组 temp 中的元素复制回原数组 nums 的对应位置
for (int q = 0; q < r - l + 1; q++) {
nums[q + l] = temp[q];
}
}
///
class Solution {
void quickSort(vector<int>& nums, int startIndex, int endIndex) {
if (startIndex >= endIndex) return;
int x = rand() % (endIndex - startIndex + 1) + startIndex; // 基于随机的原则
swap(nums[startIndex], nums[x]);
int firstNum = nums[startIndex];
int l = startIndex, r = endIndex;
while (l < r) {
// 从后往前走,将比第一个小的移到前面
while (l < r && nums[r] >= firstNum) --r;
if (l < r) {
nums[l] = nums[r];
}
// 从前往后走,将比第一个大的移到后面
while (l < r && nums[l] <= firstNum) ++l;
if (l < r) {
nums[r] = nums[l];
}
}
nums[l] = firstNum;
// 自顶向下
quickSort(nums, startIndex, l - 1);
quickSort(nums, l + 1, endIndex);
}
public:
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
quickSort(nums, 0, n - 1);
return nums;
}
};
/*way 2*/
class Solution {
public:
void quicksort(vector<int>& nums, int l, int r) {
if (l >= r) return;
int x = rand() % (r - l + 1) + l;
int value = nums[x];
swap(nums[x], nums[l]);
int left = l, right = r;
while (left < right) {
while (left < right && value <= nums[right]) right--;
while (left < right && value >= nums[left]) left++;
if (left < right) swap(nums[left], nums[right]);
}
swap(nums[l], nums[left]);
quicksort(nums, l, left - 1);
quicksort(nums, left + 1, r);
}
vector<int> sortArray(vector<int>& nums) {
int size = nums.size();
quicksort(nums, 0, size - 1);
return nums;
}
};
//way3
class Solution {
public:
void quicksort(vector<int>& nums, int l, int r) {
if (l >= r) return;
int pivot = nums[l]; // 选择第一个元素作为基准值
int i = l + 1, j = r;
while (i <= j) {
while (i <= j && nums[i] <= pivot) i++; // 从左向右找到第一个大于基准的元素
while (i <= j && nums[j] > pivot) j--; // 从右向左找到第一个小于基准的元素
if (i < j) swap(nums[i], nums[j]); // 交换这两个元素,使得小于基准的在左边,大于基准的在右边
}
swap(nums[l], nums[j]); // 将基准值交换到中间的正确位置
quicksort(nums, l, j - 1); // 对基准值左边的元素进行快速排序
quicksort(nums, j + 1, r); // 对基准值右边的元素进行快速排序
}
vector<int> sortArray(vector<int>& nums) {
quicksort(nums, 0, nums.size() - 1);
return nums;
}
};
}
滑动窗口
- 如果是使用区间来计算长度的话,则要注意right++的位置情况,for ok while 写在最后
去除尾部空格
- substr(0,res.size()-1);
while (!container.empty()) { ans += container.top(); container.pop(); if (!container.empty()) ans += ' '; }
下标推导界限
- 第一种 由递推关系出下标 由下标和数据大小定下标的上下界限
- 第二种 : ?
字符串和数字的互相转化
cpp 字符串和数字互相转化函数
在 C++ 中,字符串和数字之间的互相转换可以通过多种方式实现。下面列出了一些常用的方法:
数字转字符串:
使用 std::to_string 函数(C++11及更高版本):
cpp
int num = 123;
std::string str = std::to_string(num);
使用 std::stringstream:
cpp
int num = 123;
std::stringstream ss;
ss << num;
std::string str = ss.str();
字符串转数字:
使用 std::stoi(转换为 int)、std::stol(转换为 long)、std::stoll(转换为 long long)、std::stod(转换为 double)等函数(C++11及更高版本):
cpp
std::string str = “123”;
int num = std::stoi(str);
使用 std::istringstream:
cpp
std::string str = “123”;
std::istringstream iss(str);
int num;
iss >> num;
使用 std::atoi(转换为 int)、std::atol(转换为 long)等函数:
cpp
std::string str = “123”;
int num = std::atoi(str.c_str());
注意:
std::to_string 和 std::stoi 等函数是 C++11 新增的,它们提供了更简洁的方式来进行转换。
使用 std::stringstream 或 std::istringstream 可以提供更多的灵活性,尤其是在处理复杂格式的字符串时。
使用 std::atoi 和 std::atol 时,需要注意它们不提供错误检查,如果输入的字符串不是有效的数字格式,它们可能返回不确定的结果。
回溯
多线程
target_link_libraries(${PROJECT_NAME} PRIVATE pthread) cmakelist
来自于 https://zhuanlan.zhihu.com/p/613630658
建议看这个
https://blog.csdn.net/qq_36784975/article/details/87699113
std::thread:表示一个线程对象。
std::mutex:表示互斥量,用于保护共享资源。
std::condition_variable:表示条件变量,用于线程之间的通信。
std::atomic:表示原子类型,用于实现无锁数据结构。
std::async:表示异步操作,可以在另一线程中异步地执行函数。
join()函数是一个等待线程函数,主线程需等待子线程运行结束后才可以结束(注意不是才可以运行,运行是并行的),如果打算等待对应线程,则需要细心挑选调用join()的位置
detach()函数是子线程的分离函数,当调用该函数后,线程就被分离到后台运行,主线程不需要等待该线程结束才结束
https://www.zhihu.com/tardis/zm/art/194198073?source_id=1005
等待调用线程运行结束后当前线程再继续运行,例如,主函数中有一条语句th1.join(),那么执行到这里,主函数阻塞,直到线程th1运行结束,主函数再继续运行。
整个过程就相当于:你在处理某件事情(你是主线程),中途你让老王帮你办一个任务(与你同时执行)(创建线程1,该线程取名老王),又叫老李帮你办一件任务(创建线程2,该线程取名老李),现在你的一部分工作做完了,剩下的工作得用到他们的处理结果,那就调用"老王.join()“与"老李.join()”,至此你就需要等待(主线程阻塞),等他们把任务做完(子线程运行结束),你就可以继续你手头的工作了(主线程不再阻塞)。
lock()与unlock():
#include
#include
#include
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
m.lock();
cout << “proc1函数正在改写a” << endl;
cout << “原始a为” << a << endl;
cout << “现在a为” << a + 2 << endl;
m.unlock();
}
void proc2(int a)
{
m.lock();
cout << “proc2函数正在改写a” << endl;
cout << “原始a为” << a << endl;
cout << “现在a为” << a + 1 << endl;
m.unlock();
}
int main()
{
int a = 0;
thread t1(proc1, a);
thread t2(proc2, a);
t1.join();
t2.join();
return 0;
}
需要在进入临界区之前对互斥量加锁lock,退出临界区时对互斥量解锁unlock;当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。
程序实例化mutex对象m,本线程调用成员函数m.lock()会发生下面 2 种情况: (1)如果该互斥量当前未上锁,则本线程将该互斥量锁住,直到调用unlock()之前,本线程一直拥有该锁。 (2)如果该互斥量当前被其他线程锁住,则本线程被阻塞,直至该互斥量被其他线程解锁,此时本线程将该互斥量锁住,直到调用unlock()之前,本线程一直拥有该锁。
不推荐实直接去调用成员函数lock(),因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock则能避免忘记解锁带来的问题。
#include
#include
#include
using namespace std;
std::mutex mtx;
int cnt = 0;
void foo()
{
// 创建lock_guard对象,注意lock_guard的生命周期
//lock_guard lck(mtx);
//std::lock_guardstd::mutex lck(mtx);
cnt++;
}
void foo2()
{
// 创建lock_guard对象,注意lock_guard的生命周期
//lock_guard lck(mtx);
//std::lock_guardstd::mutex lck(mtx);
cnt–;
}
int main ( )
{
thread t(foo);
thread t1(foo2);
t.join();
std::cout << "cnt: " << cnt << std::endl;
//t1.join();
std::cout << "cnt: " << cnt << std::endl;
return 0;
}
https://www.zhihu.com/tardis/zm/art/194198073?source_id=1005