1.10从二叉树开始,与前序刷过的题一起形成两条并行路径,每天N道新题,回顾N道旧题
二叉树基本概念+二叉树深度优先遍历(前中后序遍历)递归算法
节点:根节点,分支节点,叶子节点;子节点,父节点
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
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奇数圈会多一个中心框
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
前段时间一直在整理操作系统的相关八股文,整理得差不多了,继续回归刷题
1.平衡二叉树
该题注意一点就是平衡二叉树的定义:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1(求高度用后序遍历最合适)
回顾:
1.反转字符串II
该题只需标记每个反转串的起始位置,同时判断结束位置是否超出字符串长度即可
2.右旋转字符串
该题有两种无须额外空间的思路
第一种是双指针遍历一次,每次遍历都交换一次
第二种是非双指针的遍历两次,第一次整体交换第二次分左右局部交换
3.反转字符串中的单词
先整体反转,再局部反转,主要难点还是去掉多余的空格,将空格绑定到子串的前面进行处理最合适
1.二叉树的所有路径
该题有两种方式,一种是值传递,另一种是地址传递(引用传递)
引用传递方式将首次接触回溯
1.左叶子之和
其实做到这里大部分题都是分左子树和右子树计算然后当前节点进行整合后向上传递,该题也不例外:返回值为:当前节点左子树左叶子节点之和+当前节点右子树左叶子节点之和
判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子
2.寻找二叉树左下角的值
该题用层序遍历直接秒,但递归也得会,递归方法由于函数传参有不只一个参数,就自由发挥了,可以值传递,可以地址传递,地址传递可以用到回溯
可以从左节点开始遍历,当深度更新时更新左值
3.路径总和/路径总和II
该题与『二叉树的所有路径』如出一辙,相对而言该题更简
单该题在参数设计上有讲究,可以正向向加也可以反向相减
如果采用减法,可以直接传入目标值,每过一个节点减1
如果采用加法,则需要多传入一个参数
地址传递需要回溯
4.中后序序列构建二叉树/前中序序列构建二叉树
该题的关键是:先确定中节点,然后向下划分子树
必须要有中序,否则无法对左右子树进行分割
前序:中左右
中序:左中右 ->从前序或后序得到中间节点值,然后在中序中找到该值进行左右分割
后序:左右中
该题的前提是元素不重复
5.最大二叉树
理解了构造二叉树,这道题直接秒了
凡是构造二叉树的题,一定是前序遍历
6.合并二叉树
秒了
难点是:如何同时遍历两个二叉树
1.二叉搜索树中的搜索
什么是二叉搜索树(BST):
二叉搜索树是一个有序树:
该题用递归法比较简单
用迭代法则不是以前使用栈模拟的那种写法,因为基于二叉搜索树的特性,迭代的写法也十分简洁
二叉搜索树自带顺序(左中右),不用再区分前中后序,也不需要遍历整个二叉树
2.验证二叉搜索树
这道题直接给卡了好久,用左右中整了半天已经到面向测试的修改了,虽然最后过了但思路没对
最好的思路还是遵循左中右的次序,类似于双指针的比较前后节点
为什么要中序遍历:因为中序遍历得到的元素顺序才满足由小到大,中为处理逻辑
也可以用中序遍历将二叉树转为数组,然后再验证是否为递增数组
要充分理解二叉树中的双指针法
3.二叉搜索树的最小绝对差
该题的思路同样是要遵循左中右的遍历顺序
对上一题有清晰理解后做这道题就没那么困难了
二叉树双指针法
4.二叉搜索树中的众数
该题是二叉树第一次没自己写出来,直接看了视频,看完视频后发现思路是一样的
问题在于:对于中间节点的处理我把count计数和元素处理写在了一陀,导致ifelse很臃肿,而且对于count计数的起始值和到底是处理pre还是cur不清晰
该题的解法还是有点难理解的
再强调一边,二叉搜索树一定是中序遍历
5.二叉树的最近公共祖先
做题前要明确思路的可行性,再敲代码
该题稍稍多花了点时间,对于左中右等遍历顺序需要进一步理解
6.二叉搜索树的最近公共祖先
利用BST的特性,寻找方向便可确定,比普通二叉树寻找祖先要方便很多
1.二叉搜索树中的插入操作
该题用二叉搜索树双指针法解决了
但有更简单的思路:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了
2.删除二叉搜索树中的节点
结合了上一题添加节点的方法
注意:删除节点需要释放内存,避免内存泄露
3.修剪二叉搜索树
该题第一次做是用了从低往上搜索,后来发现这种方式多了很多不必要的递归
最正确的方式是从上往下先对每个节点判断是否合规,再选择往左还是往右走
但是有个问题是从底层往上遍历每个节点的方法才能进行无效节点的有效内存释放,否则从上往下的做法除非增加内存释放的写法,否则就没法释放无效节点的内存了
还有个问题是使用从下往上处理释放内存时会发生无效访问报错******暂未解决
******剪枝******剪的是节点的左右子树
4.将有序数组转换为二叉搜索树
秒了
注意可以传入数组的左右区间,免去copy的过程
5.把二叉搜索树转换为累加树
秒了
1.回溯基本知识
回溯(回溯搜索法):纯暴力搜索算法,效率不高,回溯函数也就是递归函数,指的都是一个函数
回溯的本质是穷举,穷举所有可能
所有回溯法的问题都可以抽象为一个n叉树,回溯三部曲:递归嵌套for循环
void backtracking(...参数...) //函数返回值一般为void,需要什么参数,就填什么参数
{
if( 终止条件 ) { //一般来说搜到叶子节点
收集结果;
return;
}
for( 元素集合,通常为当前节点所有子节点 ) {
处理节点;
递归;
回溯;
}
return;
}
通常用于:排列问题,组合问题,切割问题,子集问题,棋盘问题
2.组合+剪枝
首次实现回溯算法,看了代码随想录的思路后自个儿写通过
可以剪枝的地方就在递归中每一层的for循环所选择的起始位置
如果for循环选择的起始位置之后的元素个数已不足需要的元素个数,就没有必要搜索了
3. 组合总和III
理解了上一道题,这道题秒了
组合:不强调顺序
注意:可以使用求和的目标值与各值相减,最后判断是否为0,这样就节省了一个int空间
同时还要注意这道题剪枝操作:除了上一题的剪枝外还可加入总和超过目标值以及总和达不到目标值的情况
4.电话号码的字母组合
秒了
似乎做到这里可以理解为回溯就是解决多层for循环嵌套的问题
主要是明确深度和宽度都由题目中哪些元素控制
这道题视频中说明了隐藏回溯过程的写法(其实就是进行值传递)
5.组合总和
秒了
6.组合总和II
乱序的处理首先需要进行排序(十大排序算法)
这里要理解去重的两个方向:数层去重和树枝去重
树层去重是解决当前遍历的元素集合中,取重复元素时可能会造成后序层取到相同结果(结果相同)
树枝去重是解决结果中不能出现重复元素(结果中元素相同)
1.分割回文串
花了点时间但最后解出,主要还是明确深度和宽度的控制要素
2.复原IP地址
虽然踉踉跄跄做出来了,但花了很多时间,这题情况相对之前的都复杂一些
这道题思路还需要进行优化,细节处理上也没有想太清楚就直接动笔,导致花费了太多时间
终止条件可以用‘.’逗点数量来判断,不需要单独计算层数
对于字符串转数字判断是否超出的写法还需要改进,当前写法不兼容任意长度
采用insert和erase直接对原字符串进行操作,无需重新创建path,这样就不用对元素加入了,而只需要操作逗点
3.子集
秒了
该题的结果不在叶子节点,每一层的每个节点都是结果
4.子集II
秒了
与 组合总和II 异曲同工,都是树层去重
子集在每个节点都要取结果,除非对结果有其他要求
5.递增子序列
卡了!!崩了!!难受了!!
其实是没理解透,当前层的索引是从startindex开始的,而不是从0开始的!!!
子集在每个节点都要取结果,除非对结果有其他要求
1.全排列
相较于前面的题目,这类题目多了一个标记使用过的步骤
2.全排列 II
排列问题有两点:
第一:树层去重用集合,每次在集合中找是否有 重复元素,当然也可以先进行排序,将相同的元素放在一起,然后比较前后元素是否相同,如果相同且前一位used是0,则continue
第二:树枝去重用vector
3.重新安排行程
脑袋都要炸了,还没写出来,我是渣渣
4.N皇后
虽然磨出来了,而且思路也很清晰,但细节上还是有点问题,尤其是使用二维used应当使用int而不是bool
5.解数独
虽然又磨出来了,但还不是最完美的
把我当什么人了连续3道困难题,折磨死我了~.~
1.贪心基本概念
贪心没有做题套路!!!
贪心的本质是选择每一阶段的局部最优,从而达到全局最优
唯一的难点就是如何通过局部最优,推出整体最优
靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划
有同学问了如何验证可不可以用贪心算法呢?
最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧
2.分发饼干
虽然秒了,但却时每感觉到这是算法哈哈哈~ 就是排序+比较+计数
3.摆动序列
秒,没啥感觉!!!
4.最大子序和
G了思路不对
如果连续和为负,则从下一个开始重新计算,这是关键
1.冒泡算法:时间复杂度: 稳定O(N^2) 空间复杂度:O(1)
演示参考:冒泡排序思路演示
void BubbleSort(vector &nums)
{
int size = nums.size();
for(int i=0; i nums[j+1])
{
//最快的两数交换算法
nums[j] ^= nums[j+1]; // A ^ B
nums[j+1] ^= nums[j]; // (A ^ B) ^ B == A ^ (B ^ B) == A ^ 0 == A
nums[j] ^= nums[j+1]; // (A ^ B) ^ A == B ^ (A ^ A) == B ^ 0 == B
}
}
}
2.归并排序:时间复杂度:稳定O(NlogN) 空间复杂度:O(N) 采用分治法
演示参考:归并排序【图解+代码】
void merge(vector &nums, vector &aux, int low, int mid, int high)
{
int k=low;
int i=low,j=mid+1;
while(i<=mid && j<=high)
{
if(nums[i] < nums[j])
aux[k++] = nums[i++];
else
aux[k++] = nums[j++];
}
while(i<=mid) aux[k++] = nums[i++];
while(j<=high) aux[k++] = nums[j++];
for(int i=low; i<=high; i++)
nums[i] = aux[i];
}
void MergeSort(vector &nums, vector &aux, int low, int high)
{
//左右拆分到叶子节点返回
if(low >= high) return;
//左右拆分
int mid = low + (high-low)/2;
MergeSort(nums, aux, low, mid);
MergeSort(nums, aux, mid+1, high);
if(nums[mid] <= nums[mid+1]) return; //优化:分治后已满足大小顺序则不用再进行合并
//整理当前节点的左右子节点的顺序
merge(nums, aux, low, mid, high);
}
3.快速排序:时间复杂度:不稳定O(NlogN) ~O(N^2) 空间复杂度:O(logN) 采用分治法
演示参考:快排思想和代码
void QuickSort(vector &nums, int low, int high)
{
if(low >= high) return;
int l=low,h=high;
int nums_ref = nums[low]; //取出参考值
while(low < high)
{
while(low < high && nums[high] >= nums_ref)
--high;
nums[low] = nums[high];
while(low < high && nums[low] <= nums_ref)
++low;
nums[high] = nums[low];
}
nums[low] = nums_ref; //放回参考值
//分治
FastSort(nums, l, low-1);
FastSort(nums, low+1, h);
}
4.堆排序
参考:堆排序
【从堆的定义到优先队列、堆排序】
//堆排序
//维护顶堆
#define MIN_MAX 1 //0为小顶堆,从大到小排序 1为大顶堆从小到大排序
#if MIN_MAX == 0
#define COM >
#else
#define COM <
#endif
void heapify(vector &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 &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);
}
}
1.基本知识
当前的值与前一个或前几个相关,可以推出递推公式,则使用动态规划
动规的五部曲:
2.斐波那契数
秒了!
3.爬楼梯
该题展现出动态规划的特征了需要自己推递推公式 ,花了点时间,不过还是做出来了
斐波那契数列
4.使用最小花费爬楼梯
虽然没有做出来,但进一步理解了动态规划 ,注意要看清题意,确定如何初始化
斐波那契数列
5. 不同路径
秒了! 动态规划从一维转到二维,注意:一定要先明确dp数组的含义
6.不同路径 II
秒了! 虽然加了障碍,但逻辑是一样的
7.整数拆分
麻了!想不到
8.不同的二叉搜索树
麻了!梦回高中做数学归纳题