19 木構造(二分木)

1. 木構造とは

木構造(ツリー)とは、データを階層的に管理するためのデータ構造です。 家系図やフォルダ構造のように、1つの起点から枝分かれしていく形をしています。

A B C D E F G ← 根(root) 葉(leaf)

2. 木構造の用語

用語意味例(上の図)
節点(ノード)木を構成する各要素A, B, C, D, E, F, G
根(root)木の頂点にある節点(親を持たない)A
葉(leaf)子を持たない節点D, E, F, G
親(parent)すぐ上にある節点B の親は A
子(child)すぐ下にある節点A の子は B, C
兄弟(sibling)同じ親を持つ節点B と C
深さ(depth)根からの距離A=0, B=1, D=2
高さ(height)最も深い葉までの距離この木は 2

3. 二分木とは

二分木(バイナリツリー)とは、各節点が最大2つの子しか持たない木構造です。 子はそれぞれ「左の子」「右の子」と区別されます。

左の 右の left right
ポイント:「左の子」と「右の子」は区別されます。 たとえ片方しかなくても、「左にいる子」「右にいる子」かは意味が異なります。

4. 二分木の節点の構造

二分木の節点は、以下の3つを持ちます。

節点型: Node
    値型:  value      // この節点が持つデータ
    Node型: left      // 左の子(無いときは null)
    Node型: right     // 右の子(無いときは null)
endtype
null(ヌル)とは「何もない」「存在しない」を表す特別な値です。 葉の節点では leftright はどちらも null になります。

5. 木の走査(traversal)

木のすべての節点を順番に訪問することを走査といいます。 訪問順序によって3種類があり、いずれも再帰で書けます。

1 2 3 4 5

① 前順(行きがけ順 / preorder)

「自分 → 左 → 右」の順に訪問する。

○ preorder(Node型: node)
    if ( node が null と等しい )
        return
    endif
    node.value を出力      // 自分
    preorder(node.left)    // 左
    preorder(node.right)   // 右

上の木の場合、出力は 1 → 2 → 4 → 5 → 3

② 中順(通りがけ順 / inorder)

「左 → 自分 → 右」の順に訪問する。

○ inorder(Node型: node)
    if ( node が null と等しい )
        return
    endif
    inorder(node.left)     // 左
    node.value を出力      // 自分
    inorder(node.right)    // 右

上の木の場合、出力は 4 → 2 → 5 → 1 → 3

③ 後順(帰りがけ順 / postorder)

「左 → 右 → 自分」の順に訪問する。

○ postorder(Node型: node)
    if ( node が null と等しい )
        return
    endif
    postorder(node.left)    // 左
    postorder(node.right)   // 右
    node.value を出力       // 自分

上の木の場合、出力は 4 → 5 → 2 → 3 → 1

覚え方:「自分」をどこに置くかで名前が決まる。
=最初、=真ん中、=最後。

6. 二分探索木(BST)

二分探索木(Binary Search Tree)は、以下の決まりを持つ二分木です。

50 30 70 20 40 60 80

この木で 40 を探す場合:

  1. 50 と比較 → 40 < 50 なので左へ
  2. 30 と比較 → 40 > 30 なので右へ
  3. 40 と一致 → 発見!
中順走査(inorder)で二分探索木をたどると、値が小さい順に並んで出力されます。
例の木:20 → 30 → 40 → 50 → 60 → 70 → 80(昇順)

新しい値の挿入

新しい値を二分探索木に追加するときは、根から比較しながら進み、空いている場所(null)に到達したらそこに置くだけです。

○ insert(Node型: node, 整数型: v)
    if ( node が null と等しい )
        return 新しいノード(value=v, left=null, right=null)
    endif
    if ( v が node.value より小さい )
        node.left ← insert(node.left, v)     // 左の部分木に追加
    elseif ( v が node.value より大きい )
        node.right ← insert(node.right, v)   // 右の部分木に追加
    endif
    return node

例:上の木に 45 を挿入してみる:

  1. 50 と比較 → 45 < 50 → 左へ移動
  2. 30 と比較 → 45 > 30 → 右へ移動
  3. 40 と比較 → 45 > 40 → 右へ移動
  4. 40 の右は null(空き) → ここに 45 を置く

7. 二分探索木での探索アルゴリズム

○ 論理型: search(Node型: node, 整数型: target)
    if ( node が null と等しい )
        return false                         // 見つからなかった
    endif
    if ( node.value が target と等しい )
        return true                          // 発見
    elseif ( target が node.value より小さい )
        return search(node.left, target)     // 左の部分木を探す
    else
        return search(node.right, target)    // 右の部分木を探す
    endif
開始 node が null? True return false False value= target? True return true target< value? False True search (node.left) False search (node.right) 終了

8. 計算量

二分探索木がバランスよく形成されているとき、探索の計算量は O(log n) です。

状態探索の計算量説明
バランスがよい場合O(log n)1ステップごとに範囲が約半分
偏った場合(最悪)O(n)1列に並んで連結リスト同様になる
注意:挿入順によっては木が大きく偏ることがあります。
例えば、1, 2, 3, 4, 5 を順に挿入すると、右側だけに伸びる「斜めの木」になり、探索は O(n) になります。

9. 理解度チェック問題

問題19-1

次の木構造の「葉」をすべて挙げなさい。

A B C D E
解説を表示 正解:ウ
葉とは「子を持たない節点」。図では C, D, E が葉。B は子 D, E を持つので葉ではない。

問題19-2

次の二分木を前順(preorder)で走査したときの出力を選びなさい。

A B C D E
解説を表示 正解:ア
前順は「自分→左→右」。
A(自分) → B(左へ) → D(Bの左) → E(Bの右) → C(Aの右)
よって A B D E C。

問題19-3

問題19-2と同じ木を中順(inorder)で走査したときの出力を選びなさい。

解説を表示 正解:イ
中順は「左→自分→右」。
Aの左部分木: D → B → E、A、Aの右部分木: C
よって D B E A C。

問題19-4

次の二分探索木に 45 を挿入する場合、どの節点の子になりますか。

50 30 70 20 40
解説を表示 正解:エ
45 を根から比較していくと:
50と比較 → 45<50 → 左へ → 30
30と比較 → 45>30 → 右へ → 40
40と比較 → 45>40 → 右へ → null(空き)
よって 45 は 40 の右の子になる。

問題19-5

二分探索木を中順走査すると、節点の値はどのように出力されますか。

解説を表示 正解:ア
二分探索木は「左の子<自分<右の子」という決まりがあるため、中順(左→自分→右)でたどると必ず昇順になります。
この性質を使ったソート方法を「二分木ソート」と呼びます。