- 二叉树经典题
- 80% 二叉树的算法题基本上就是用递归
- LeetCode 105、从前序与中序遍历序列构造二叉树(重要,掌握)
- 题目解读
- 前序、中序
- 在前序中找到根节点3,再在中序里找到左孩子节点9
- 再举一个字母的例子,以中序为对应索引,然后遍历前序,找根节点和左右孩子节点
- 代码编写思路
- 通过哈希表把中序遍历序列中的值和顺序建立映射关系,通过 for 循环,遍历完中序遍历序列中的所有元素
- 然后构建二叉树
- 把前序遍历序列中的第一个元素 preorder[0] 作为二叉树的根节点,因为任意二叉树的前序遍历序列中的第一个元素,一定是二叉树的根节点
- 继续遍历前序遍历序列中的其它元素,把当前遍历的节点插入到以 root 为根节点的二叉树中
- 当 preorder 中所有元素都构造并且插入完毕之后,二叉树就完成了构建
- 那么如何插入呢?
- root : 二叉树的根节点 node : 待插入的节点
- 进行while循环,当 root 和 node 指向的节点相同时,跳出循环
- 如果 node 的中序遍历序列位置小于 root 的中序遍历序列位置 ,说明 node 应该在 root 的左子树中
- 如果 node 的中序遍历序列位置大于 root 的中序遍历序列位置 ,说明 node 应该在 root 的右子树中
- 题目解读
- LeetCode 222 、完全二叉树的节点个数(重点)
- 如果是满二叉树,总节点数是2^depth - 1
- 当左子树的高度不等于右子树的高度时,因为完全二叉树的定义,因此左子树必然大于右子树高度,并且右子树是满二叉树,可以计算右子树的节点数
- 当左子树的高度等于右子树的高度时,左子树是满二叉树,可以计算左子树的节点数
- 如果是满二叉树,总节点数是2^depth - 1
- LeetCode 236、二叉树的最近公共祖先
- 先找到p和q的路径
- p和q路径中最远的公共节点是5,也就是它们的最近公共祖先
- 怎么找呢?递归
- LeetCode 235、二叉搜索树的最近公共祖先
- 二叉搜索树:对每个根节点而言,它的左子树的节点都小于它,右子树的节点都大于它,即root.left < root < root.right
- 如果说 root 是 p、q 的最近公共祖先,也就意味着 p、q 在 root 的两侧
- 假设 p 在 root 的左侧,q 在 root 的右侧,那么 p.val < root.val < q.val
- 1、root.val - p.val > 0
- 2、root.val - q.val < 0, 即 ( root.val - p.val ) * ( root.val - q.val ) < 0
- 循环当发现 ( root.val - p.val ) * ( root.val - q.val ) > 0 ,说明 p、q 在 root 的同一侧,不是最近公共祖先, 需要搜索 root 的左右子树
- p.val < root.val,说明p在root的左子树(q也在左子树),所以去左边找,root = root.left;否则就去右边找,root = root.right
- 跳出循环,说明 (root.val - p.val) * (root.val - q.val) <= 0, 此时,root 就是 p 、q 的最近公共祖先节点
- 回溯算法
- 回溯算法的思考步骤如下:
- 1、画出递归树,找到状态变量(回溯函数的参数)
- 2、寻找结束条件,由于回溯算法是借助递归实现,所以也就是去寻找递归终止条件
- 3、确定选择列表,即需要把什么数据存储到结果里面
- 4、判断是否需要剪枝,去判断此时存储的数据是否之前已经被存储过
- 5、做出选择,添加元素到路径,递归调用该函数,进入下一层继续搜索
- 6、撤销选择,回到上一层的状态
- 基本思想是:为了求得问题的解,先选择某一种可能情况向前探索,在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索,如此反复进行,直至得到解或证明无解。
- 当问题碰到走不通的路径,需要"回头",以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止。
- 注意:在回溯算法中,在回溯函数中对路径要使用path.copy()的拷贝操作!!!
- 题型
- 子集(没有顺序)、排列类问题(有顺序)
- 组合类问题
- 搜索、N皇后类问题
- 子集问题&&排列问题
- LeetCode 78、子集(典型回溯算法)
- 1、画出递归树,找到状态变量(回溯函数的参数)
- 结合最终结果来画递归树。
- 2、寻找结束条件,由于回溯算法是借助递归实现,所以也就是去寻找递归终止条件。
- 当访问的数组下标超过了 nums 数组的长度时,递归结束。
- 3、确定选择列表,即需要把什么数据存储到结果里面。
- 4、判断是否需要剪枝,去判断此时存储的数据是否之前已经被存储过
- 由于题目要求我们去存储所有的子集,也就意味着不存在丢弃的操作,也就不需要剪枝了。
- 5、做出选择,把本次递归访问的元素加入到 subset 数组中,递归调用该函数,进入下一层继续搜索
- 1、画出递归树,找到状态变量(回溯函数的参数)
- LeetCode 90、子集II(加入了剪枝操作)
- 剪枝:if j > i and nums[j] == nums[j - 1]: continue
- 时间复杂度:O(n * 2 ^ n)。一共 2^n 个状态,每种状态需要 O(n) 的时间来构造子集。
- 空间复杂度:O(n)。临时数组 subsets 的空间代价是 O(n),递归时栈空间的代价为 O(n)。
- LeetCode 46、全排列(有顺序)
- 和子集问题的不同点在于全排列可以走之前走过的元素,比如213,所以在回溯函数中,只有当递归结束时,我们才把path添加到res中(子集问题中,每次回溯都要将path添加到res中)
- 可以设置一个True/False数组来记录元素是否被使用过,即在做选择是将nums[i]=True,退出选择时将nums[i]=False
- LeetCode 47、全排列II(加入了剪枝操作)
- 剪枝:如果当前元素和前一个元素相同,并且前一个已经使用过了,就剪去
- if i > 0 and nums[i] == nums[i - 1] and used[i - 1]: continue
- LeetCode 78、子集(典型回溯算法)
- 组合问题
- LeetCode 39、组合总和(要剪枝)
- 剪枝操作:对于4来说,它是由7选择3得到的,那么为什么7不选择2得到4呢?因为前面得到5的时候已经选择过2了,所以也就是说在4这条路径上已经不能选择2了,所以剪枝掉(这里path里的元素是选择的数字,不是节点的数字)。也就是说,回溯函数中的for循环i是从start位置而非0位置开始的,并且start是从i递归的,即后续可以选的元素一开始只能从 start 开始(注意和组合总和II的区别,II中是从i+1递归的,即不选当前i的元素了)
- LeetCode 40、组合总和II(要剪枝的剪枝)
- 剪枝操作:同一层相同数值的结点,从第 2 个开始,结果一定发生重复,因此跳过,用 continue;并且now_position也从上一题的i变为了现在这道题的下一个元素,即i+1(因为一个元素只能使用一次)
- LeetCode 77、组合
- 填坑操作:从1到n中选择k个数来填k个坑,先填第一个坑,再从剩下的元素里面选一个来填剩下的k-1个坑......
- 注意选择列表为for i in range(start, n - k + 1 + 1): n-k表示1到n中,还需要填k个坑,那么只能取到第n-k+1的位置,但是这个位置也可以取,所以是range(start, n - k + 1 + 1)
- 比如[1,2,3,4,5,6]如果选3个数,那么start位置只能选到4这个数
- LeetCode 39、组合总和(要剪枝)
- 简答题
- 子集问题、排列问题、组合问题都是回溯算法里的高频题目,这几种问题的解答在代码上有什么区别和联系?
- 三者都是选用回溯算法的模板进行解题
- 子集问题关注是否包含元素,排列问题关注元素的顺序,组合问题关注元素的组合方式。子集问题关注是否包含元素,排列问题关注元素的顺序,组合问题关注元素的组合方式。
- 子集问题、排列问题、组合问题都是回溯算法里的高频题目,这几种问题的解答在代码上有什么区别和联系?
第27-28天学习笔记——二叉树经典题、回溯算法
最新推荐文章于 2024-09-27 16:14:00 发布