アルゴリズム概要

💡 この問題を一言で言うと:「二分木を階層ごとに読み取り、1階層おきに読む向きを反転させる問題」

木の各階層(レベル)をキュー(待ち行列)で順番に処理し、偶数番目の階層は左→右、奇数番目の階層は右→左の順で値を収集します。 最終的に各階層の値リストを2次元配列として返します。

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

  • 「木を階層ごとに読む(BFS)」は典型的な手法ですが、偶数・奇数階層で読む向きを変えるという追加条件をどこで処理するかがポイントです。
  • 階層の境界を正確に管理しないと、「今の階層」と「次の階層」のノードが混在してしまいます。level_size を事前に固定するのが鍵です。
  • Pythonでは キューの実装の選び方listdeque か)がパフォーマンスに直結します。
O(n)
時間計算量
O(n)
空間計算量
collections.deque
キュー実装
0 ≤ n ≤ 2000
ノード数制約

例 1

入力:[3,9,20,null,null,15,7]
出力:[[3],[20,9],[15,7]]

階層0→[3](左→右)、階層1→[20,9](右→左に反転)、階層2→[15,7](左→右)

例 2

入力:[1]
出力:[[1]]

ノードが1つだけなので、そのまま[[1]]を返す。

例 3

入力:[](空ツリー)
出力:[]

ガード節(root is None)で即座に空リストを返す。

📌 制約

  • ノード数:0 以上 2000 以下
  • ノードの値:−100 以上 100 以下
  • rootNone の場合あり(空ツリー)

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

Python 実装

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

  1. 空ツリーのガード節 — root is None なら即 [] を返す
  2. collections.deque にルートノードを入れてキューを初期化する
  3. while queue: で階層ループ — level_size を固定してから内側ループへ
  4. 内側ループで値を収集し、子ノードをキューに追加。ループ後に偶奇判定で逆順にして結果へ追加する
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from collections import deque

if TYPE_CHECKING:
    class TreeNode:
        val: int
        left: Optional[TreeNode]
        right: Optional[TreeNode]
        def __init__(self, val=0, left=None, right=None) -> None: ...


class Solution:
    def zigzagLevelOrder(self, root: Optional[TreeNode]) -> list[list[int]]:
        # ── ガード節 ──────────────────────────────────────────────
        # root が None(空ツリー)なら即座に空リストを返す。
        # 後続処理で node.val へアクセスして AttributeError が起きるのを防ぐ。
        if root is None:
            return []

        result: list[list[int]] = []

        # ── deque(両端キュー)の初期化 ───────────────────────────
        # list.pop(0) は O(n)、deque.popleft() は O(1)。
        # 全ノード分繰り返すと list では O(n²) になってしまう。
        queue: deque[TreeNode] = deque([root])

        # ── BFS メインループ ──────────────────────────────────────
        while queue:
            # 「今の階層のノード数」をここで固定する。
            # ループ中に子ノードをキューへ追加するため、固定しないと
            # 今の階層と次の階層の境界が崩れてしまう。
            level_size: int = len(queue)
            level_values: list[int] = []

            for _ in range(level_size):
                # deque の先頭から O(1) で取り出す
                node: TreeNode = queue.popleft()
                level_values.append(node.val)

                if node.left is not None:
                    queue.append(node.left)
                if node.right is not None:
                    queue.append(node.right)

            # ── ジグザグ処理(偶奇による方向切り替え)──────────────
            # len(result) == 「完了済み階層数」 == 「現在の階層番号」
            #   偶数(0,2,4…)→ 左→右(そのまま追加)
            #   奇数(1,3,5…)→ 右→左(in-place で逆順にする)
            # list.reverse() は新しいリストを作らない in-place 操作なので
            # [::-1] よりメモリ効率・速度ともに優れる。
            if len(result) % 2 == 1:
                level_values.reverse()

            result.append(level_values)

        return result

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

初期状態:
  queue  = deque([Node(3)])
  result = []

─ 階層 0(len(result)=0 → 偶数 → そのまま)─────────────────
  level_size = 1
  popleft() → Node(3) → level_values = [3]
    └ Node(9) と Node(20) をキューへ追加
  0 % 2 == 0 → reverse しない
  result = [[3]]   queue = deque([Node(9), Node(20)])

─ 階層 1(len(result)=1 → 奇数 → 逆順)──────────────────────
  level_size = 2
  popleft() → Node(9)  → level_values = [9]       (子なし)
  popleft() → Node(20) → level_values = [9, 20]
    └ Node(15) と Node(7) をキューへ追加
  1 % 2 == 1 → reverse() → level_values = [20, 9]
  result = [[3],[20,9]]   queue = deque([Node(15), Node(7)])

─ 階層 2(len(result)=2 → 偶数 → そのまま)─────────────────
  level_size = 2
  popleft() → Node(15) → level_values = [15]      (子なし)
  popleft() → Node(7)  → level_values = [15, 7]   (子なし)
  2 % 2 == 0 → reverse しない
  result = [[3],[20,9],[15,7]]   queue = deque([]) ← 空

while queue: → False → ループ終了
最終出力: [[3], [20, 9], [15, 7]] ✅

処理フローチャート

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

開始 丸角(緑)= 開始・終了
処理 四角(青)= 処理ステップ
◆ 条件 ◆ 四角(黄)= 条件分岐
緑=はい 赤=いいえ

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

  1. ① 開始 → root は None でないので ② の「いいえ」経路へ
  2. ③ 初期化:result=[]、queue=deque([Node(3)]) をセット
  3. ④ while queue → キューに Node(3) があるので「はい」へ
  4. ⑤ level_size=1 に固定。level_values=[] を用意
  5. ⑥ for ループ:Node(3) を popleft → level_values=[3]。Node(9)・Node(20) をキューへ追加。ループ終了
  6. ⑧ len(result)=0(偶数)→「いいえ」→ reverse せずそのまま append → result=[[3]]。④ に戻る
  7. ④ while → Node(9)・Node(20) あり。⑤ level_size=2。⑥ 両ノード処理 → level_values=[9,20]。Node(15)・Node(7) をキューへ
  8. ⑧ len(result)=1(奇数)→「はい」→ reverse() → level_values=[20,9] → append → result=[[3],[20,9]]
  9. 同様に階層2を処理:level_values=[15,7]、len(result)=2(偶数)→ そのまま append → result=[[3],[20,9],[15,7]]
  10. ④ while → キューが空 →「いいえ」→ ⑨ return [[3],[20,9],[15,7]]

計算量分析

📖 Big-O 記法の読み方(入力サイズ n が大きくなるにつれて処理時間がどう増えるかの目安)

O(1)
常に一定
例:辞書の直接引き
O(n)
入力に比例
例:リストを1回走査
O(n log n)
n より少し多い
例:ソートアルゴリズム
O(n²)
入力の2乗
例:二重ループ総当たり
種別 計算量 理由
時間計算量 O(n) 全ノードを一度だけ訪問する。reverse() は各階層サイズ k に対して O(k) だが、全階層を合計すると O(n)
空間計算量 O(n) deque は最大で最も幅の広い階層のノード数を格納する。完全二分木では最大 n/2 ノード。result リストも最終的に n ノード分を格納する
操作 コード 追加メモリ 速度
✅ in-place(採用) level.reverse() O(1)(なし) 高速(C実装)
❌ Pure(不採用) level[::-1] O(k)(新リスト生成) やや低速

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

BFS では各ノードをキューから1回だけ取り出し、その値を収集して子ノードを追加します。 ノード数を n とすると、popleft・append・level_values.append はすべて O(1) なので合計 O(n)。 reverse() は各階層のサイズ k に対して O(k) ですが、 すべての階層のサイズの合計はちょうど n になるため、全体でも O(n) に収まります。 空間については、deque に同時に入るノードは「最も幅の広い階層」だけなので最大 O(n/2) = O(n) です。

📖 用語集

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

BFS(幅優先探索)
木やグラフを「階層ごと・横方向に」順番に訪問する探索手法。キュー(待ち行列)を使って実装する。
例え話:マンションの1階を全室確認してから2階へ、2階を全室確認してから3階へ……と進むイメージ。深さ優先(DFS)とは逆のアプローチ。
deque(両端キュー)
Python の collections.deque が提供するデータ構造。先頭・末尾への追加・取り出しがどちらも O(1)(一定時間)で行える。
通常の listpop(0)(先頭取り出し)をすると O(n) かかるため、BFS キューには必ず deque を使う。
ガード節(早期リターン)
関数の先頭で特殊ケース(空の入力・不正な値など)をチェックし、その場で return する書き方。
後続の処理をネストさせずに済み、コードをシンプルに保てる。この問題では if root is None: return [] がそれにあたる。
in-place 操作
新しいメモリを確保せず、元のデータを直接書き換える操作。list.reverse() がその代表例。
対比:list[::-1] は新しいリストを生成する(Pure 操作)。in-place の方が追加メモリを消費しない分、効率的。
不変条件
アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件のこと。
この問題では「while ループの先頭で queue に現在の階層のノードだけが格納されている」ことが不変条件。level_size の固定がこれを保証する。
完全二分木
全ての葉(子を持たないノード)が同じ深さにある最もバランスの取れた二分木。最下層の幅(ノード数)は全ノード数 n の約半分(n/2)になる。
この問題の空間計算量 O(n) を考えるときに参考になる最悪ケース。
キュー(Queue)
「先に入れたものを先に取り出す」データ構造(FIFO: First In First Out)。
例え話:コンビニのレジの行列と同じ。先に並んだ人が先に処理される。BFS では「次に訪問すべきノード」をキューで管理する。
ルートノード
木の最上位にある起点ノード(頂点)。木全体の出発点で、親を持たない唯一のノード。
BFS ではルートノードをキューに入れることで探索を開始する。