代码随想录算法训练营Day17
110.平衡二叉树
题目
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
示例 3:
输入:root = []
输出:true
提示:
- 树中的节点数在范围
[0, 5000]内 -104 <= Node.val <= 104
思路
递归方法
由平衡二叉树的定义,需要检查每一棵子树是否符合左右子树高度相差不超过1的这个条件,因此需要一个求二叉树高度的方法,这些方法,之前的题目已经出现过了。
确定递归函数的参数和返回值
递归的主题应该是求子树的高度,因此应该按照求子树高度的方法来设计。
每轮需要计算一个二叉树的高度,因此每次都需要传入子树的根节点;
要求子树的高度,那么返回值自然是当前的子树的高度;
确定终止条件
不难理解,如果当前节点为空,那么算是平衡的。
确定单层递归的逻辑
如果当前的节点不是空,那么它的平衡性可以根据它的左右子树的高度差来判定。
分别求出它的左子树和右子树的高度,求两者相减的绝对值即可判定结果。
迭代方法
使用后序遍历来迭代地检查二叉树的每个节点,确保每个节点的左右子树高度差不超过1。在非递归方法中使用后序遍历而非其他遍历方式的原因是,后序遍历允许先处理子节点再处理父节点,这对于计算节点的高度和判断平衡性非常有用。首先处理子节点可以确保在处理父节点时已经得到了所有子节点的高度信息。
迭代思路关键在于使用栈来模拟递归过程,同时记录每个节点的高度。具体步骤如下:
- 使用栈:创建一个栈来存储遍历过程中的节点和它们的高度。
- 后序遍历:使用迭代方式进行后序遍历。在遍历每个节点时,先将其左右子节点压入栈中。
- 计算高度:计算每个节点左右子树的高度,并更新当前节点的高度。
- 检查平衡:在每个节点处,检查左右子树的高度差是否超过 1。如果超过,立即返回 false。
- 返回结果:如果所有节点都满足高度差不超过 1 的条件,则返回 true。
代码实现
递归方法
func isBalanced(root *TreeNode) bool {
//空节点算是平衡的
if root == nil {
return true
}
//非空节点则要比较左右子树的相减的绝对值。
return abs(height(root.Left)-height(root.Right)) <= 1 && isBalanced(root.Left) && isBalanced(root.Right)
}
func height(root *TreeNode) int {
if root == nil {
return 0
}
return max(height(root.Left), height(root.Right)) + 1
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
迭代方法
func isBalanced(root *TreeNode) bool {
if root == nil {
return true
}
stack := []*TreeNode{} // 创建一个栈
node := root // 开始遍历的节点
var lastVisit *TreeNode // 记录最近访问过的节点
heights := make(map[*TreeNode]int) // 用于存储每个节点的高度
// 后序遍历
for node != nil || len(stack) > 0 {
// 遍历左子树,直到最左
for node != nil {
stack = append(stack, node)
node = node.Left
}
node = stack[len(stack)-1]
// 如果右子节点为空或已经访问过
if node.Right == nil || node.Right == lastVisit {
stack = stack[:len(stack)-1] // 弹出栈顶元素
leftHeight := heights[node.Left]
rightHeight := heights[node.Right]
// 检查左右子树高度差是否超过1
if abs(leftHeight-rightHeight) > 1 {
return false
}
// 更新当前节点的高度
heights[node] = max(leftHeight, rightHeight) + 1
lastVisit = node
node = nil
} else {
// 否则,转向右子树
node = node.Right
}
}
return true
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
257. 二叉树的所有路径
题目
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]
示例 2:
输入:root = [1]
输出:["1"]
提示:
- 树中节点的数目在范围
[1, 100]内 -100 <= Node.val <= 100
思路
递归方法
确定递归函数的参数和返回值
定义一个递归函数,接收当前节点和当前路径作为参数。
确定终止条件
如果当前节点是叶子节点,将当前路径添加到结果中,并返回。
确定单层递归的逻辑
如果当前节点不是叶子节点,递归调用左右子节点。在递归调用时,更新当前路径。
迭代方法
- 使用栈:创建一个栈来存储节点和对应的路径。
- 初始化:将根节点及其路径压入栈中。
- 迭代处理:当栈不为空时,弹出一个节点及其路径。
- 检查叶子节点:如果弹出的节点是叶子节点,则将其路径添加到结果中。
- 处理子节点:如果节点有子节点,将子节点及更新后的路径压入栈中。
- 返回结果:遍历完成后返回收集到的所有路径。
代码实现
递归方法
func binaryTreePaths(root *TreeNode) []string {
var paths []string
constructPaths(root, "", &paths)
return paths
}
// 递归构造路径
func constructPaths(node *TreeNode, path string, paths *[]string) {
if node != nil {
path += strconv.Itoa(node.Val)
if node.Left == nil && node.Right == nil { // 当前节点是叶子节点
*paths = append(*paths, path) // 把路径加入到答案中
} else {
path += "->" // 不是叶子节点,继续递归遍历
constructPaths(node.Left, path, paths)
constructPaths(node.Right, path, paths)
}
}
}
迭代方法
type NodePath struct {
node *TreeNode // 当前节点
path string // 到当前节点的路径
}
func binaryTreePaths(root *TreeNode) []string {
if root == nil {
return nil
}
var paths []string // 用于存储所有路径
stack := []NodePath{{root, strconv.Itoa(root.Val)}} // 初始化栈,加入根节点及其路径
// 使用栈进行深度优先搜索
for len(stack) > 0 {
np := stack[len(stack)-1] // 取出栈顶元素
stack = stack[:len(stack)-1] // 弹出栈顶元素
node, path := np.node, np.path
// 检查是否为叶子节点
if node.Left == nil && node.Right == nil {
paths = append(paths, path) // 是叶子节点,添加路径
}
// 处理右子节点
if node.Right != nil {
newPath := path + "->" + strconv.Itoa(node.Right.Val)
stack = append(stack, NodePath{node.Right, newPath})
}
// 处理左子节点
if node.Left != nil {
newPath := path + "->" + strconv.Itoa(node.Left.Val)
stack = append(stack, NodePath{node.Left, newPath})
}
}
return paths
}
404.左叶子之和
题目
给定二叉树的根节点 root ,返回所有左叶子之和。
示例 1:
输入: root = [3,9,20,null,null,15,7]
输出: 24
解释: 在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24
示例 2:
输入: root = [1]
输出: 0
提示:
- 节点数在
[1, 1000]范围内 -1000 <= Node.val <= 1000
思路
递归方法
确定递归函数的参数和返回值
定义一个递归函数,接收当前节点作为参数。
确定终止条件
如果当前节点为空,返回0。
确定单层递归的逻辑
对于每个非空节点,检查其左子节点是否是叶子节点(即左子节点存在且左子节点的左右子节点均为空)。如果是,将左子节点的值累加到总和中。接着递归地处理当前节点的左右子节点。
迭代方法
要以非递归方式解决这个问题,可以采用栈来进行深度优先搜索(DFS):
- 使用栈:创建一个栈来存储节点及其父节点信息。
- 初始化:将根节点及其父节点信息(设为null)压入栈中。
- 迭代处理:当栈不为空时,弹出一个节点及其父节点信息。
- 检查左叶子节点:如果当前节点是叶子节点且是其父节点的左子节点,则累加其值。
- 处理子节点:如果节点有子节点,将子节点及当前节点(作为子节点的父节点)压入栈中。
- 返回结果:遍历完成后返回累加的左叶子节点之和。
代码实现
递归方法
func sumOfLeftLeaves(root *TreeNode) int {
if root == nil {
return 0
}
sum := 0
if root.Left != nil && root.Left.Left == nil && root.Left.Right == nil {
sum += root.Left.Val // 如果左子节点是叶子节点,则累加其值
}
sum += sumOfLeftLeaves(root.Left) // 递归左子树
sum += sumOfLeftLeaves(root.Right) // 递归右子树
return sum
}
迭代方法
type NodeParent struct {
node *TreeNode
parent *TreeNode
}
func sumOfLeftLeaves(root *TreeNode) int {
if root == nil {
return 0
}
sum := 0
stack := []NodeParent{{node: root, parent: nil}}
for len(stack) > 0 {
np := stack[len(stack)-1]
stack = stack[:len(stack)-1]
node, parent := np.node, np.parent
if node.Left == nil && node.Right == nil && parent != nil && parent.Left == node {
sum += node.Val // 当前节点是左叶子节点,累加其值
}
// 将子节点压入栈中
if node.Right != nil {
stack = append(stack, NodeParent{node: node.Right, parent: node})
}
if node.Left != nil {
stack = append(stack, NodeParent{node: node.Left, parent: node})
}
}
return sum
}