基本认识
- 满二叉树: 只有度为0或者度为2的节点,而且度为0的节点全部在同一层。
- 完全二叉树:除了最后一层,其他每层都是满的, 且最后一层所有节点集中在左侧。
- 二叉搜索树: 如果左子树不为空,那么左子树所有节点的值小于根节点。 如果右子树不为空,那么右子树所有节点的值大于根节点。 左右子树也分别是二叉搜索树。
- 平衡二叉搜索树: 树为空 或者 左右两个子树的高度差不超过1。 并且左右两个子树的都是平衡二叉搜索树。
表达方式
链式, 顺序(根节点下标为i,左孩子 下标为 i * 2 + 1, 右孩子 下标为 i * 2 + 2 )
遍历方式
广义划分:深度优先遍历、广度优先遍历。
深度优先遍历分为: 前中后序遍历。 广度优先遍历主要指: 层序遍历
代码形式有两种:递归遍历、 迭代遍历。
题目简析
-
二叉树的递归遍历
解析: 递归遍历是二叉树重要的基础操作之一。后续二叉树的各种操作会基于此操作 和 迭代遍历进行。 注意顺序:所谓前中后序,就是中间节点的位置。 -
二叉树的迭代遍历 解析:使用迭代法进行二叉树的前中后序遍历 还是有比较大的差异的。
二叉前序 后序遍历可以复用,调整下前序的顺序,变为中右左, 再进行完全的反转既为后序遍历。
中序的遍历的差异比较大。需要先找到最左侧节点。 取用栈顶节点后,再压栈右节点。 -
二叉树的统一迭代法
二叉树的统一迭代方式 可以理解为是使用标记法。
将需要遍历的节点后面加上空元素, 标记为要遍历了。
另外需要注意每次遍历前都需要移除栈顶元素, case1是防止栈顶元素重复添加,case2是移除空的标记元素。
记住这一个就可以随意切换前中后序迭代遍历了。 -
二叉树层序遍历登场!
层序遍历很好理解, 这里用的是堆,将每一层的元素导入堆里,再依次按照每次堆的原始大小,遍历每层元素。 -
226.翻转二叉树 *
回忆时还是容易忘。
直接在前序递归遍历的基础上进行swap 左右节点即可。
如果想使用迭代遍历,使用大一统迭代遍历形式,处理节点时swap即可。 -
101. 对称二叉树 *
思路还是挺难的。 注意递归定义: 传入左右孩子,返回是否对称的左右孩子是否对称。 -
104.二叉树的最大深度
层序遍历既可解决 或者 后序递归
func maxDepth(_ root: TreeNode?) -> Int {
guard let root else { return 0 }
return 1 + max(maxDepth(root.left), maxDepth(root.right))
}
首先层序遍历依然好使。
递归方法的话 需要分开考虑以下几种情况(左子树为空,右子树不为空) (右子树为空,左子树不为空) (左子树不为空,右子树不为空)
- 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)
}
}
-
257. 二叉树的所有路径
因为用了一个会穿透递归的path变量, 所以回溯的时候需要移除当前节点。
回溯和递归是一一对应的,有一个递归,就要有一个回溯 -
404.左叶子之和
此题的难点主要在如何判断左叶子上。
节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点 -
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) }
}
}
- 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)
}
}
- 106.从中序与后序遍历序列构造二叉树 *
首先要搞清楚手动构造的过程, 用后续遍历的结果作为根节点,对中序遍历进行切割。
另外此题麻烦的地方在于,切割的时候 各种下标的处理。 递归的定义
func buildTreeRecursive(_ inorder: [Int], _ postorder: [Int], _ inBegin: Int, _ inEnd: Int, _ postBegin: Int, _ postEnd: Int) -> TreeNode?
- 654.最大二叉树
按照15题的思路, 每次找最大值的坐标, 然后拆分左右区间,递归构造左右子树即可。
注意保持左右区间开闭的一致性。
func buildTree(_ nums: [Int], _ left: Int, _ right: Int) -> TreeNode?
- 617.合并二叉树
关键点:
相信这道题目很多同学疑惑的点是如何同时遍历两个二叉树呢?
其实和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。
func mergeTrees(_ root1: TreeNode?, _ root2: TreeNode?) -> TreeNode?
- 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
}
}
-
98.验证二叉搜索树
注意二叉树的性质,按照中序遍历左升序判断即可。 -
530.二叉搜索树的最小绝对差
和19题几乎一模一样。 -
501.二叉搜索树中的众数
和上面的19 20 题几乎一样。 要注意的是众数结果 有可能有多个,用数组返回。 -
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)
-
235. 二叉搜索树的最近公共祖先 *
思路巧妙:
因为是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。
即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p。 -
701.二叉搜索树中的插入操作
按照二叉搜索树的特性, 沿着树往下找, 碰到的第一空节点 就是它该插入的位置。 -
450.删除二叉搜索树中的节点
主要是删除的四种情况, 左右子树都为空, 左子树为空右子树不为空,右子树为空左子树不为空,左右子树都不为空。 -
669. 修剪二叉搜索树
重点:
如果递归到的节点 比下界小,那么直接舍弃左子树, 返回裁剪后的右子树(这里右子树需要继续递归)
如果递归到的节点 比上界大,那么直接舍弃右子树, 返回裁剪后的左子树(这里左子树需要继续递归) -
108.将有序数组转换为二叉搜索树
直接找mid元素作为根节点, 递归构造即可。 注意开闭区间的统一。 -
538.把二叉搜索树转换为累加树
巧思:
中序的逆序遍历,来进行累加皆可。
基本操作
- 如何记录深度? 第13题
- 如何记录高度? 第9题
- 如何统一迭代? 第3题