第27-28天学习笔记——二叉树经典题、回溯算法

  • 二叉树经典题
    • 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
      • 当左子树的高度不等于右子树的高度时,因为完全二叉树的定义,因此左子树必然大于右子树高度,并且右子树是满二叉树,可以计算右子树的节点数
      • 当左子树的高度等于右子树的高度时,左子树是满二叉树,可以计算左子树的节点数
    • 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 数组中,递归调用该函数,进入下一层继续搜索
      • 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 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这个数
  • 简答题
    • 子集问题、排列问题、组合问题都是回溯算法里的高频题目,这几种问题的解答在代码上有什么区别和联系?
      • 三者都是选用回溯算法的模板进行解题
      • 子集问题关注是否包含元素,排列问题关注元素的顺序,组合问题关注元素的组合方式。子集问题关注是否包含元素,排列问题关注元素的顺序,组合问题关注元素的组合方式。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值