アルゴリズム概要

💡 この問題を一言で言うと:「木のすべてのノードで、左右の枝の深さの差が1以内かどうかを判定する問題」

高さ均衡二分木(height-balanced binary tree)とは、全ノードで左右の部分木の高さの差が最大1であるような二分木です。 空の木(root = null)も高さ均衡として扱います。

⚠️ なぜ単純な方法では解けないのか

  • 「高さ計算」と「均衡チェック」を別々に行うと、同じノードを何度も訪問するためO(n²)になってしまいます(素朴なトップダウン再帰の罠)。
  • 不均衡が見つかった瞬間に結果を上位ノードへ伝える仕組みがないと、無駄な計算が増えてしまいます。
O(n)
時間計算量
O(h)
空間計算量
Bottom-up DFS
手法
番兵値 -1
エラー伝播
Example 1 — true
    3
   / \
  9  20
     / \
    15   7

全ノードで左右の高さの差 ≤ 1 → true

Example 2 — false
      1
    /   \
   2     2
  / \
 3   3
/ \
4   4

ルートの左右の高さ差 = 2 → false

Example 3 — true
(空の木)
root = null

空の木は定義上 均衡 → true

🧠 解法のアイデア:1パスで高さ計算と均衡チェックを同時に行う

葉ノード(子のないノード)から根ノードに向かってさかのぼりながら(ボトムアップ)、高さを返しつつ同時に均衡チェックも行います。 不均衡が見つかったら -1(番兵値)を返し、上位ノードへ伝播させます。 これにより各ノードをちょうど1回だけ訪問する O(n) が実現できます。

ステップバイステップ解説

各ステップをクリックするか ▶ Play で自動再生できます。

Python 実装

📋 このコードの構造(先に全体像を把握しよう)

  1. isBalanced:外部に公開するエントリポイント。check_height を呼んで -1 でなければ True を返す
  2. check_height(ネスト関数):再帰の本体。高さを返しつつ不均衡なら -1 を返す
  3. ベースケース:node が None なら高さ 0 を返す
  4. 左右を再帰し、-1 が返ってきたら即 -1 を伝播(早期リターン)
  5. 左右の高さの差 > 1 なら -1、そうでなければ max(左, 右) + 1 を返す
from typing import Optional

class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:

        def check_height(node: Optional[TreeNode]) -> int:
            # ベースケース:空のノードは高さ0
            # `is None` はPEP 8推奨。None はシングルトンなので is が正確
            if node is None:
                return 0

            # 左サブツリーの高さを再帰で取得
            left_height = check_height(node.left)
            # 左が -1(不均衡検知済み)なら即座に -1 を返す(早期リターン)
            if left_height == -1:
                return -1

            # 右サブツリーの高さを再帰で取得
            right_height = check_height(node.right)
            # 右が -1 のときも同様に伝播
            if right_height == -1:
                return -1

            # このノードでの均衡チェック
            # abs() はC実装の組み込み関数で高速
            if abs(left_height - right_height) > 1:
                return -1  # 番兵値 -1 を返して不均衡を上位へ知らせる

            # このノードの高さ = max(左, 右) + 自分の1
            # max() もC実装の組み込み関数で高速
            return max(left_height, right_height) + 1

        # check_height が -1 でなければ均衡している
        return check_height(root) != -1

▶ 入力例 [3,9,20,null,null,15,7] での動作トレース

check_height(3)  開始
  ├─ check_height(9)   → left=0, right=0, diff=0 ≤ 1  → return 1
  │   left_height=1 (≠-1、継続)
  ├─ check_height(20)  開始
  │   ├─ check_height(15) → return 1
  │   │   left_height=1 (≠-1、継続)
  │   ├─ check_height(7)  → return 1
  │   │   right_height=1 (≠-1、継続)
  │   └─ abs(1-1)=0 ≤ 1 → return max(1,1)+1 = 2
  │   right_height=2 (≠-1、継続)
  └─ abs(1-2)=1 ≤ 1 → return max(1,2)+1 = 3

check_height(root) = 3
3 != -1  →  isBalanced = True ✅

▶ 不均衡ケース [1,2,2,3,3,null,null,4,4] での動作トレース

check_height(4) → 1  (左の4)
check_height(4) → 1  (右の4)
check_height(3) [左]  → abs(1-1)=0 → return 2
check_height(3) [右]  → abs(0-0)=0 → return 1
check_height(2) [左]  → abs(2-1)=1 ≤ 1 → return 3
check_height(2) [右]  → return 1
check_height(1) [ルート]
  left_height=3, right_height=1
  abs(3-1) = 2  > 1  → return -1  ← 不均衡!

check_height(root) = -1
-1 == -1  →  isBalanced = False ✅

処理フローチャート

🗺️ フローチャートの読み方

楕円(緑)= 開始・終了
四角(青)= 処理ステップ
ひし形(黄)= 条件分岐
緑=はい 赤=いいえ
isBalanced(root) 開始 check_height(root) を呼ぶ check_height(node) を呼ぶ node = 現在処理中のノード node is None ? (木の末端に到達したか) はい return 0 いいえ left_height = check_height(node.left) 左の部分木の高さを再帰で計算 left_height == -1 ? (左サブツリーで不均衡を検知済みか) はい return -1 いいえ right_height = check_height(node.right) 右の部分木の高さを再帰で計算 right_height == -1 ? (右サブツリーで不均衡を検知済みか) はい return -1 いいえ abs(left_height - right_height) > 1 ? (このノードで左右の高さの差が大きすぎるか) はい return -1 いいえ return max(left_height, right_height) + 1 このノードの高さ(均衡OK)を親へ返す check_height(root) != -1 を返す True(均衡)または False(不均衡)

🔎 入力例 [3,9,20,null,null,15,7] でのフロー追跡

  1. 「開始」→ check_height(node=3) を呼ぶ。node は None でないので処理継続
  2. 左サブツリー check_height(9) を計算 → 9の左右はどちらも None なので高さ1を返す。-1 でないので継続
  3. 右サブツリー check_height(20) を計算 → 15と7の高さがそれぞれ1 → abs(1-1)=0 ≤ 1 → 高さ2を返す。-1 でないので継続
  4. ルート3での均衡チェック:abs(1-2)=1 ≤ 1 → 均衡OK → 高さ3を返す
  5. 「終了」→ 3 != -1 → isBalanced = True

計算量分析

📖 Big-O 記法の読み方(n = ノード数が大きくなるにつれて処理時間がどう増えるかの目安)

O(1)
常に一定
例:辞書の直接引き
O(n)
入力に比例
例:リストを1回走査
O(log n)
log倍に増加
例:二分探索
O(n²)
入力の2乗
例:二重ループ総当たり
アプローチ 時間計算量 空間計算量 備考
✅ ボトムアップDFS(番兵値-1) O(n) O(h) 各ノードを1回だけ訪問。h=木の高さ(均衡木ではO(log n)、最悪O(n))
❌ トップダウン再帰(素朴) O(n²) O(h) 高さ計算と均衡チェックを分離するため同じノードを繰り返し訪問してしまう
BFS(幅優先探索) O(n) O(n) dequeのメモリ確保コストがある。実装も複雑になりやすい

🔍 なぜこの計算量になるのか

時間計算量 O(n):check_height はすべてのノードをちょうど1回だけ訪問します。不均衡が見つかった瞬間に -1 を返す早期リターンにより、無駄な再帰呼び出しを省けています。
空間計算量 O(h):再帰呼び出しのコールスタックが木の高さ h 分だけ積み重なります。均衡木では h = O(log n)、最悪の一直線の木では h = O(n) になります。

📖 用語集

このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。

高さ均衡二分木(Height-balanced Binary Tree)
すべてのノードで、左右の部分木の高さの差が最大1である二分木のこと。AVL木がこの代表例です。均衡が保たれていると、検索・挿入・削除などの操作をO(log n)で行えます。
番兵値(Sentinel Value)
通常の値としてあり得ない特別な値を使ってエラーや特殊状態を表す手法です。この問題では -1 が番兵値で「不均衡が検知済み」を意味します。高さは常に0以上なので、-1 は安全な番兵値として機能します。
ボトムアップ再帰(Bottom-up Recursion)
葉ノード(末端)から根ノードに向かって結果を積み上げていく再帰の方向です。対義語はトップダウン(根から葉へ)。ボトムアップにすると、計算結果を再利用できるためO(n)が実現できます。
ネスト関数(Nested Function)
関数の中に定義された関数のこと。外部スコープから直接呼べないため、内部実装を隠蔽できます。Pythonではネスト関数はローカルスコープで名前解決されるため、クラスメソッドより少し高速です。
DFS(深さ優先探索 / Depth-First Search)
木やグラフを探索する方法の一つで、できるだけ深く進んでから戻る方式です。迷路を解くとき「行き止まりになるまで進んで、戻って別の道を試す」イメージです。木の問題では再帰で自然に実装できます。
ベースケース(Base Case)
再帰関数の終了条件のことです。再帰が無限に続かないよう、「これ以上分割できない」状態で値を返します。この問題では node is None がベースケースで、高さ0を返します。
早期リターン(Early Return)
条件を満たした時点で即座に return して処理を終える手法です。不均衡が確定した時点で右サブツリーを調べる必要がなくなるため、無駄な計算を省けます。
シングルトン(Singleton)
プログラム中に1つしか存在しないオブジェクトのことです。Pythonの NoneTrueFalse がこれにあたります。is None== None より正確で速い理由は、Noneがシングルトンだからです。
コールスタック(Call Stack)
関数呼び出しが積み重なっていく記録です。「お皿の積み重ね」のように、最後に呼んだ関数が最初に終わります(LIFO)。再帰が深くなるほどコールスタックが大きくなり、空間計算量に影響します。