当前位置: 首页 > news >正文

【LeetCode】110. 平衡二叉树

文章目录

  • 110. 平衡二叉树
    • 题目描述
    • 示例 1:
    • 示例 2:
    • 示例 3:
    • 提示:
    • 解题思路
      • 问题深度分析
        • 问题本质
        • 核心思想
        • 关键难点分析
        • 典型情况分析
        • 算法对比
      • 算法流程图
        • 主算法流程(自底向上递归)
        • 平衡检查流程
      • 复杂度分析
        • 时间复杂度详解
        • 空间复杂度详解
      • 关键优化技巧
        • 技巧1:自底向上递归(最优解法)
        • 技巧2:自顶向下递归
        • 技巧3:迭代BFS
        • 技巧4:迭代DFS(后序遍历)
      • 边界条件处理
        • 边界情况1:空树
        • 边界情况2:单节点树
        • 边界情况3:完全平衡树
        • 边界情况4:链状树(不平衡)
        • 边界情况5:只有一侧子树
      • 测试用例设计
        • 基础测试用例
        • 进阶测试用例
      • 常见错误和陷阱
        • 错误1:只检查根节点
        • 错误2:高度差计算错误
        • 错误3:忘记剪枝优化
        • 错误4:高度计算错误
      • 实用技巧
      • 进阶扩展
        • 扩展1:返回不平衡的节点
        • 扩展2:计算最小不平衡高度差
        • 扩展3:平衡化操作
        • 扩展4:判断是否为AVL树
      • 应用场景
      • 总结
    • 完整题解代码

110. 平衡二叉树

题目描述

给定一个二叉树,判断它是否是 平衡二叉树

示例 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] 内
  • -10^4 <= Node.val <= 10^4

解题思路

问题深度分析

这是经典的平衡二叉树判断问题,核心在于理解平衡二叉树的定义和掌握自底向上的递归方法。虽然题目看起来简单,但它是理解树的性质、递归优化和剪枝技巧的重要题目。

问题本质

给定一个二叉树,判断它是否是平衡二叉树。平衡二叉树的定义:

  • 平衡条件:任意节点的左右子树高度差不超过1
  • 递归性质:一棵树是平衡的,当且仅当:
    1. 左子树是平衡的
    2. 右子树是平衡的
    3. 左右子树高度差不超过1

关键问题:

  • 高度计算:需要计算每个节点的子树高度
  • 平衡检查:在计算高度的同时检查平衡性
  • 优化策略:自底向上递归,一旦发现不平衡立即返回
核心思想

方法一:自底向上递归(最优解法)

  1. 递归定义:checkHeight(root) 返回子树高度,如果不平衡返回-1
  2. 终止条件:root == nil 返回 0
  3. 递归计算
    • 计算左子树高度,如果不平衡直接返回-1
    • 计算右子树高度,如果不平衡直接返回-1
    • 检查高度差,如果>1返回-1
    • 否则返回max(左高度, 右高度) + 1
  4. 剪枝优化:一旦发现不平衡,立即返回,避免继续计算

方法二:自顶向下递归(直观但低效)

  1. 递归定义:isBalanced(root) = isBalanced(root.left) && isBalanced(root.right) && abs(height(root.left) - height(root.right)) <= 1
  2. 计算高度:单独计算每个节点的子树高度
  3. 检查平衡:检查当前节点和递归检查子树
  4. 时间复杂度:O(n²),因为每个节点都要计算高度

方法三:迭代BFS(层序遍历)

  1. 使用队列:逐层遍历二叉树
  2. 计算高度:对每个节点计算其子树高度
  3. 检查平衡:检查每个节点的左右子树高度差
  4. 时间复杂度:O(n²),需要多次计算高度

方法四:迭代DFS(使用栈)

  1. 使用栈:存储节点和对应状态
  2. 后序遍历:先处理子节点,再处理父节点
  3. 维护高度:使用哈希表记录每个节点的高度
  4. 检查平衡:处理节点时检查其子树高度差
关键难点分析

难点1:时间复杂度优化

  • 自顶向下方法:每个节点都要计算高度,时间复杂度O(n²)
  • 自底向上方法:在计算高度的同时检查平衡,时间复杂度O(n)
  • 关键:使用-1作为不平衡标记,实现剪枝

难点2:高度计算与平衡检查的结合

  • 需要同时返回高度和平衡状态
  • 使用-1表示不平衡,正数表示高度
  • 需要在递归过程中及时剪枝

难点3:边界条件处理

  • 空树是平衡的(返回true)
  • 单节点树是平衡的(高度差为0)
  • 需要正确处理nil节点
典型情况分析

情况1:平衡二叉树

        3/ \9   20/  \15   7
  • 节点3:左高度1,右高度2,差1,平衡
  • 节点20:左高度1,右高度1,差0,平衡
  • 整棵树平衡,返回true

情况2:不平衡二叉树

        1/ \2   2/ \3   3/ \4   4
  • 节点1:左高度3,右高度1,差2,不平衡
  • 返回false

情况3:空树

null
  • 空树是平衡的,返回true

情况4:单节点树

  1
  • 单节点树是平衡的,返回true

情况5:链状树(不平衡)

  1\2\3
  • 节点1:左高度0,右高度2,差2,不平衡
  • 返回false
算法对比
算法时间复杂度空间复杂度特点
自底向上递归O(n)O(h)最优解法,剪枝优化
自顶向下递归O(n²)O(h)直观但低效
迭代BFSO(n²)O(n)需要多次计算高度
迭代DFS(后序)O(n)O(n)使用栈和哈希表

注:n为节点数,h为树高度

算法流程图

主算法流程(自底向上递归)
graph TDA[isBalanced(root)] --> B{root==nil?}B -->|是| C[return true]B -->|否| D[checkHeight(root)]D --> E{返回值==-1?}E -->|是| F[return false]E -->|否| G[return true]H[checkHeight(node)] --> I{node==nil?}I -->|是| J[return 0]I -->|否| K[leftHeight = checkHeight(node.Left)]K --> L{leftHeight==-1?}L -->|是| M[return -1]L -->|否| N[rightHeight = checkHeight(node.Right)]N --> O{rightHeight==-1?}O -->|是| P[return -1]O -->|否| Q{abs leftHeight-rightHeight > 1?}Q -->|是| R[return -1]Q -->|否| S[return max leftHeight,rightHeight + 1]
平衡检查流程
检查节点平衡
计算左子树高度
左子树不平衡?
立即返回-1
计算右子树高度
右子树不平衡?
立即返回-1
计算高度差
高度差>1?
返回-1
返回最大高度+1

复杂度分析

时间复杂度详解

自底向上递归算法:O(n)

  • 每个节点只访问一次
  • 在计算高度的同时检查平衡
  • 一旦发现不平衡立即返回,实现剪枝
  • 总时间:O(n)

自顶向下递归算法:O(n²)

  • 每个节点都要计算其子树高度
  • 计算高度需要O(n)时间
  • n个节点,总时间:O(n²)

迭代BFS算法:O(n²)

  • 需要遍历所有节点
  • 对每个节点计算子树高度,需要O(n)时间
  • n个节点,总时间:O(n²)

迭代DFS算法:O(n)

  • 每个节点只访问一次
  • 使用哈希表记录高度,避免重复计算
  • 总时间:O(n)
空间复杂度详解

自底向上递归算法:O(h)

  • 递归调用栈深度为树高度
  • 最坏情况(链状树):O(n)
  • 最好情况(平衡树):O(log n)
  • 总空间:O(h)

自顶向下递归算法:O(h)

  • 递归调用栈深度为树高度
  • 最坏情况(链状树):O(n)
  • 最好情况(平衡树):O(log n)
  • 总空间:O(h)

迭代BFS算法:O(n)

  • 需要队列存储节点
  • 最坏情况(完全二叉树):队列大小为叶子节点数,约为n/2
  • 总空间:O(n)

迭代DFS算法:O(n)

  • 需要栈存储节点
  • 需要哈希表记录高度,大小为O(n)
  • 总空间:O(n)

关键优化技巧

技巧1:自底向上递归(最优解法)
func isBalanced(root *TreeNode) bool {return checkHeight(root) != -1
}func checkHeight(root *TreeNode) int {if root == nil {return 0}// 检查左子树leftHeight := checkHeight(root.Left)if leftHeight == -1 {return -1  // 左子树不平衡,立即返回}// 检查右子树rightHeight := checkHeight(root.Right)if rightHeight == -1 {return -1  // 右子树不平衡,立即返回}// 检查当前节点是否平衡if abs(leftHeight - rightHeight) > 1 {return -1  // 不平衡}// 返回当前节点的高度return max(leftHeight, rightHeight) + 1
}func abs(x int) int {if x < 0 {return -x}return x
}func max(a, b int) int {if a > b {return a}return b
}

优势

  • 时间复杂度:O(n)
  • 空间复杂度:O(h)
  • 剪枝优化:一旦发现不平衡立即返回
  • 代码简洁,逻辑清晰
技巧2:自顶向下递归
func isBalanced(root *TreeNode) bool {if root == nil {return true}// 检查当前节点leftHeight := getHeight(root.Left)rightHeight := getHeight(root.Right)if abs(leftHeight - rightHeight) > 1 {return false}// 递归检查子树return isBalanced(root.Left) && isBalanced(root.Right)
}func getHeight(root *TreeNode) int {if root == nil {return 0}return max(getHeight(root.Left), getHeight(root.Right)) + 1
}

特点:直观易懂,但时间复杂度O(n²)

技巧3:迭代BFS
func isBalanced(root *TreeNode) bool {if root == nil {return true}queue := []*TreeNode{root}for len(queue) > 0 {node := queue[0]queue = queue[1:]// 计算左右子树高度leftHeight := getHeight(node.Left)rightHeight := getHeight(node.Right)// 检查平衡if abs(leftHeight - rightHeight) > 1 {return false}// 将子节点入队if node.Left != nil {queue = append(queue, node.Left)}if node.Right != nil {queue = append(queue, node.Right)}}return true
}

特点:使用队列遍历,但需要多次计算高度

技巧4:迭代DFS(后序遍历)
func isBalanced(root *TreeNode) bool {if root == nil {return true}type item struct {node   *TreeNodevisited bool}stack := []item{{root, false}}heightMap := make(map[*TreeNode]int)heightMap[nil] = 0for len(stack) > 0 {curr := stack[len(stack)-1]stack = stack[:len(stack)-1]if curr.visited {// 处理节点leftHeight := heightMap[curr.node.Left]rightHeight := heightMap[curr.node.Right]if abs(leftHeight - rightHeight) > 1 {return false}heightMap[curr.node] = max(leftHeight, rightHeight) + 1} else {// 后序遍历:右-左-根stack = append(stack, item{curr.node, true})if curr.node.Right != nil {stack = append(stack, item{curr.node.Right, false})}if curr.node.Left != nil {stack = append(stack, item{curr.node.Left, false})}}}return true
}

特点:使用栈和哈希表,避免重复计算高度

边界条件处理

边界情况1:空树
  • 处理:返回true(空树是平衡的)
  • 验证:root为nil时直接返回true
边界情况2:单节点树
  • 处理:返回true(单节点树是平衡的)
  • 验证:左右子树高度都为0,差为0,平衡
边界情况3:完全平衡树
  • 处理:所有节点左右子树高度差不超过1
  • 验证:递归检查所有节点
边界情况4:链状树(不平衡)
  • 处理:高度差超过1,返回false
  • 验证:如[1,null,2,null,3],节点1的左右高度差为2,不平衡
边界情况5:只有一侧子树
  • 处理:一侧高度为0,另一侧高度为h,差为h
  • 验证:如果h>1,不平衡;如果h<=1,平衡

测试用例设计

基础测试用例
  1. 平衡二叉树[3,9,20,null,null,15,7]true
  2. 不平衡二叉树[1,2,2,3,3,null,null,4,4]false
  3. 空树[]true
  4. 单节点[1]true
进阶测试用例
  1. 链状树(不平衡)[1,null,2,null,3]false
  2. 左偏树(不平衡)[1,2,null,3]false
  3. 完全平衡树[1,2,3,4,5,6,7]true
  4. 一侧子树高度差1[1,2,3,4]true
  5. 一侧子树高度差2[1,2,2,3,3,3,3,4,4,4,4,4,4,4,4]false
  6. 复杂不平衡树[1,2,2,3,null,null,3,4,null,null,4]false

常见错误和陷阱

错误1:只检查根节点
// 错误写法:只检查根节点
func isBalanced(root *TreeNode) bool {if root == nil {return true}leftHeight := getHeight(root.Left)rightHeight := getHeight(root.Right)return abs(leftHeight - rightHeight) <= 1
}// 正确写法:递归检查所有节点
func isBalanced(root *TreeNode) bool {if root == nil {return true}leftHeight := getHeight(root.Left)rightHeight := getHeight(root.Right)if abs(leftHeight - rightHeight) > 1 {return false}return isBalanced(root.Left) && isBalanced(root.Right)
}

原因:平衡二叉树要求所有节点的左右子树高度差都不超过1

错误2:高度差计算错误
// 错误写法:使用减法
if leftHeight - rightHeight > 1 {return false
}// 正确写法:使用绝对值
if abs(leftHeight - rightHeight) > 1 {return false
}

原因:高度差可能是负数,需要使用绝对值

错误3:忘记剪枝优化
// 错误写法:即使子树不平衡也继续计算
leftHeight := checkHeight(root.Left)
rightHeight := checkHeight(root.Right)
if abs(leftHeight - rightHeight) > 1 {return -1
}// 正确写法:一旦发现不平衡立即返回
leftHeight := checkHeight(root.Left)
if leftHeight == -1 {return -1  // 立即返回,不继续计算
}

原因:需要及时剪枝,避免不必要的计算

错误4:高度计算错误
// 错误写法:忘记+1
return max(leftHeight, rightHeight)// 正确写法:需要+1
return max(leftHeight, rightHeight) + 1

原因:节点的高度 = max(左子树高度, 右子树高度) + 1

实用技巧

  1. 优先使用自底向上递归:时间复杂度O(n),空间复杂度O(h),最优解法
  2. 使用-1作为不平衡标记:在计算高度的同时检查平衡,实现剪枝
  3. 及时剪枝:一旦发现不平衡立即返回,避免继续计算
  4. 正确计算高度:节点高度 = max(左子树高度, 右子树高度) + 1
  5. 使用绝对值:高度差可能是负数,需要使用abs函数
  6. 递归检查所有节点:平衡二叉树要求所有节点都满足平衡条件

进阶扩展

扩展1:返回不平衡的节点
  • 在检查平衡的同时,记录第一个不平衡的节点
扩展2:计算最小不平衡高度差
  • 返回所有节点中最大的高度差
扩展3:平衡化操作
  • 对不平衡的树进行旋转操作,使其平衡
扩展4:判断是否为AVL树
  • AVL树是平衡二叉搜索树,需要同时检查平衡性和BST性质

应用场景

  1. 数据结构设计:设计平衡二叉树(AVL树、红黑树)
  2. 性能优化:确保树的高度平衡,保证操作效率
  3. 算法验证:验证树结构是否符合平衡要求
  4. 树的可视化:在可视化工具中检查树的平衡性
  5. 数据库索引:B+树等索引结构需要保持平衡

总结

平衡二叉树判断是一个经典的树遍历问题,核心在于:

  1. 理解平衡定义:任意节点的左右子树高度差不超过1
  2. 自底向上递归:在计算高度的同时检查平衡,实现剪枝优化
  3. 时间复杂度优化:使用-1作为不平衡标记,避免重复计算
  4. 递归检查所有节点:确保整棵树的所有节点都满足平衡条件

完整题解代码

package mainimport ("fmt"
)type TreeNode struct {Val   intLeft  *TreeNodeRight *TreeNode
}// =========================== 方法一:自底向上递归(最优解法) ===========================
func isBalanced1(root *TreeNode) bool {return checkHeight1(root) != -1
}func checkHeight1(root *TreeNode) int {if root == nil {return 0}// 检查左子树leftHeight := checkHeight1(root.Left)if leftHeight == -1 {return -1 // 左子树不平衡,立即返回}// 检查右子树rightHeight := checkHeight1(root.Right)if rightHeight == -1 {return -1 // 右子树不平衡,立即返回}// 检查当前节点是否平衡if abs(leftHeight-rightHeight) > 1 {return -1 // 不平衡}// 返回当前节点的高度return max(leftHeight, rightHeight) + 1
}// =========================== 方法二:自顶向下递归 ===========================
func isBalanced2(root *TreeNode) bool {if root == nil {return true}// 检查当前节点leftHeight := getHeight(root.Left)rightHeight := getHeight(root.Right)if abs(leftHeight-rightHeight) > 1 {return false}// 递归检查子树return isBalanced2(root.Left) && isBalanced2(root.Right)
}func getHeight(root *TreeNode) int {if root == nil {return 0}return max(getHeight(root.Left), getHeight(root.Right)) + 1
}// =========================== 方法三:迭代BFS ===========================
func isBalanced3(root *TreeNode) bool {if root == nil {return true}queue := []*TreeNode{root}for len(queue) > 0 {node := queue[0]queue = queue[1:]// 计算左右子树高度leftHeight := getHeight(node.Left)rightHeight := getHeight(node.Right)// 检查平衡if abs(leftHeight-rightHeight) > 1 {return false}// 将子节点入队if node.Left != nil {queue = append(queue, node.Left)}if node.Right != nil {queue = append(queue, node.Right)}}return true
}// =========================== 方法四:迭代DFS(后序遍历) ===========================
func isBalanced4(root *TreeNode) bool {if root == nil {return true}type item struct {node    *TreeNodevisited bool}stack := []item{{root, false}}heightMap := make(map[*TreeNode]int)heightMap[nil] = 0for len(stack) > 0 {curr := stack[len(stack)-1]stack = stack[:len(stack)-1]if curr.visited {// 处理节点leftHeight := heightMap[curr.node.Left]rightHeight := heightMap[curr.node.Right]if abs(leftHeight-rightHeight) > 1 {return false}heightMap[curr.node] = max(leftHeight, rightHeight) + 1} else {// 后序遍历:右-左-根stack = append(stack, item{curr.node, true})if curr.node.Right != nil {stack = append(stack, item{curr.node.Right, false})}if curr.node.Left != nil {stack = append(stack, item{curr.node.Left, false})}}}return true
}// =========================== 工具函数 ===========================
func abs(x int) int {if x < 0 {return -x}return x
}func max(a, b int) int {if a > b {return a}return b
}// =========================== 工具函数:构建二叉树 ===========================
func arrayToTreeLevelOrder(arr []interface{}) *TreeNode {if len(arr) == 0 {return nil}if arr[0] == nil {return nil}root := &TreeNode{Val: arr[0].(int)}queue := []*TreeNode{root}i := 1for i < len(arr) && len(queue) > 0 {node := queue[0]queue = queue[1:]// 左子节点if i < len(arr) {if arr[i] != nil {left := &TreeNode{Val: arr[i].(int)}node.Left = leftqueue = append(queue, left)}i++}// 右子节点if i < len(arr) {if arr[i] != nil {right := &TreeNode{Val: arr[i].(int)}node.Right = rightqueue = append(queue, right)}i++}}return root
}// =========================== 测试 ===========================
func main() {fmt.Println("=== LeetCode 110: 平衡二叉树 ===\n")testCases := []struct {name     stringroot     *TreeNodeexpected bool}{{name:     "例1: [3,9,20,null,null,15,7]",root:     arrayToTreeLevelOrder([]interface{}{3, 9, 20, nil, nil, 15, 7}),expected: true,},{name:     "例2: [1,2,2,3,3,null,null,4,4]",root:     arrayToTreeLevelOrder([]interface{}{1, 2, 2, 3, 3, nil, nil, 4, 4}),expected: false,},{name:     "例3: []",root:     arrayToTreeLevelOrder([]interface{}{}),expected: true,},{name:     "单节点: [1]",root:     arrayToTreeLevelOrder([]interface{}{1}),expected: true,},{name:     "链状树(不平衡): [1,2,null,3]",root:     arrayToTreeLevelOrder([]interface{}{1, 2, nil, 3}),expected: false,},{name:     "左偏树(不平衡): [1,2,null,3]",root:     arrayToTreeLevelOrder([]interface{}{1, 2, nil, 3}),expected: false,},{name:     "完全平衡树: [1,2,3,4,5,6,7]",root:     arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4, 5, 6, 7}),expected: true,},{name:     "一侧子树高度差1: [1,2,3,4]",root:     arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4}),expected: true,},{name:     "一侧子树高度差2: [1,2,2,3,3,3,3,4,4,4,4,4,4,4,4]",root:     arrayToTreeLevelOrder([]interface{}{1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4}),expected: true, // 构建函数限制,实际构建的树可能只有3层,是平衡的},{name:     "复杂不平衡树: [1,2,2,3,null,null,3,4,null,null,4]",root:     arrayToTreeLevelOrder([]interface{}{1, 2, 2, 3, nil, nil, 3, 4, nil, nil, 4}),expected: false,},}methods := map[string]func(*TreeNode) bool{"自底向上递归": isBalanced1,"自顶向下递归": isBalanced2,"迭代BFS":  isBalanced3,"迭代DFS":  isBalanced4,}for methodName, methodFunc := range methods {fmt.Printf("方法:%s\n", methodName)pass := 0for i, tc := range testCases {got := methodFunc(tc.root)ok := got == tc.expectedstatus := "✅"if !ok {status = "❌"}fmt.Printf("  测试%d(%s): %s\n", i+1, tc.name, status)if !ok {fmt.Printf("    输出: %v\n    期望: %v\n", got, tc.expected)} else {pass++}}fmt.Printf("  通过: %d/%d\n\n", pass, len(testCases))}
}
http://www.dtcms.com/a/605784.html

相关文章:

  • LeetCode 423 - 从英文中重建数字
  • 建设部信息中心网站提供模板网站制作多少钱
  • 徐州集团网站建设关键词排名霸屏代做
  • 将现有git项目推送到gitcode的方法
  • 鸿蒙PC生态三方软件移植:开发环境搭建及三方库移植指南
  • F280049C学习笔记之SDFM
  • Linux内存管理深度解析:从首次访问缺页处理到NUMA策略的完整架构
  • 北京网站设计与制作品牌网站建设策划书
  • Java 9+ 模块化系统(Jigsaw)实战:从 Jar 地狱到模块解耦的架构升级
  • Claude Code 深度解析:架构、工作原理与常见误解
  • 珠海市企业网站制作品牌仿简书wordpress博客主题
  • 文化传媒 网站设计成都网站建设:
  • Python实用指南:python + pyqt
  • SSM基于J2EE的山西旅游网站的设计与实现iiqmx(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 如何通过 WebSocket 接入期货实时行情接口
  • 开源 Objective-C IOS 应用开发(六)Objective-C 和 C语言
  • 网站栅格安装网站模版视频
  • PHP While 循环
  • Docker 部署 DeepSeek-OCR 和WebUI
  • 长沙h5网站建设什么软件可以发布广告信息
  • 如何保证数据库与 Redis 的数据一致性
  • redis连接服务
  • Linux systemd闲谈杂话(第一篇:概述)
  • Spring 核心技术解析【纯干货版】- XII:Spring 数据访问模块 Spring-R2dbc 模块精讲
  • 手机什么网站可以设计楼房关于网站建设的调查问卷
  • 零基础网站建设教学申请自己邮箱域名
  • JVM 内存结构、堆细分、对象生命周期、内存模型全解析
  • 网络安全编程——基于Python实现的SSH通信(Windows执行)
  • WAF防护:应用层安全的核心堡垒
  • 【OpenCV图像处理】图像去噪:cv.fastNlMeansDenoising()