二叉树总结

9 阅读9分钟

基本认识

  • 满二叉树: 只有度为0或者度为2的节点,而且度为0的节点全部在同一层。
  • 完全二叉树:除了最后一层,其他每层都是满的, 且最后一层所有节点集中在左侧。 
  • 二叉搜索树: 如果左子树不为空,那么左子树所有节点的值小于根节点。 如果右子树不为空,那么右子树所有节点的值大于根节点。  左右子树也分别是二叉搜索树。 
  • 平衡二叉搜索树: 树为空 或者 左右两个子树的高度差不超过1。 并且左右两个子树的都是平衡二叉搜索树。

表达方式

链式, 顺序(根节点下标为i,左孩子 下标为 i * 2 +  1,  右孩子 下标为 i * 2 +  2 )

遍历方式

广义划分:深度优先遍历、广度优先遍历。 
深度优先遍历分为: 前中后序遍历。 广度优先遍历主要指: 层序遍历
代码形式有两种:递归遍历、 迭代遍历。

题目简析

  1. 二叉树的递归遍历
    解析: 递归遍历是二叉树重要的基础操作之一。后续二叉树的各种操作会基于此操作 和 迭代遍历进行。  注意顺序:所谓前中后序,就是中间节点的位置。

  2. 二叉树的迭代遍历 解析:使用迭代法进行二叉树的前中后序遍历 还是有比较大的差异的。
    二叉前序 后序遍历可以复用,调整下前序的顺序,变为中右左, 再进行完全的反转既为后序遍历。
    中序的遍历的差异比较大。需要先找到最左侧节点。 取用栈顶节点后,再压栈右节点。

  3. 二叉树的统一迭代法
    二叉树的统一迭代方式 可以理解为是使用标记法。
    将需要遍历的节点后面加上空元素, 标记为要遍历了。
    另外需要注意每次遍历前都需要移除栈顶元素, case1是防止栈顶元素重复添加,case2是移除空的标记元素。
    记住这一个就可以随意切换前中后序迭代遍历了。

  4. 二叉树层序遍历登场!
    层序遍历很好理解, 这里用的是堆,将每一层的元素导入堆里,再依次按照每次堆的原始大小,遍历每层元素。

  5. 226.翻转二叉树 *
    回忆时还是容易忘。
    直接在前序递归遍历的基础上进行swap 左右节点即可。
    如果想使用迭代遍历,使用大一统迭代遍历形式,处理节点时swap即可。

  6. 101. 对称二叉树 *
    思路还是挺难的。 注意递归定义: 传入左右孩子,返回是否对称的左右孩子是否对称。

  7. 104.二叉树的最大深度
    层序遍历既可解决 或者 后序递归

func maxDepth(_ root: TreeNode?) -> Int { 
    guard let root else { return 0 } 
    return 1 + max(maxDepth(root.left), maxDepth(root.right)) 
}
  1. 111.二叉树的最小深度

首先层序遍历依然好使。
递归方法的话 需要分开考虑以下几种情况(左子树为空,右子树不为空) (右子树为空,左子树不为空) (左子树不为空,右子树不为空)

  1. 110.平衡二叉树 *
    重点定义:返回以该节点为根节点的二叉树的高度,如果不是平衡二叉树了则返回-1
// 递归法
class Solution {
    func isBalanced(_ root: TreeNode?) -> Bool {
        height(root) == -1 ? false : true
    }

    func height(_ root: TreeNode?) -> Int {
        if root == nil { return 0 }
        let leftHeight = height(root?.left)
        let rightHeight = height(root?.right)
        if leftHeight == -1 || rightHeight == -1  { return -1 }

        if abs(leftHeight - rightHeight) > 1 { return -1 }
        return 1 + max(leftHeight, rightHeight)
    }
}
  1. 257. 二叉树的所有路径
    因为用了一个会穿透递归的path变量, 所以回溯的时候需要移除当前节点。
    回溯和递归是一一对应的,有一个递归,就要有一个回溯

  2. 404.左叶子之和
    此题的难点主要在如何判断左叶子上。
    节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点

  3. 222.完全二叉树的节点个数
    如果不用完全二叉树的性质 则直接如下求法即可。

class Solution {
    func countNodes(_ root: TreeNode?) -> Int {
        if root == nil { return 0}
        return 1 + countNodes(root?.left) + countNodes(root?.right)
    }
}

进阶做法需要依靠完全二叉树的性质, 相当于利用性质在递归过程中剪枝。

    完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。  
    对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。  
    对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。

13. 513.找树左下角的值

题目转化为要找深度最大的叶子节点。
这里写一下如何记录深度模版代码

class Solution {
    func findBottomLeftValue(_ root: TreeNode?) {
        recursive(root, depth: 0)
    }
    func recursive(_ root: TreeNode?, depth: Int) {
        print("当前深度\(depth)")
        // 不使用depth指针传递,则不需要处理回溯 - 1的逻辑了。
        if let left = root?.left { recursive(left, depth: depth + 1) }
        if let right = root?.right { recursive(right, depth: depth + 1) }
    }
}
  1. 112. 路径总和
    注意回溯的写法,递归终止条件好写。 注意带回当前递归处理的结果回去(左右节点的结果取或)。
    另外此题精简版本非常漂亮
class Solution {
    func hasPathSum(_ root: TreeNode?, _ targetSum: Int) -> Bool {
        guard let root else { return false}
        if root.left == nil, root.right == nil { return targetSum == root.val }
        return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val)
    }
}
  1. 106.从中序与后序遍历序列构造二叉树 * 首先要搞清楚手动构造的过程, 用后续遍历的结果作为根节点,对中序遍历进行切割。
    另外此题麻烦的地方在于,切割的时候 各种下标的处理。 递归的定义

func buildTreeRecursive(_ inorder: [Int], _ postorder: [Int], _ inBegin: Int, _ inEnd: Int, _ postBegin: Int, _ postEnd: Int) -> TreeNode?

  1. 654.最大二叉树
    按照15题的思路, 每次找最大值的坐标, 然后拆分左右区间,递归构造左右子树即可。
    注意保持左右区间开闭的一致性。

func buildTree(_ nums: [Int], _ left: Int, _ right: Int) -> TreeNode?

  1. 617.合并二叉树
    关键点:
    相信这道题目很多同学疑惑的点是如何同时遍历两个二叉树呢?
    其实和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。

func mergeTrees(_ root1: TreeNode?, _ root2: TreeNode?) -> TreeNode?

  1. 700.二叉搜索树中的搜索
    比较简单,左移二叉搜索树的顺序就行。
    迭代法也很清爽,算是个基础操作。
// 迭代法
class Solution {
    func searchBST(_ root: TreeNode?, _ val: Int) -> TreeNode? {
        var node = root
        while let curNode = node {
            if val > curNode.val { node = curNode.right }
            else if val < curNode.val { node = curNode.left }
            else { return curNode }
        }
        return nil
    }
}
  1. 98.验证二叉搜索树
    注意二叉树的性质,按照中序遍历左升序判断即可。

  2. 530.二叉搜索树的最小绝对差
    和19题几乎一模一样。

  3. 501.二叉搜索树中的众数
    和上面的19 20 题几乎一样。 要注意的是众数结果 有可能有多个,用数组返回。

  4. 236. 二叉树的最近公共祖先 *
    这里有一点理解是在总结的时候想到了。
    看视频题解 提到了下面这两句的效果是:left知道了左子树是否包含p或者q,right是知道了右子树是否包含p或者q。
    可是这个递归函数的定义,不应该是返回左(右)子树的最近祖先节点吗?

let left = lowestCommonAncestor(root?.left, p, q)
let right = lowestCommonAncestor(root?.right, p, q)

严格理解应该是这样的: 这个函数的返回值是p,q的最近公共祖先 或者是 p,q自身。
又因为题目说了 p,q肯定包含于二叉树中。那么传入root节点。
最后的返回结果一定是最近公共祖先(哪怕root本身就是p或者q)

  1. 235. 二叉搜索树的最近公共祖先 *
    思路巧妙:
    因为是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。
    即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p。

  2. 701.二叉搜索树中的插入操作
    按照二叉搜索树的特性, 沿着树往下找, 碰到的第一空节点 就是它该插入的位置。

  3. 450.删除二叉搜索树中的节点
    主要是删除的四种情况, 左右子树都为空, 左子树为空右子树不为空,右子树为空左子树不为空,左右子树都不为空。

  4. 669. 修剪二叉搜索树
    重点:
    如果递归到的节点 比下界小,那么直接舍弃左子树, 返回裁剪后的右子树(这里右子树需要继续递归)
    如果递归到的节点 比上界大,那么直接舍弃右子树, 返回裁剪后的左子树(这里左子树需要继续递归)

  5. 108.将有序数组转换为二叉搜索树
    直接找mid元素作为根节点, 递归构造即可。 注意开闭区间的统一。

  6. 538.把二叉搜索树转换为累加树
    巧思:
    中序的逆序遍历,来进行累加皆可。

基本操作

  1. 如何记录深度? 第13题
  2. 如何记录高度? 第9题
  3. 如何统一迭代? 第3题

附录

随想录二叉树总结