算法学习打卡day23|二叉树题目总结

二叉树的理论基础

二叉树的种类(这里都是我自己的理解,都是大白话)

满二叉树
  • 什么是满二叉树?
    • 就是一棵树的所有节点要不度为0,要不度为2,什么是度?就是一个节点的子节点的个数,说白了就是这颗树的最后一层全部为叶子节点,而且最后一层的节点数目为 2 h − 1 2^{h-1} 2h1,h为树的高度,其他层都是度为2的节点。
  • 性质
    • 满二叉树的节点数为 2 h − 1 2^h-1 2h1
完全二叉树
  • 什么是完全二叉树?
    • 就是一颗二叉树除了最后一层其他层都是满二叉树(达到了每层的最大节点数),而最后一层所有的节点走在这层的左边,只要最后一层节点从左到右排列中间出现了空节点,那么就不是完全二叉树了。
  • 性质
    • 下标(从0开始)小于 n / 2的为分支节点,大于n / 2的为叶子节点
    • 如果有度为1的节点,那一定只有左孩子而没有右孩子
    • 若节点数为奇数则全部为度为2或者度为0的节点,偶数则有一个节点度为1,且只有左孩子,其实这个度为1的节点下表为 n/2(如果下标从0开始就是n / 2 - 1
    • 已知完全二叉树的节点数目n,那么数的高度为: log ⁡ ( n + 1 ) \log(n + 1) log(n+1)
二叉搜索树
  • 什么是二叉搜索树?
    • 二叉树是有序数(节点上带数字),每个节点的左子树要不为空,要不就都是比它的值小,右子树要不为空,要不就是比它的值大,二叉搜索树没有重复元素。
平衡二叉搜索树(AVL)
  • 什么是平衡二叉树?什么是平衡二叉搜索树?
    • 平衡二叉树是指的是一棵树的左右子树的高度之差的绝对值小于等于1
    • 平衡二叉搜索树就是在二叉搜索树的基础上满足平衡二叉树条件。
    • C++中map、set、multimap,multiset的底层实现都是红黑树,红黑树也是属于平衡二叉搜索树的。
红黑树
  • 为什么需要红黑树?
    • 对于二叉搜索树,如果插入的数据是随机的,那么它就是接近平衡的二叉树,而平衡的二叉树,它的操作效率(查询,插入,删除)效率较高,时间复杂度是O(logN)。但是可能会出现一种极端的情况,那就是插入的数据是有序的(递增或者递减),那么所有的节点都会在根节点的右侧或左侧,此时,二叉搜索树就变为了一个链表,它的操作效率就降低了,时间复杂度为O(N),所以可以认为二叉搜索树的时间复杂度介于O(logN)和O(N)之间,视情况而定。那么为了应对这种极端情况,红黑树就出现了,它是具备了某些特性的二叉搜索树,能解决非平衡树问题,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。
    • 现在就知道它是类似平衡二叉树就行,有时间再研究。
    • 使用:搜索的次数远远大于插入和删除,选择AVL树;搜索、插入、删除次数几乎差不多,选择红黑树
      相对于AVL树来说,红黑树牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树
      红黑树的平均统计性能优于AVL树,实际应用中更多选择使用红黑树
    • 具体可以看这里:红黑树详解
  • 堆是一棵完全二叉树同时保证父子节点的顺序关系(有序)。 但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点(大根堆,小根堆反之),而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树。

二叉树题目分类

在这里插入图片描述

二叉树的定义

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

四种遍历方式(二叉树所有题目都是这四种遍历的延伸)

力扣题目链接: 前序遍历中序遍历后序遍历

先说下二叉树遍历

  • 二叉树主要有两种遍历方式:
    • 深度优先遍历:先往深走,遇到叶子节点再往回走。
      • 前序遍历(递归法,迭代法)
      • 中序遍历(递归法,迭代法)
      • 后序遍历(递归法,迭代法)
    • 广度优先遍历:一层一层的去遍历。
      • 层次遍历(迭代法)
    • 这两种遍历是图论中最基本的两种遍历方式,和图论的一样。
  • 用如下二叉树下面举个例子:
    在这里插入图片描述
    层次遍历为:5 4 6 1 2 7 8

代码实现思路

递归法
  • 递归的步骤:三部曲
    • 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
    • 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
    • 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
    • 其实递归就是把复杂的问题化为子问题,每个子问题的处理逻辑都是一样的,我们把握好一个子问题,其余问题交给递归就行了,所以,终止条件的处理和返回值的设定尤为重要,具体下面有写。
迭代法
  • 再来说说迭代法,迭代法就是用栈和队列来模拟,前中后序用栈来模拟,层次用队列来模拟,后序所有的题目基本离不开这四种遍历模版。
    • 前序遍历:借助栈的先进后出原理,每次循环,先把根节点输出,然后把🈶️节点、左节点压栈,注意是先右后左,这样下次压栈右压的左节点的右左,依此类推,最后才处理右子树,最终达到中>左>右的顺序。
    • 中序遍历:和前序遍历有点区别就是写中序遍历时,不需要提前将root节点放入栈里,判断条件也改为cur非空或者栈非空,然后在循环里先让cur指针每次都往左子树走,直到到达左子树的最后一层的最左边节点,然后输出该节点,再把该节点的右子树入栈,等它的右子树处理完后,出栈时就到了该节点的父节点了,依次类推,达到左>中>右的效果。
    • 后序遍历:
      • 方法一:把前序遍历压栈顺序调整一下,变为左右压栈,那么最后得到的序列为中>右>左,然后数组反转一下就好了。
      • 方法二:借助两个栈,循环条件是判断第一个栈非空,第一个栈按左右的顺序压栈,那么出栈顺序为右左,也就是下一轮循环右节点出栈,然后第二个栈存放根节点,最后得到的第二个栈整体是中右左的顺序,然后遍历第二个栈,依次出栈存到数组即为左右中的顺序。
统一迭代法
  • 当然还有一种统一迭代法,因为前面的迭代法三种遍历方式各不相同,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
    那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。当然这里要标记一下要处理的节点就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。(具体的看这里二叉树的统一迭代法
  • 层次遍历
    • 借助队列来实现,每次先输出根节点,然后把他的左右子节点存入队列,然后下一轮循环取出的就是他的左右子节点了,依次类推实现按层遍历
    • 我们在这里和前序遍历做个比较,会发现同样是先输出根节点,一个是将左右压入栈,一个是将左右压入队列,得到的结果完全不同,这就是栈和队列原理不同,栈是先入后出,而队列是先出,虽然都将左右压入容器里了,但是前序遍历在下一轮的时候把当前节点的左节点的左右压入栈中了,当前节点的右节点就沉底了,而层次遍历在下一轮也是将当前节点的左节点的左右压入队列中,但是当前节点的右节点还是在队首!

求二叉树的属性

  • 二叉树:最大深度
    • 递归法:后序遍历,拿到左右子树深度,返回二者最大值 + 1
    • 迭代法:层次遍历取最大深度即可
  • 二叉树:最小深度
    • 递归法:依然是后序遍历,但是需要添加一个对叶子节点的判断,只有左右都不是空才返回深度最小值 + 1,如果root是叶子节点直接返回1,如果度为1,就返回递归后非空子节点的深度 + 1.
    • 迭代法:层次遍历,第一次遇到叶子节点就break,此时为最小深度。
  • 对于二叉树深度和高度
    • 深度为从根节点开始走到某个节点的边或者节点的数量作为深度,所以一般求深度用前序遍历。
    • 而高度是从最底层开始到某个节点的高度,一般求高度用后序遍历,只有后序遍历是自底向上。
  • 二叉树:节点个数
    • 递归法:后序遍历,返回左右子树个数+1
    • 迭代法:前中后序遍历都可以,最后返回节点树即可。
    • 完全二叉树节点个数求法:判断二叉树是否为满二叉树,如果是直接公式法求解(C++里可以使用左移和右移来进行次方运算,如:(2 << h) - 1),不是的话就递归左右子树,再将返回值求和。
      • 如何判断是满二叉树,满二叉树左右子树都是满二叉树且深度相等,那么一个指针一直往左走,一个指针一直往右走,求得左右子树的层树,相等就是满二叉树,不相等不是咯。
  • 二叉树:是否平衡
    • 递归法:整体采用后序遍历,但这里不能简单的求左右子树的高度之差,然后返回true或者false,因为还得判断左右子树是否是平衡二叉树呢,所以只要当左右子树都是平衡二叉树的前提下,再去求高度之差,返回true或false就对了,当然由于我们需要用到树的高度,所以返回值为树的高度,用-1来标记此树不是平衡二叉树
    • 怎么判断左右子树是平衡二叉树呢?
      • 很简单,增加判断条件,把后序遍历的两个递归返回值加个判断,只有两者都是平衡二叉树时,才求差值,返回-1或者当前树高度。
    • 迭代法:效率太低了,不用写了。
  • 二叉树:所有路径
  • 递归法:此题应该是采用前序遍历了,该题的函数退出条件为碰到叶子节点就return,并把路径存到results里,然后左空就递归右子树,右空就递归左子树,都不为空就都递归,但是注意回溯以及循环不变量。
  • 这个题的两个需要关注的点(包括下面路径总和、路径总和2也是):
    • 第一是循环不变量:在递归之前加下一层的path呢还是进入递归后,由当前层自己加path,这两种都是可以的,只是要在实现代码期间一定要保持一致!!!
    • 第二是回溯思想:就是在递归的时候,递归左子树之后,路径要回退到递归左子树之前,以便右子树使用
  • 迭代法:前序遍历,但是还得定义一个栈(普通前序遍历栈里存的树节点,这里可以存pair)用来同步存储路径,跟随着前序遍历一起走就行,当遇到叶子节点就进行处理,存到结果集里。
  • 二叉树:是否对称
    • 递归法:关键点在于退出条件的判断,左空右不空,右空左不空,左右不空但不相等,直接返回false,左右都空返回true,剩下交给递归,这里递归判断是外边和外边对比,里边和里边对比,下一题是相同位置对比,其余思路都一样
    • 迭代法:使用队列或者栈,同时存两个节点进行比较
  • 二叉树:两颗树是否相同,回溯思想了解下?
    • 递归法:和上题基本相同,修改一下即可。
    • 迭代法:和上题类似
  • 二叉树:另一棵树的子树
    • 递归法:compare部分和上题相同,在调用的时候,每次跟节点进来先调用compare函数,然后将自己的左右子节点和subroot比较,这样的话就不会漏掉子树,而子树进去之后依然是先调用compare比较子树和subroot比较,然后让子树的左右节点再和subroot比较。
      而compare函数内就是单纯的比较两颗树是否相同的逻辑(子函数也是递归噢!!!)
  • 二叉树:求左叶子之和
    • 递归法:还是采用后序遍历,得到左子树的左叶子之和和右子树的左叶子之和,相加即可。
    • 本题需要注意的地方:做这个题需要找到叶子节点,但是无法区分是左叶子还是右叶子,所以这道题和其他题不一样,是要跟据当前节点去找他的左叶子节点,即root->left->left == nullptr && root->left->right == nullptr这种情况才是正确的,其他情况就去递归左子树,右子树不用判断(因为右子树的判断交给递归了!!!),然后返回左右子树返回值之和。
    • 迭代法:套用后序遍历的模版,这里层次遍历也可以实现,找每个节点的左叶子就行。
  • 二叉树:找左下角的值
    • 递归法:顾名思义,只需要找到最后一层,然后找到最后一层的第一个值,这里一定是采用前序遍历了,因为前序遍历就是先遍历左子树,但其实中序遍历和后序也是可以的,因为三种遍历方式都是先左再右。(假如最大层数在左子树,那一定是第一个值啦!,后序即使遇到了最大层树也不会更新了),但是我们需要保存一个全局的深度depth,用来判断是不是最大深度
    • 迭代法:很简单,层次遍历,取最后一层的第一个元素
    • 求二叉树的各种最值,就想应该采用什么样的遍历顺序,确定了遍历循序,其实就和数组求最值一样容易了。
  • 二叉树:求路径总和
  • 递归法:遍历顺序无所谓,递归函数返回值为bool类型是为了搜索一条边,没有返回值是搜索整棵树(路径总和2),和上面求所有路径差不多,只是从求的路径里找符合要求的路径而已,但是这个操作在遍历期间就可以实现,还是记得注意回溯!!!
  • 另外路径总和2这道题可以先写出在外层push的版本,这个版本可以体现出回溯的过程,然后再修改为在递归函数开始处push路径,最后还可以优化把递归函数参数里的vector添加上引用。
  • 迭代法: 套用前中后序迭代法模版就行,就是普通迭代法栈存储的是树节点,而增加一个pair<TreeNode*, int>用来存储节点和sum,注意回溯。

二叉树的修改与构造

  • 二叉树翻转

    • 递归法:前序和后序甚至层次遍历都可以翻转,套用模版,然后加个swap操作即可。
    • 迭代法:同样套用模版即可。
  • 从中序与后遍历序列构造二叉树从中序与后遍历序列构造二叉树

    • 递归法:
      • 首先要知道后序遍历时,根节点在最后一个元素,而前序遍历根节点在第一个元素,这样可以通过这个去从中序遍历里找根节点,从而得到左右区间,最后递归左右子区间就可以了。
      • 步骤分为六步:
        • 第一步:判断根节点是否为空,如果为空返回nullptr
        • 第二步:从后序数组取最后一个元素作为(前序遍历为第一个元素)根节点
        • 第三步:从中序遍历中查找到根节点的位置,获取root_index
        • 第四步:根据index,拆分中序遍历为左右子区间,即左右子树
        • 第五步:因为不论前中后序遍历,左右子树节点数是不变的,而后序遍历是左>右>中的顺序,所以直接按区间数就可以拆分后序遍历数组的左右子树区间。(前序遍历是一样的,中>左>右,从第二个元素开始取值就行)
        • 第六步:分别拿到了中序和后序、中序和前序的左右子区间,就转化为子问题了,直接交给递归,然后将返回的子树接到root上,返回root即可。
        • 在分割的时候,一定要✊左闭右开的原则(如果不用下标写,创建vector时vec.end()刚好是开区间
    • 迭代法:太复杂了
  • 最大二叉树

    • 递归法:比上一个题还简单点,直接取数组的最大值作为根节点,然后用它的代表去分割左右区间,剩下的就是递归了。
    • 迭代法:太复杂了
  • 合并二叉树

    • 递归法:依旧是先确定根节点,如果有一个度为1,就递归另一个,将其结果返回,如果都不为空,求和作为新的根节点,然后递归左右子树,将左右子树接到root上,返回root。
    • 迭代法:层次遍历同时遍历两颗子树。

求二叉搜索树的属性

  • 二叉搜索树中的搜索

    • 递归法:比目标值小就往右递归,比目标值大就往左递归
    • 迭代法:一样的道理不需要借助栈或队列,比目标值小就往右走,比目标值大就往左走,找到直接return。
  • 接下来的四个题目其实是一道题,都要借助pre节点来实现,在二叉树中通过两个前后指针作比较,会经常用到。

  • 验证二叉搜索树

    • 递归法:因为二叉搜索树中序遍历是整体升序的,所以采用中序遍历,在中间节点处理逻辑上,加上判断是否比前一个节点大即可。
    • 迭代法:套用中序遍历模版,加上判断即可。
  • 二叉搜索树的最小绝对差

    • 递归法:和上题一样,但是要增加一个全局的差值。
    • 迭代法:同上。
  • 501.二叉搜索树中的众数

    • 递归法:
      • 方法一:还是中序遍历也是存储一个pre节点,然后每次都和上一个节点比较,相同就计数器+1,不同就更新计数器为1,只要比计数器大,就清空结果集,然后重新添加。
      • 方法二:遍历一遍二叉树,然后用哈希map存起来每个数字的次数,排序,取众数即可。
  • 把二叉搜索树转换为累加树

    • 递归法:依然是中序遍历,不过得是将序,所以左右递归顺序换一下就行了!
    • 迭代法:套用中序遍历模版,交换左右子树压栈顺序。

二叉搜索树的修改与构造

  • 二叉搜索树中的插入操作

    • 递归法:还是二叉搜索树的遍历方法,val比跟节点大,就忘右走,比跟节点小就往左走,如果往左走的时候左节点为空了,把val存到cur的左节点,往右走的时候右节点为空了,把val存到cur的右节点,剩下的递归就行了,最后返回root节点。
    • 迭代法:和递归法类似,套用二叉搜索树遍历模版
  • 删除二叉搜索树中的节点

    • 递归法:套用二叉搜索树的遍历法模版,然后在找到目标值时增加判断,分以下几种情况:
      1. 目标值为空,说明没找到,返回nullptr。
      2. 目标值为叶子节点,直接删除,返回nullptr。
      3. 目标值为非叶子节点,这里又分为三种情况
        1. 左子树空,右子树不空,返回右子树
        2. 左子树不空,右子树空,返回左子树
        3. 都不为空,就得取左右子树其中一个作为新的root返回了,这里以左子树为root举例,先找到左子树最大的那个节点,就是一直往右走,然后把右子树接到这个最大节点的右子树上。
  • 修剪二叉搜索树

    • 递归法:采用中序遍历,如果根节点值比low小,证明区间在树的右子树上,就往右走,如果跟节点值比high大,说明区间在左子树上,就往左走,如果根节点恰好在区间里,那么就把根节点不动,去分别递归左右子树,返回的结果接在根节点的左右指针上就行。
    • 迭代法:先找到根节点,然后对根节点的左右子树做剪枝(其实递归法也是这个逻辑,前两个判断不就是在找根节点嘛!!!,找到了再对左右子节点做剪枝)。
      • 根节点怎么找?
        • 这不就是在找二叉树的公共祖先嘛,只要遍历到root在区间里就行了,方法一样!
      • 左右子树怎么剪枝?
        • 剪左子树的时候,我们要剪掉比low小的,那么就去判断cur的左子树是不是比low小,如果比low小,那么它左子树的左子树以及它都剪掉,把左子树的右子树接到cur到左子树上,此时cur的左子树已经更新,然后继续对cur的左子树判断,直到全部遍历完。
      • 剪右子树和左子树一样的逻辑,去找cur的右子树,只要比high大,就把右子树的左子树接到cur的右子树上,依此类推。
  • 108.将有序数组转换为二叉搜索树

    • 递归法:利用平衡二叉搜索树的性质,正好数组有序,我们每次取数组的中间元素作为根节点,然后递归左右子区间就好了,这里要注意区间的开闭,一定要保持一致。
    • 迭代法:很复杂。

二叉树的公共祖先问题

  • 二叉树的最近公共祖先
    • 递归法:后序遍历,后序遍历才是从下往上找!然后分情况判断其返回值,都为空就返回空,都不为空,返回root,一个为空,一个不为空就返回另一个,这个题要注意退出条件,如果root是p或者q直接返回即可,另外root为空也直接返回
    • 迭代法:太麻烦了
  • 二叉搜索树的最近公共祖先
    • 递归法:遍历二叉搜索树,只要发现节点值在目标区间内就一定是公共祖先
    • 迭代法:一样的思路。

方法总结

递归函数相关疑问

  • 什么时候递归函数前加if判断?
    • 一般情况来说:如果让空节点(空指针)进入递归,就不加if,此时对于空节点的判断交给函数的退出条件那里了,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。
  • 递归函数什么时候需要返回值,什么时候不需要?
    • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(113.路径总和ii)
    • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (二叉树的最近公共祖先)
    • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(路径总和)

在二叉树题目选择什么遍历顺序呢?

  • 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中间节点。

  • 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。

  • 求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。

  • 注意在普通二叉树的属性中,一般为后序,但是单纯求深度就用前序,求最大深度就是求高度了那就用后序了,二叉树:找所有路径 (opens new window)也用了前序,这是为了方便让父节点指向子节点。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值