1.10从二叉树开始,与前序刷过的题一起形成两条并行路径,每天N道新题,回顾N道旧题
DAY1(2024.1.10):
二叉树基本概念+二叉树深度优先遍历(前中后序遍历)递归算法
节点:根节点,分支节点,叶子节点;子节点,父节点
N叉树:最大节点数 <= N
度:节点的孩子节点数
满二叉树
完全二叉树
平衡二叉树
二叉树存储结构:顺序结构(数组),链式结构(链表)
二叉树遍历:深度优先搜索DFS,广度优先搜索BFS
深度优先搜索:前序遍历(中左右),中序遍历(中左右),后序遍历(中左右)
深度:节点从上往下所处的层数
高度:节点从下往上所处的层数
根节点的高度就是二叉树的最大深度
二叉树节点为i的父节点其左右子节点索引分别为2i+1 2i+2
二叉树节点为i的子节点,其父节点为(i-1)/2(整除,去掉余数)
红黑树是一种特殊的平衡二叉树。map,set,multimap,multiset都是用的红黑树实现
unordered_map,和unordered_set都是由哈希表实现
优先队列的底层实现是vector,但其存储的是树型结构,利用大顶堆小顶堆方法实现
c++提供了三个容器适配器:stack,queue,priority_queue
DAY2(2024.1.11):
1. 二叉树深度遍历(前中后序遍历)迭代统一算法:
----利用栈结构,因为递归就是通过函数调用栈实现的
----将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记(标记法)
(要处理的节点放入栈之后,紧接着放入一个空指针作为标记)
迭代和递归相比在时间复杂度上差不多,但空间复杂度上递归消耗更大
(实际项目开发的过程中我们是要尽量避免递归)
2. 二叉树广度优先遍历(层序遍历)算法:
----利用队列结构
----重点关注二叉树最大深度,和二叉树最小深度的递归写法和层序遍历写法
3. 翻转二叉树:----注意中序遍历递归法中,在处理中间节点时,左右子树是进行了反转的,后续的处理应当还是左子树
4. 二分法(开闭区间判定)+有序数组的平方(开闭区间判定+双指针法):
主要关注区间的统一:以下三个位置需要进行统一
right = 0; left = size(右开) or size-1(右闭)
while(< or <=) <(右开) <=(右闭)
right = middle or middle-1; middle(右开) middle-1(右闭)
注意预防越界:middle = left + (right-left)/2
5. 移除元素(双指针法)
6. 长度最小子数组(双指针法):
需要注意结束条件,并非一轮遍历完就结束了,可能存在遍历完但和大于target的情况
7.螺旋矩阵II
注意3点:1保证每条边的遍历个数一致 2计算圈数 3奇数圈会多一个中心框
DAY3(2024.1.12)
1.对称二叉树
这道题体现了写递归的第一个步骤:确定函数的参数,这道题因为要比较两个节点的值是否相等,因此参数应该是两个,左节点和右节点
注意要比较内侧和外侧节点
2.二叉树的最大最小深度:
注意最小深度:如果左子树为空,右子树不为空,说明最小深度是 1 + 右子树的深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。,注意是叶子节点。
什么是叶子节点,左右孩子都为空的节点才是叶子节点
求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑
3.完全二叉树节点个数:
可以用前面的所有遍历算法求解,也可以用完全二叉树特性来解:
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置
这样针对每个节点,可以求以他为根节点的子树是否为满二叉树,如果是则可以直接通过[2<<子树层数] -1 向其父节点返回该子数节点数量。注:2的n次方为2<<(n-1)
判断是否为满二叉树,仅需判断向左遍历的层数与向右遍历的层数是否一致便可判断
回顾:
1.移除链表元素:
存在第一个就是要移除的元素,因此需要虚拟头节点
因为要移除,所以肯定需要记录当前节点的前一个,因此需要移动pre和cur
2.设计链表:
链表主要注意2点:
链表的增删导致节点的变化,因此需要记录pre
链表可能为空,也可能改变,因此需要虚拟头节点
本题注意在删除节点的时候要删除对应的指针,以免内存泄露
3.反转链表:
不要定义next,不然很容易造成非法访问,用cur->next代替,这样指用判断cur是否有效就行
递归写法
4.删除链表的倒数第N个节点:
倒数第n个数的定位可以用双指针正向一次性定位:先让快指针走n步,再快慢一起走直到快指针完成遍历
5.链表相交
求重合的首个节点可以通过求长度差值
6.环形链表检测
快指针走第一轮检测是否有环,快指针走第二轮确定环的入口
(x+y)*2 = n(y+z) + y 即 x = (n-1)(y+z) + z
DAY4(2024.1.21)
前段时间一直在整理操作系统的相关八股文,整理得差不多了,继续回归刷题
1.平衡二叉树
该题注意一点就是平衡二叉树的定义:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1(求高度用后序遍历最合适)
回顾:
1.反转字符串II
该题只需标记每个反转串的起始位置,同时判断结束位置是否超出字符串长度即可
2.右旋转字符串
该题有两种无须额外空间的思路
第一种是双指针遍历一次,每次遍历都交换一次
第二种是非双指针的遍历两次,第一次整体交换第二次分左右局部交换
3.反转字符串中的单词
先整体反转,再局部反转,主要难点还是去掉多余的空格,将空格绑定到子串的前面进行处理最合适
DAY5(2024.1.22)
1.二叉树的所有路径
该题有两种方式,一种是值传递,另一种是地址传递(引用传递)
引用传递方式将首次接触回溯
DAY6(2024.1.23)
1.左叶子之和
其实做到这里大部分题都是分左子树和右子树计算然后当前节点进行整合后向上传递,该题也不例外:返回值为:当前节点左子树左叶子节点之和+当前节点右子树左叶子节点之和
判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子
该题用层序遍历直接秒,但递归也得会,递归方法由于函数传参有不只一个参数,就自由发挥了,可以值传递,可以地址传递,地址传递可以用到回溯
可以从左节点开始遍历,当深度更新时更新左值
该题与『二叉树的所有路径』如出一辙,相对而言该题更简
单该题在参数设计上有讲究,可以正向向加也可以反向相减
如果采用减法,可以直接传入目标值,每过一个节点减1
如果采用加法,则需要多传入一个参数
地址传递需要回溯
该题的关键是:先确定中节点,然后向下划分子树
必须要有中序,否则无法对左右子树进行分割
前序:中左右
中序:左中右 ->从前序或后序得到中间节点值,然后在中序中找到该值进行左右分割
后序:左右中
该题的前提是元素不重复
5.最大二叉树
理解了构造二叉树,这道题直接秒了
凡是构造二叉树的题,一定是前序遍历
6.合并二叉树
秒了
难点是:如何同时遍历两个二叉树
DAY7(2024.1.24)
什么是二叉搜索树(BST):
二叉搜索树是一个有序树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉搜索树
该题用递归法比较简单
用迭代法则不是以前使用栈模拟的那种写法,因为基于二叉搜索树的特性,迭代的写法也十分简洁
二叉搜索树自带顺序(左中右),不用再区分前中后序,也不需要遍历整个二叉树
2.验证二叉搜索树
这道题直接给卡了好久,用左右中整了半天已经到面向测试的修改了,虽然最后过了但思路没对
最好的思路还是遵循左中右的次序,类似于双指针的比较前后节点
为什么要中序遍历:因为中序遍历得到的元素顺序才满足由小到大,中为处理逻辑
也可以用中序遍历将二叉树转为数组,然后再验证是否为递增数组
要充分理解二叉树中的双指针法
该题的思路同样是要遵循左中右的遍历顺序
对上一题有清晰理解后做这道题就没那么困难了
二叉树双指针法
该题是二叉树第一次没自己写出来,直接看了视频,看完视频后发现思路是一样的
问题在于:对于中间节点的处理我把count计数和元素处理写在了一陀,导致ifelse很臃肿,而且对于count计数的起始值和到底是处理pre还是cur不清晰
该题的解法还是有点难理解的
再强调一边,二叉搜索树一定是中序遍历
做题前要明确思路的可行性,再敲代码
该题稍稍多花了点时间,对于左中右等遍历顺序需要进一步理解
利用BST的特性,寻找方向便可确定,比普通二叉树寻找祖先要方便很多
DAY8(2024.1.25)
该题用二叉搜索树双指针法解决了
但有更简单的思路:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了
结合了上一题添加节点的方法
注意:删除节点需要释放内存,避免内存泄露
3.修剪二叉搜索树
该题第一次做是用了从低往上搜索,后来发现这种方式多了很多不必要的递归
最正确的方式是从上往下先对每个节点判断是否合规,再选择往左还是往右走
但是有个问题是从底层往上遍历每个节点的方法才能进行无效节点的有效内存释放,否则从上往下的做法除非增加内存释放的写法,否则就没法释放无效节点的内存了
还有个问题是使用从下往上处理释放内存时会发生无效访问报错******暂未解决
******剪枝******剪的是节点的左右子树
秒了
注意可以传入数组的左右区间,免去copy的过程
秒了
DAY9(2024.1.26) -- 回溯:组合
1.回溯基本知识
回溯(回溯搜索法):纯暴力搜索算法,效率不高,回溯函数也就是递归函数,指的都是一个函数
回溯的本质是穷举,穷举所有可能
所有回溯法的问题都可以抽象为一个n叉树,回溯三部曲:递归嵌套for循环
void backtracking(...参数...) //函数返回值一般为void,需要什么参数,就填什么参数
{
if( 终止条件 ) { //一般来说搜到叶子节点
收集结果;
return;
}
for( 元素集合,通常为当前节点所有子节点 ) {
处理节点;
递归;
回溯;
}
return;
}
通常用于:排列问题,组合问题,切割问题,子集问题,棋盘问题
2.组合+剪枝
首次实现回溯算法,看了代码随想录的思路后自个儿写通过
可以剪枝的地方就在递归中每一层的for循环所选择的起始位置
如果for循环选择的起始位置之后的元素个数已不足需要的元素个数,就没有必要搜索了
3. 组合总和III
理解了上一道题,这道题秒了
组合:不强调顺序
注意:可以使用求和的目标值与各值相减,最后判断是否为0,这样就节省了一个int空间
同时还要注意这道题剪枝操作:除了上一题的剪枝外还可加入总和超过目标值以及总和达不到目标值的情况
秒了
似乎做到这里可以理解为回溯就是解决多层for循环嵌套的问题
主要是明确深度和宽度都由题目中哪些元素控制
这道题视频中说明了隐藏回溯过程的写法(其实就是进行值传递)
5.组合总和
秒了
6.组合总和II
乱序的处理首先需要进行排序(十大排序算法)
这里要理解去重的两个方向:数层去重和树枝去重
树层去重是解决当前遍历的元素集合中,取重复元素时可能会造成后序层取到相同结果(结果相同)
树枝去重是解决结果中不能出现重复元素(结果中元素相同)
DAY10(2024.1.27) -- 回溯:分割+子集
1.分割回文串
花了点时间但最后解出,主要还是明确深度和宽度的控制要素
2.复原IP地址
虽然踉踉跄跄做出来了,但花了很多时间,这题情况相对之前的都复杂一些
这道题思路还需要进行优化,细节处理上也没有想太清楚就直接动笔,导致花费了太多时间
终止条件可以用‘.’逗点数量来判断,不需要单独计算层数
对于字符串转数字判断是否超出的写法还需要改进,当前写法不兼容任意长度
采用insert和erase直接对原字符串进行操作,无需重新创建path,这样就不用对元素加入了,而只需要操作逗点
3.子集
秒了
该题的结果不在叶子节点,每一层的每个节点都是结果
4.子集II
秒了
与 组合总和II 异曲同工,都是树层去重
子集在每个节点都要取结果,除非对结果有其他要求
5.递增子序列
卡了!!崩了!!难受了!!
其实是没理解透,当前层的索引是从startindex开始的,而不是从0开始的!!!
子集在每个节点都要取结果,除非对结果有其他要求
DAY11(2024.1.28) -- 回溯:排列+棋盘
1.全排列
相较于前面的题目,这类题目多了一个标记使用过的步骤
2.全排列 II
排列问题有两点:
第一:树层去重用集合,每次在集合中找是否有 重复元素,当然也可以先进行排序,将相同的元素放在一起,然后比较前后元素是否相同,如果相同且前一位used是0,则continue
第二:树枝去重用vector<bool>,标记元素是否被使用
3.重新安排行程
脑袋都要炸了,还没写出来,我是渣渣
4.N皇后
虽然磨出来了,而且思路也很清晰,但细节上还是有点问题,尤其是使用二维used应当使用int而不是bool
5.解数独
虽然又磨出来了,但还不是最完美的
把我当什么人了连续3道困难题,折磨死我了~.~
DAY12(2024.1.29) -- 贪心
1.贪心基本概念
贪心没有做题套路!!!
贪心的本质是选择每一阶段的局部最优,从而达到全局最优
唯一的难点就是如何通过局部最优,推出整体最优
靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划
有同学问了如何验证可不可以用贪心算法呢?
最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧
2.分发饼干
虽然秒了,但却时每感觉到这是算法哈哈哈~ 就是排序+比较+计数
3.摆动序列
秒,没啥感觉!!!
4.最大子序和
G了思路不对
如果连续和为负,则从下一个开始重新计算,这是关键
4.加油站
思路想到了但实现起来坎坎坷坷,神了
5.分发糖果
想不到啊想不到!!!~~~
有两边的情况分边处理,先一次遍历处理左边,然后一次遍历处理右边
6.根据身高重建队列
list<>的实现是双向链表,由于链表无法实现随机访问,因此迭代器不能直接+-n,必须+1/-1移动
频繁插入删除应用list
7.单调递增的数字
处理方向的选择
8.监控二叉树
麻了!
DAY13(2024.1.30) -- 四个排序算法
------时间复杂度:
时间复杂度要考虑到数据规模,如果数据规模很小甚至可以用O(n^2)的算法比O(n)的更合适(在有常数项的时候)
时间复杂度中的O代表的是一般情况下的时间复杂度,而非最坏情况下的
大O是数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度,这个数据量也就是常数项系数已经不起决定性作用的数据量
我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂的的一个排行如下所示
但是也要注意大常数,如果这个常数非常大,例如10^7 ,10^9 ,那么常数就是不得不考虑的因素了
平时说这个算法的时间复杂度是logn的,可以是以10为底n的对数,也可以是以20为底n的对数,但我们统一说 logn,也就是忽略底数的描述
对递归复杂度的理解:通过一道面试题目,讲一讲递归算法的时间复杂度! | 代码随想录
题目:Pow(x, n)
递归的时间复杂度等于递归的次数 * 每次递归中的操作次数
- 究竟什么是大O?大O表示什么意思?严格按照大O的定义来说,快排应该是O(n^2)的算法!
- O(n^2)的算法为什么有时候比O(n)的算法更优?
- 时间复杂度为什么可以忽略常数项?
- 如何简化复杂的时间复杂度表达式,原理是什么?
- O(log n)中的log究竟是以谁为底?
---空间复杂度
空间复杂度是考虑程序运行时占用内存的大小,而不是可执行文件的大小
编译器的内存对齐,编程语言容器的底层实现等等这些都会影响到程序内存的开销
空间复杂度是预先大体评估程序内存使用的大小
当消耗空间和输入参数n保持线性增长,这样的空间复杂度为O(n)
随着n的变化,所需开辟的内存空间并不会随着n的变化而变化,示为大O(1)
递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度
因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的)
大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是O(nlogn)
代码区和数据区所占空间都是固定的,而且占用的空间非常小,那么看运行时消耗的内存主要看可变部分;在可变部分中,栈区间的数据在代码块执行结束之后,系统会自动回收,而堆区间数据是需要程序员自己回收,所以也就是造成内存泄漏的发源地
不要以为只有C/C++才会有内存对齐,只要可以跨平台的编程语言都需要做内存对齐,Java、Python都是一样的。
而且这是面试中面试官非常喜欢问到的问题,就是:为什么会有内存对齐?
CPU读取内存不是一次读取单个字节,而是一块一块的来读取内存,块的大小可以是2,4,8,16个字节,具体取多少个字节取决于硬件
主要是两个原因
-
平台原因:不是所有的硬件平台都能访问任意内存地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。为了同一个程序可以在多平台运行,需要内存对齐。
-
硬件原因:经过内存对齐后,CPU访问内存的速度大大提升。
1.冒泡算法:时间复杂度: 稳定O(N^2) 空间复杂度:O(1)
演示参考:冒泡排序思路演示
void MyBubbleSort(vector<int> &nums)
{
for(int i=0; i<nums.size()-1; i++)
{
int changed = 0;
for(int j=1; j<nums.size()-i; j++)
{
if(nums[j-1] > nums[j])
{
changed = 1;
nums[j-1] ^= nums[j]; // A ^ B
nums[j] ^= nums[j-1]; // (A ^ B) ^ B == A ^ (B ^ B) == A ^ 0 == A
nums[j-1] ^= nums[j]; // (A ^ B) ^ A == B ^ (A ^ A) == B ^ 0 == B
}
}
if(changed == 0) break; //提前结束
}
}
2.归并排序:时间复杂度:稳定O(NlogN) 空间复杂度:O(N) 采用分治法
时间复杂度的解释:每次进行对半拆分,总共要进行n/(2^x) = 1 x = logN次拆分
每次拆分后处理的值为N个,故时间复杂度为NlogN
空间复杂度在使用非递归法时是N,在使用递归法时是N+logN(递归的栈空间)
演示参考:归并排序【图解+代码】
void MyMergeSort(vector<int> &nums, vector<int> &aux, int start, int end)
{
if(start >= end) return;
//分
int mid = start + (end-start)/2;
MyMergeSort(nums, aux, start, mid);
MyMergeSort(nums, aux, mid+1, end);
//治
if(nums[mid] <= nums[mid+1]) return;
else
{
int i=start;
int l=start, r=mid+1;
while(l <= mid && r <= end)
{
if(nums[l] < nums[r]) aux[i++] = nums[l++];
else aux[i++] = nums[r++];
}
while(l <= mid) aux[i++] = nums[l++];
while(r <= mid) aux[i++] = nums[r++];
for(int i=start; i<=end; i++)
nums[i] = aux[i];
}
}
3.快速排序:时间复杂度:不稳定O(NlogN) ~O(N^2) 空间复杂度:O(logN) 采用分治法
时间复杂度的解释:当每次选取的值恰好能排在中间位置时,时间复杂度为NlogN
当每次选取的值恰好排在最左或最右时,时间复杂度为N^2
空间复杂度是递归造成的栈空间的使用logN
基于不稳定的时间复杂度,因此每次选取参考值时都需要随即选取,以达到最佳效果
演示参考:快排思想和代码
void FastSort(vector<int> &nums, int low, int high)
{
if(low >= high) return;
//治(随机选择能够最大程度上的减少最坏情况发生)
int ref = nums[low + (high-low)/2]; //随机取参考值(每次取中间值比较合理,或者随机取值)
swap(nums[low], nums[low + (high-low)/2]);
while(low < high)
{
while(low < high && nums[high] >= ref)
--high;
nums[low] = nums[high];
while(low < high && nums[low] <= ref)
++low;
nums[high] = nums[low];
}
nums[low] = ref; //放回参考值
//分
FastSort(nums, l, low-1);
FastSort(nums, low+1, h);
}
快排和归并都是分治思想,区别在于快排是先治再分 ,归并是先分再治
4.堆排序:时间复杂度:不稳定O(NlogN) 空间复杂度:O(1)
建堆过程的时间复杂度其实为N:堆排序中建堆过程时间复杂度O(n)怎么来的? - 知乎
排序的过程也需要下滤,故也为NlogN,但建堆和排序是分开的,故总的时间复杂度为NlogN
参考:堆排序
//堆排序
//维护顶堆
#define MIN_MAX 1 //0为小顶堆,从大到小排序 1为大顶堆从小到大排序
#if MIN_MAX == 0
#define COM >
#else
#define COM <
#endif
void heapify(vector<int> &nums, int size, int i)
{
int father = i; //子节点i的父节点为(i-1)/2 i为索引
int l = 2*i + 1; //父节点的左子节点为2*i+1
int r = 2*i + 2; //父节点的右子节点为2*i+2
//注意索引的有效性
if(l<size && nums[father] COM nums[l]) father = l;
if(r<size && nums[father] COM nums[r]) father = r;
if(father != i)
{
nums[father] ^= nums[i];
nums[i] ^= nums[father];
nums[father] ^= nums[i];
heapify(nums, size, father);
}
}
void HeapSort(vector<int> &nums)
{
int n = nums.size();
//建大顶堆(将原本的数组表示为二叉树,从最后一个父节点开始维护成大顶堆)
for(int i=(n-1-1)/2; i>=0; i--) //子节点i的父节点为 (i-1)/2
heapify(nums, n, i);
//排序
for(int i=n-1; i>0; i--) //最后一个元素不需要再执行
{
//交换(注意这里i不能为0,因为自己和自己进行如下交换结果将为0)
nums[0] ^= nums[i];
nums[i] ^= nums[0];
nums[0] ^= nums[i];
//下沉维护堆结构
heapify(nums, i, 0); //注意,这里元素总数发生了变化,因为排序时每完成一个元素的下沉后则将该元素固定,所以可更改的元素个数发生了变化
}
}
等级:堆排序(空间复杂度为O(1)) > 归并(时间复杂度为O(NlogN)) 约等于 快排 > 冒泡
补充:2024.02.24
排序又分为全局排序和局部排序(如TopK问题:找出最大/最小的k个数)
1.冒泡排序:每次冒泡都将最大值上浮,直接进行k次冒泡便能找到前k个数,时间复杂度为N*K
2.堆排序:优先对列维护k个元素的堆,时间复杂度为NlogK
3.快速选择算法:(快排思想的简化):快排每次随机选择一个元素,并将其他元素左右划分为比他大的部分和比它小的部分,那么我们只需要找到第k大的元素,则其左边/右边(取决于划分策略)的所有元素以及其本身便是前k大的元素,平均时间复杂度为O(N)(极不稳定O(1)~O(N^2))
平均时间复杂度按每次参考元素排在最中间,则每次处理次数为N + N/2 + N/4 + ...+1 ,数列和小于2N因此平均为O(N)
Top K 问题的最优解 - 快速选择算法(Quickselect) - 知乎
以上三种最佳为第三种:快速选择算法
DAY14(2024.1.31) -- 动态规划
1.基本知识
当前的值与前一个或前几个相关,可以推出递推公式,则使用动态规划
动规的五部曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
2.斐波那契数
秒了!
3.爬楼梯
该题展现出动态规划的特征了需要自己推递推公式 ,花了点时间,不过还是做出来了
斐波那契数列
虽然没有做出来,但进一步理解了动态规划 ,注意要看清题意,确定如何初始化
斐波那契数列
5. 不同路径
秒了! 动态规划从一维转到二维,注意:一定要先明确dp数组的含义
6.不同路径 II
秒了! 虽然加了障碍,但逻辑是一样的
7.整数拆分
麻了!想不到
8.不同的二叉搜索树
麻了!梦回高中做数学归纳题
DAY15(2024.2.1) -- 二叉树复习
在二叉树的层序遍历 II中有个技巧,返回倒序的vector时可以直接用rbegin和rend迭代器
二叉树的右视图该题用迭代重做一下
N 叉树的层序遍历这题可以参考一下N叉树类的写法,利用一个指针向量装载孩子节点
满二叉树第n层的元素个数为2的(n-1)次方,即1<<(n-1)
总共n层的满二叉树其节点总数为2的n次方减1,即(1<<n) - 1
二叉树的最小深度:该题用迭代和递归都重做一下
翻转二叉树该题建议重写递归和迭代的中序遍历,看一下两者的区别
完全二叉树的节点个数用完全二叉树的性质再解一下
平衡二叉树的概念:二叉树的每个节点的左右子树的高度差的绝对值不超过1
二叉树的所有路径在这题还需要在巩固一下,理解终止条件为cur->left/right时的写法,以及收集元素的写法
左叶子之和这道题不用标志再做一下,也不要写其他的函数定义其他的存储结果的变量
找树左下角的值这题可以用递归再做一次
路径总和z这道题再用递归做一便,该题的教训是如果与叶子接点有关的,则不能单纯用cur == nullptr做为终止条件,而是要考虑到左右孩子均为null才能结束,如果考虑左右孩子为空时的结束条件,那么在递归前,也需要先判定当前接点左右孩子是否有效
06. 从中序与后序遍历序列构造二叉树该题再顺一下思路
验证二叉搜索树还是没有形成惯性思维,对于二叉搜索树主打一个中序便利,这样就相当于从小到大遍历一个递增数组,该题请在次用非双指针和双指针做题,思考为什么双指针更优,然后用迭代法做一下
二叉搜索树中的众数z该题的处理逻辑有待进一步熟练,处理逻辑有待变更,频率记数应当是记录当前元素的频率,而不是上一元素的频率,频率的计算和元素的处理逻辑分开处理是最合理的
二叉树的最近公共祖先z该题再明确思路
二叉搜索树的最近公共祖先z该题再明确思路
二叉搜索树中的插入操作 明确返回值
删除二叉搜索树中的节点 明确返回值
修剪二叉搜索树z明确返回值
DAY16(2024.2.2) -- 回溯复习
组合z需要再明确减枝的含义
组合总和审题,重做,该题每个数字可以重复使用
组合总和 II该题每个数组只能用一次,但有重复数字,无非就是加一个排序,跳过重复元素,这题引出数层和树枝去重
组合总和 IIIa需要再明确减枝的含义,该题每个数字只能用一次(所给数组为增序)
一般来说:组合问题是收集叶子节点的结果
分割回文串注意子串范围即可
复原 IP 地址重做:该题重点关注有效性,可单独列为一个函数进行处理
子集求取子集问题,不需要任何剪枝!
子集 II加了一个数层去重
非递减子序列注意求子序列不能进行元素重排列的,该题重做
一般来说:子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果;子集问题一般不需要写终止条件
全排列排列问题需要借助used数组标记进行树枝去重
全排列 II值得关注的点是排列在进行排序后数层去重时,需要判断前一个重复元素是否被使用,只有未被使用时才属于同层重复去重对象,如果使用哈希去重则没有该问题
注:如果不进行排序去重,则使用hash辅助去重,否则使用排序后去重
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果
需要注意:使用set去重的版本相对于used数组的版本效率都要低很多:unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的
参考:去重总结
三道困难题:
N 皇后思路清晰,将有效性检查作为单独函数写即可
解数独只求一个结果的处理逻辑
for循环横向遍历,递归纵向遍历,回溯不断调整结果集
注意排列的树层去重
回溯的复杂度问题复杂度在最后
DAY17(2024.2.3) -- 异或骚操作
异或就是无进位加法,符号也是加法外面套个圈
1.异或进行两数交换的前提:两个数都存在相互独立的存储空间,不能自己与自己交换
2.无判断语句来比较两数大小:略
3.找到缺失的数字:当知道完整的数字集合,并给定缺失数字集合时,则可以找到缺失的数字
利用的是: m = b^c^d n = b^c 则d = m^n
4.找到唯一一个奇数个数的值:利用异或交换律和 a^a = 0 和 a^0 = a
5.找到唯二奇数个不同的数值:利用(4) 找到这两个数的异或值 a^b, 然后提取a^b仅保留右边第一个二进制1(因为a不等于b,因此异或的二进制结果中必存在1),然后再将给定集合中满足该位为1或该位为0的数进行异或,得到的结果要么是a要么是b,至此可以将a^b分开
(保留右边第一个二进制1的方法可以用:对该数取反后加一后再与原数进行与操作,即n&-n)
DAY18(2024.2.4) -- 位运算骚操作
1.判断一个数是否为2的幂
若一个数是2的幂次方,则二进制表示中只有一个1: 若n & -n == n则是2的幂次方(前提是n必须大于0)
2.其他的太骚了不便展示!!!
DAY19(2024.2.5) -- 单调栈
要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)
单调栈的本质是空间换时间
单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取
DAY20(2024.2.26) -- 重入动态规划
1.不同的二叉搜索树
找到了一些动态规划的规律,递推的方式还需要进一步练习
背包问题:
01背包的暴力解法:因为每个物品只有一个,选或者不选,因此可以用回溯来暴力解决,时间复杂度为2的n次方,n表示物品总数
二维dp解法:
先遍历背包,再遍历物品与先遍历物品,再遍历背包都可以,因为当前的最优解来自于正上方和左上方
本质上也是依据推导公式一步步推出,只是背包问题的推导公式很难想得到
一维dp解法:
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
我的理解:因为二维递推公式中对dp[i][j]的选择来自于上一层 <=j的数据,因此可以直接在上一层原数据的情况下从右往左更新j,此时使用j左边的数据都是上一层的数据,右边更新的为本层的数据
由于数据是上下拷贝,因此遍历顺序被固定为列在前,行在后(先for物品,再for背包)
注意:依据递推公式判断是否可以颠倒遍历顺序,是否可以倒序遍历
如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
2.分割等和子集
转换错了,把每个变量的重量看成1了
这道题第一反应应该是想到回溯
一个是要求的为target = sum/2,另一个是总和是否为sum/2
很慌!!!好难!!
4.目标和
越来越慌,越来越难~!!!
0-1背包关键点:
1.物品一定是选与不选两种状态,依据所给题意转化为选与不选的解体策略
2.背包能装的最大价值/最大重量/最多物品 , 背包能否装满, 背包装满有多少种方法(转换为帕楼梯的递推)
3.一定要先依据dp的含义计算递推公式
完全背包:
0-1背包因为不能重复使用物品,因此需要在上层寻找减去当前重量后的最优解
完全背包因为可以重复使用物品,因此可以在当前层寻找减去当前重量后的最优解
总结:
01背包滚动数组写法:必须倒叙,且必须先遍历物品
完全背包滚动数组写法:可以正序,如果倒叙,写法会不同,一般就采用正序写法,纯完全背包先遍历物品和先遍历背包都可
完全背包在求背包装满有多少种方法时需要先明确是求排列还是求组合,先遍历物品为组合,先遍历背包为排列