0Pricing
Coding Interview Prep · 课时

直径、高度与平衡树

使用一次 DFS 遍历和同时返回两个值的辅助函数计算树的直径与高度,然后检查树是否高度平衡。

直径、高度与平衡树 是 CoddyKit 上的免费 Coding Interview Prep 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Coding Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Coding Interview Prep 课程共包含 4 节课。

二叉树的高度

二叉树的高度(或最大深度)是从根节点到任意叶节点的最长路径长度。它可以通过递归计算:任意节点的高度为 1 + max(height(left), height(right)),空节点的基本情况为 0。这种后序计算非常基础——高度是计算直径、检查平衡性以及执行 AVL 树旋转的基础。

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def height(root):
    if not root:
        return 0
    return 1 + max(height(root.left), height(root.right))

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
root.left.left.left = TreeNode(6)
print(height(root))  # 4

直径:最长路径

二叉树的直径是任意两个节点之间的最长路径长度(这条路径不一定经过根节点)。路径长度以边为单位。对于任意节点,经过该节点的直径等于 height(left) + height(right)。整棵树的直径就是树中所有节点对应值的最大值。

def diameter_of_binary_tree(root):
    max_diameter = [0]  # use list to allow closure mutation

    def dfs(node):
        if not node:
            return 0
        left_h = dfs(node.left)
        right_h = dfs(node.right)
        # Diameter through this node
        max_diameter[0] = max(max_diameter[0], left_h + right_h)
        return 1 + max(left_h, right_h)  # height for parent

    dfs(root)
    return max_diameter[0]

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print(diameter_of_binary_tree(root))  # 3

只需一次 DFS 遍历计算直径

朴素方法会在每个节点调用 height(),对于平衡树来说时间复杂度为 O(n²)。最优解法在一次 DFS 遍历中同时计算高度并更新直径。关键在于,递归函数 dfs() 同时承担两个作用:向父节点返回高度,同时通过副作用更新全局最大直径。这种双重用途的后序模式会出现在许多树问题中。

# O(n^2) NAIVE: recomputes height for every node
def diameter_naive(root):
    if not root:
        return 0
    through_root = height(root.left) + height(root.right)
    in_left = diameter_naive(root.left)
    in_right = diameter_naive(root.right)
    return max(through_root, in_left, in_right)

# O(n) OPTIMAL: single DFS pass (shown in previous scene)
# The naive version is O(n^2) because height() is O(n)
# and it is called for every node.
print('Naive: O(n^2) | Optimal single-pass: O(n)')

平衡二叉树检查

如果每个节点的左右子树高度之差最多为一,则该二叉树是高度平衡的。蛮力方法会在每个节点调用 height(),时间复杂度为 O(n²)。最优方法使用相同的一次遍历技巧:使用 -1 作为“不平衡”的哨兵值并向上传递,一旦发现某个节点不平衡就提前停止。

def is_balanced(root):
    def check(node):
        if not node:
            return 0
        left = check(node.left)
        if left == -1:
            return -1  # propagate early exit
        right = check(node.right)
        if right == -1:
            return -1
        if abs(left - right) > 1:
            return -1  # unbalanced here
        return 1 + max(left, right)  # height if balanced

    return check(root) != -1

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.left.left = TreeNode(5)  # too deep on left
print(is_balanced(root))  # False

哨兵返回值模式

返回一个哨兵值(不平衡时返回 -1,或返回一个特殊元组)是一种常见模式,适用于 DFS 辅助函数需要传递两类信息的情况:计算结果,以及是否违反了约束。不要抛出异常或使用全局标志,而是将错误编码在返回类型中。这种方法简洁、避免了全局状态,并且能自然地与其他递归辅助函数组合。

# General pattern: return (is_valid, computed_value)
def balanced_height(node):
    if not node:
        return True, 0
    left_ok, left_h = balanced_height(node.left)
    if not left_ok:
        return False, 0  # short-circuit
    right_ok, right_h = balanced_height(node.right)
    if not right_ok:
        return False, 0
    balanced = abs(left_h - right_h) <= 1
    return balanced, 1 + max(left_h, right_h)

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
ok, h = balanced_height(root)
print(ok, h)  # True 2

以节点数还是边数表示直径

请注意题目描述:LeetCode #543 以边为单位计算直径,而有些问题以节点为单位计算。如果需要计算节点数,经过某个节点的直径为 height(left) + height(right) + 1(为节点自身加 1)。如果需要计算边数,则省略 +1。开始编码前,请务必先与面试官确认这一点。

def diameter_in_nodes(root):
    max_path = [0]

    def dfs(node):
        if not node:
            return 0
        left_h = dfs(node.left)
        right_h = dfs(node.right)
        # Path through this node in NODE count
        nodes_through = left_h + right_h + 1
        max_path[0] = max(max_path[0], nodes_through)
        return 1 + max(left_h, right_h)

    dfs(root)
    return max_path[0]

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print(diameter_in_nodes(root))  # 4 nodes: 4-2-1-3 or 5-2-1-3

路径和:任意根节点到叶节点的路径

路径和问题要求判断:是否存在一条从根节点到叶节点的路径,其路径和等于目标值?使用 DFS,并在向下遍历时从目标值中减去当前节点的值。在叶节点处,检查剩余目标值是否等于该叶节点的值。这是一种传递剩余和作为参数的前序 DFS,是自顶向下递归的经典例子。

def has_path_sum(root, target):
    if not root:
        return False
    # Leaf node: check if we've exactly hit the target
    if not root.left and not root.right:
        return root.val == target
    remaining = target - root.val
    return (has_path_sum(root.left, remaining) or
            has_path_sum(root.right, remaining))

root = TreeNode(5)
root.left = TreeNode(4)
root.right = TreeNode(8)
root.left.left = TreeNode(11)
root.left.left.left = TreeNode(7)
root.left.left.right = TreeNode(2)
print(has_path_sum(root, 22))  # True: 5+4+11+2=22

最大路径和(困难变体)

最大路径和(LeetCode #124)要难得多:路径可以从任意节点开始并在任意节点结束,而不只是从根节点到叶节点,并且节点值可能为负数。在每个节点处,需要考虑四种情况:只有该节点、节点加左分支、节点加右分支,或节点加左右两个分支。只有前三种情况可以继续向上延伸到父节点;第四种情况是全局最大值的终止候选值。

def max_path_sum(root):
    max_sum = [float('-inf')]

    def gain(node):
        if not node:
            return 0
        # Only take positive contributions
        left = max(gain(node.left), 0)
        right = max(gain(node.right), 0)
        # Best path through this node (can't go both ways upward)
        max_sum[0] = max(max_sum[0], node.val + left + right)
        # Return the best single-branch gain for parent
        return node.val + max(left, right)

    gain(root)
    return max_sum[0]

root = TreeNode(-10)
root.left = TreeNode(9)
root.right = TreeNode(20)
root.right.left = TreeNode(15)
root.right.right = TreeNode(7)
print(max_path_sum(root))  # 42: 15+20+7

AVL 树与自平衡

AVL 树是一种通过在插入和删除操作后执行旋转来保持高度平衡性质的 BST。每个节点都存储一个平衡因子(右子树高度减去左子树高度),其值必须保持在 {-1, 0, 1} 之内。发生违反平衡条件的情况时,单旋转或双旋转可以在 O(1) 时间内恢复平衡,使整体高度保持为 O(log n),并保证所有操作的复杂度为 O(log n)。

# Balance factor = height(right) - height(left)
# AVL invariant: balance factor in {-1, 0, 1} for every node

# Four violation types and their fixes:
# LL (left-heavy left child): single right rotation
# RR (right-heavy right child): single left rotation
# LR (right-heavy left child): left rotate child, then right rotate root
# RL (left-heavy right child): right rotate child, then left rotate root

# Knowing this is enough for interviews; you rarely implement
# full AVL in an interview but must discuss the concept.
print('AVL maintains O(log n) height via rotations')

对称树检查

如果一棵二叉树是自身的镜像,则它是对称的。可以递归检查:对于对称轴两侧的每一对对应节点,如果它们的值相等且子树互为镜像,则该树是对称的。定义辅助函数 is_mirror(left, right),检查以下情况:两者都为空(可以),一个为空(不可以),值相等且内侧与外侧子树互为镜像。

def is_symmetric(root):
    def is_mirror(left, right):
        if not left and not right:
            return True
        if not left or not right:
            return False
        return (left.val == right.val and
                is_mirror(left.left, right.right) and
                is_mirror(left.right, right.left))

    return is_mirror(root.left, root.right)

sym = TreeNode(1)
sym.left = TreeNode(2)
sym.right = TreeNode(2)
sym.left.left = TreeNode(3)
sym.right.right = TreeNode(3)
print(is_symmetric(sym))  # True

nosym = TreeNode(1)
nosym.left = TreeNode(2)
nosym.right = TreeNode(2)
nosym.left.right = TreeNode(3)
print(is_symmetric(nosym))  # False

结合高度与直径的要点

单次遍历的后序模式可以复用于许多问题:直径、最大路径和、检查平衡性、统计优质节点等。在解决问题时,请始终思考:“父节点需要从每个子节点获得什么信息?”这就是返回值。“当前节点的局部计算是什么?”它会更新全局答案。这种拆解是解决复杂树问题的关键技能。

# Reusable template for post-order dual-purpose DFS:
def tree_problem(root):
    result = [float('-inf')]  # or 0 depending on problem

    def dfs(node):
        if not node:
            return 0  # base return (height, count, etc.)
        left_val = dfs(node.left)
        right_val = dfs(node.right)
        # --- Update global result using both children ---
        candidate = left_val + right_val  # example: diameter
        result[0] = max(result[0], candidate)
        # --- Return info needed by PARENT ---
        return 1 + max(left_val, right_val)  # example: height

    dfs(root)
    return result[0]

root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
print(tree_problem(root))  # diameter = 2

快速检查

请测试您对本课数据结构与算法——编程面试准备相关概念的理解。

课程回顾

在本课中,您学习了:使用递归后序 DFS 计算高度,使用具有双重用途的 DFS 辅助函数在单次 O(n) 遍历中计算直径,以及使用提前退出标记进行平衡性检查。接下来我们将学习路径总和问题和最低公共祖先。

常见问题解答

「直径、高度与平衡树」课时是免费的吗?

是的 — 「直径、高度与平衡树」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Coding Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 Coding Interview Prep 课程共包含 4 节课。

「直径、高度与平衡树」这节课中我会学到什么?

使用一次 DFS 遍历和同时返回两个值的辅助函数计算树的直径与高度,然后检查树是否高度平衡。 你通过在浏览器中直接运行的动手代码来练习 Coding Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Coding Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Coding Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「直径、高度与平衡树」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Coding Interview Prep 课中编写并运行代码吗?

能。每节 Coding Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. TreeNode 类与层序 BFS
  2. 中序、前序、后序 DFS
  3. 直径、高度与平衡树
  4. 路径和与最近公共祖先
← 返回 Coding Interview Prep