ゆめさくエンジニア研究所
木の走査問題 16

木を​段ごとに読む

二分木の​値を、​上の​段から​順に、​段ごとの​並びに​まとめて​返す。​待ち行列で​近い​節から​順に​見ていく、​幅優先の​読み方の​練習です。

問題

二分木の根 root を受け取ります。上の段から順に、各段の値を左から並べた並びを作り、それらをまとめた並びの並びを返す関数を書いてください。

  • 各節は val (値)、left (左の子)、right (右の子) を持ちます。子が無いところは、Python では None、JavaScript では null です
  • 木が空 (root が無い) のときは、空の並び [] を返します
  • 節の数は 0 以上 2000 以下、値はどれも -1000 以上 1000 以下です

例では、木を上の段から左の順に読んだ値の並びで書きます。子が無いところは null です。

入力と出力の例

例 1

根が 5、その下の段が 2 と 8、さらに下の段が 1、6、9 の木です。2 には右の子がありません。

入力

root = [5, 2, 8, 1, null, 6, 9]

出力

[[5], [2, 8], [1, 6, 9]]

例 2

右へ、左へと 1 本道に伸びる木です。どの段も 1 つずつになります。

入力

root = [4, null, 7, 3]

出力

[[4], [7], [3]]

例 3

空の木です。

入力

root = []

出力

[]

面接での聞かれ方

面接では、短く次のように渡されることが多いです。

  • 「二分木を、段ごとに値をまとめて返してください」
  • 書き終えたあとに「右から読む形にできますか?」「段ごとではなく、ジグザグに読めますか?」と少しずつ形を変えて聞かれます
  • 「再帰でも書けますか?」と、別の書き方を聞かれることもあります

木の問題では、空の木をどう返すかを最初に確かめておくと、あとで迷いません。

まず考える手順

段 1

まずは、木を全部たどる、いつもの書き方を思い出します。根から左、右と再帰でたどると、全部の節に 1 回ずつ行けます。ただし、この順は「左の奥へ先に潜る」順なので、段の順にはなりません。

段 2

段の順に読むには、根に近い節から先に見る必要があります。そこで、待ち行列 (先に入れた物から先に出す列) を使います。根を入れ、1 つ取り出すたびに、その子を後ろへ入れていきます。こうすると、浅い段の節が必ず先に出てきます。これを幅優先と呼びます。

段 3

最後に、段の切れ目を見分けます。ある段を読み始める時点で、待ち行列にはちょうどその段の節だけが入っています。そこで、読み始める前の長さを覚えておき、その数だけ取り出したら 1 段分の並びとしてまとめます。

段 4

待ち行列の作り方は、言語ごとに選びます。Python では collections.deque を使うと、先頭から取り出す手間が一定で済みます。JavaScript では、配列と「次に取り出す位置」を指す数を使い、先頭を消さずに指を進めます。

ここから​解答です。​先に​自分で​書いてから​読むと、​考え方が​残ります。

解答

Python

Python

from collections import deque
def tree_levels(root):
    if root is None:
        return []
    levels = []
    queue = deque([root])
    add = queue.append
    while queue:
        row = []
        size = len(queue)
        for _ in range(size):
            nd = queue.popleft()
            row.append(nd.val)
            if nd.left:
                add(nd.left)
            if nd.right:
                add(nd.right)
        levels.append(row)
    return levels

JavaScript

JavaScript

function treeLevels(root) {
  if (root === null) return [];
  const levels = [];
  const queue = [root];
  let head = 0;
  while (head < queue.length) {
    const end = queue.length;
    const row = [];
    while (head < end) {
      const node = queue[head];
      head++;
      row.push(node.val);
      if (node.left) {
        queue.push(node.left);
      }
      if (node.right) {
        queue.push(node.right);
      }
    }
    levels.push(row);
  }
  return levels;
}

JavaScript の end は、その段を読み始めたときの配列の長さです。読んでいる間に子を後ろへ足しても、end までで止まるので、次の段の節が混ざりません。

計算量

  • 時間は O(n) です。どの節も待ち行列に 1 回入り、1 回出るだけです
  • 使う記憶は O(n) です。いちばん節の多い段が、待ち行列に一度に並びます。返す並びも全部の値を持ちます
  • JavaScript の書き方は、取り出した節も配列に残したままにします。その分の記憶は増えますが、O(n) のままです

よくある間違い

  • 配列の先頭を消して取り出す。 JavaScript の shift() や Python の list.pop(0) は、残りを全部 1 つずつ前へずらします。節が多いと、全体が O(n²) になります
  • 読みながら長さを見直す。段の長さを、取り出すたびに len(queue) で見直すと、足したばかりの次の段まで同じ段に入ってしまいます。読み始める前に 1 回だけ数えます
  • 空の木を見落とす。 root が無いのに待ち行列へ入れると、取り出した節の val を読もうとして止まります。最初に空の並びを返しておきます

面接での話し方の例

書き始める前に、方針を短く口に出します。

「段の順に読みたいので、深さ優先ではなく、待ち行列を使った幅優先で書きます。段の切れ目は、その段を読み始める前の待ち行列の長さで分けます。どの節も 1 回ずつしか出し入れしないので、時間は O(n) です。」

書き終えたら、例を 1 つ手でたどって見せます。

「例 1 でたどります。最初は 5 だけが入っていて、長さ 1 なので 1 つ取り出して [5] です。そのとき 2 と 8 を足します。次は長さ 2 なので 2 つ取り出して [2, 8] になり、1、6、9 が足されます。最後に長さ 3 で [1, 6, 9] となり、待ち行列が空になって終わります。」

解答を動かした版 Python 3.12.14Node.js 22.23.4確認日 2026年10月24日

コーディング面接問題の一覧へ戻る