0Pricing
Coding Interview Prep · レッスン

BSTへの挿入と探索

挿入と探索を再帰版・反復版で実装し、さまざまなキーについて木を通る経路を追跡し、非平衡木の最悪時計算量を分析します。

「BSTへの挿入と探索」はCoddyKit上の無料Coding Interview Prepレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはCoding Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Coding Interview Prepコースには全4レッスンが含まれています。

BSTの性質の定義

二分探索木は、1つの不変条件を満たします。すべてのノードについて、左部分木のすべての値はノードの値より厳密に小さく、右部分木のすべての値はノードの値より厳密に大きいという条件です。この順序付けの性質は、直下の子だけでなく部分木全体で維持されます。そのため、平衡した木では検索、挿入、削除をO(log n)で行うことができ、BSTは一般的な二分木と区別されます。

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

# Valid BST:
#       4
#      / \
#     2   6
#    / \ / \
#   1  3 5  7
# For node 4: left subtree {1,2,3} < 4 < right subtree {5,6,7}
# This holds recursively for EVERY node in the tree.
print('BST property: left < node < right at every level')

BSTの再帰的な検索

BSTの検索は二分探索と同じように機能します。目標値と現在のノードの値を比較し、適切な部分木へ再帰します。目標値が現在の値と等しければ、そのノードを返します。目標値が小さければ左へ、大きければ右へ進みます。空のノードに到達したらnullを返します。計算量はO(h)で、平衡した木ではO(log n)、偏った木ではO(n)です。

def search_bst(root, val):
    if not root:
        return None  # not found
    if root.val == val:
        return root  # found
    if val < root.val:
        return search_bst(root.left, val)
    else:
        return search_bst(root.right, val)

root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)

result = search_bst(root, 2)
print(result.val if result else 'Not found')  # 2
result = search_bst(root, 5)
print(result.val if result else 'Not found')  # Not found

BSTの反復的な検索

反復的な検索では呼び出しスタックのオーバーヘッドを避けられるため、実運用のコードで推奨されます。currポインタを使い、比較結果に応じて左または右へ木を下っていきます。これは、null(見つからない)、一致(見つかった)、方向の調整という3つのケースを処理する単純なwhileループです。反復的な検索もO(h)ですが、再帰版のO(h)空間に対して、空間計算量はO(1)です。

def search_bst_iterative(root, val):
    curr = root
    while curr:
        if val == curr.val:
            return curr
        elif val < curr.val:
            curr = curr.left
        else:
            curr = curr.right
    return None  # not found

root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)

node = search_bst_iterative(root, 3)
print(node.val if node else 'Not found')  # 3
print(search_bst_iterative(root, 9))     # None

BSTの再帰的な挿入

BSTへの挿入では、検索と同じ左または右の判断をたどって正しい位置を見つけ、最初に到達したnullの位置に新しいノードを取り付けます。再帰的な方法では、各部分木の(新しくなった可能性のある)根を返します。現在のノードがnullなら新しいTreeNodeを返し、それ以外の場合は再帰呼び出しの結果でroot.leftまたはroot.rightを更新します。このパターンは分かりやすく、面接の解答でよく使われます。

def insert_bst(root, val):
    if not root:
        return TreeNode(val)  # create new node here
    if val < root.val:
        root.left = insert_bst(root.left, val)
    elif val > root.val:
        root.right = insert_bst(root.right, val)
    # val == root.val: duplicate, do nothing (or handle as needed)
    return root

root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root = insert_bst(root, 1)
root = insert_bst(root, 5)
# Tree is now: 4, left=2(left=1), right=7(left=5)
print(root.right.left.val)  # 5

BSTの反復的な挿入

反復的な挿入では、挿入位置に到達する前の最後のnullでないノードを追跡するために、parentポインタを使います。検索と同じように木を下りながら、親と最後に進んだ方向を記録します。nullに到達したら、親の適切な側に新しいノードを取り付けます。空の木の場合(rootがnullの場合)は、必ず別に処理してください。

def insert_bst_iterative(root, val):
    new_node = TreeNode(val)
    if not root:
        return new_node
    curr = root
    while True:
        if val < curr.val:
            if curr.left is None:
                curr.left = new_node
                break
            curr = curr.left
        else:  # val > curr.val
            if curr.right is None:
                curr.right = new_node
                break
            curr = curr.right
    return root

root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root = insert_bst_iterative(root, 3)
print(root.left.right.val)  # 3

最悪の場合のBST:偏った木

BSTにソート済みのシーケンスを挿入すると、偏った木になり、連結リストのような構造に変化します。検索、挿入、削除はすべてO(n)になります。これが平衡したBST(AVL木やRed-Black木)が存在する理由です。面接でBSTの計算量を聞かれたら、必ずこの最悪ケースにも言及してください。「平衡していない木では平均O(log n)、最悪O(n)」と答えれば、深い理解を示せます。

# Inserting 1, 2, 3, 4, 5 into a BST:
# 1
#  \
#   2
#    \
#     3
#      \
#       4
#        \
#         5
# This is a right-skewed tree: search is O(n) not O(log n)

root = None
for val in [1, 2, 3, 4, 5]:
    root = insert_bst(root, val)

# Verify the skew
node = root
depth = 0
while node:
    depth += 1
    node = node.right
print(f'Height: {depth}')  # 5 = O(n), not O(log n)

最小値と最大値の検索

BSTでは、最小値は必ず左端のノードにあります(nullに到達するまで左へ進み続けます)。同様に、最大値は右端のノードにあります。これらのO(h)の操作は、BSTの削除(中順後継ノードの検索)や範囲クエリのサブルーチンとして頻繁に使われます。これらのヘルパー関数を覚えておくと、面接で時間を節約できます。

def find_min(root):
    while root.left:
        root = root.left
    return root

def find_max(root):
    while root.right:
        root = root.right
    return root

root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
root.right.right = TreeNode(9)

print(find_min(root).val)  # 1
print(find_max(root).val)  # 9

中順後継ノードと前任ノード

ノードの中順後継ノードとは、そのノードより大きい値の中で最小の値を持つノードです。そのノードに右部分木がある場合、後継ノードはfind_min(node.right)です。右部分木がない場合、後継ノードは、対象のノードが左部分木に含まれる最も近い祖先です。これはBSTの削除やBSTイテレータの問題を理解するうえで重要です。

def inorder_successor(root, p):
    successor = None
    while root:
        if p.val < root.val:
            successor = root  # possible successor
            root = root.left
        else:
            root = root.right
    return successor

def inorder_predecessor(root, p):
    predecessor = None
    while root:
        if p.val > root.val:
            predecessor = root  # possible predecessor
            root = root.right
        else:
            root = root.left
    return predecessor

root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(6)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
p = root.left  # node with val=2
print(inorder_successor(root, p).val)   # 3
print(inorder_predecessor(root, p).val) # 1

BSTの検索計算量の分析

BSTの性能は木の高さに完全に依存します。n個のノードを持つ平衡したBSTでは、高さがO(log n)になるため、検索、挿入、削除はいずれもO(log n)です。偏ったBSTでは高さがO(n)になり、すべての操作がO(n)になります。Pythonには、JavaのTreeMapのような組み込みの平衡BSTがありません。そのため、自分でAVL木やRed-Black木を実装するか、sortedcontainers.SortedListを使うか、優先度付きキューの用途ではヒープを利用します。

# Python's BST alternatives:
# 1. heapq - min/max heap, O(log n) push/pop
# 2. sortedcontainers.SortedList (third-party, often allowed)
# 3. Manual AVL or Red-Black (rarely required in interviews)

# When interviews say 'use a BST':
# - LeetCode: implement TreeNode-based solution
# - Real interview: mention sortedcontainers or Java TreeMap equivalent
# - O(log n) operations matter when you need ordered access

# For pure insert/lookup without ordering: use dict (O(1) average)
print('Use heap for priority, dict for lookup, BST for ordered range')

BSTへの挿入:エッジケース

挿入処理では、空の木(新しいノードを根として返す)、重複値(無視するのか、左に挿入するのか、右に挿入するのかを定義し、一貫させる)、非常に大きい値や小さい値を必ず確認してください。面接では、コーディングの前に重複値についての前提を述べます。LeetCodeの問題で最も一般的な規約は、特に指定がない限り、すべての値が異なるというものです。

def insert_bst_no_duplicates(root, val):
    if not root:
        return TreeNode(val)
    if val < root.val:
        root.left = insert_bst_no_duplicates(root.left, val)
    elif val > root.val:
        root.right = insert_bst_no_duplicates(root.right, val)
    # else: val == root.val -> duplicate, skip
    return root

# Test all edge cases:
root = None
root = insert_bst_no_duplicates(root, 5)  # empty tree
root = insert_bst_no_duplicates(root, 5)  # duplicate
root = insert_bst_no_duplicates(root, 3)
root = insert_bst_no_duplicates(root, 7)
print(root.val, root.left.val, root.right.val)  # 5 3 7

ソート済み配列からBSTを構築する

ソート済み配列から高さが平衡したBSTを構築する(LeetCode #108)には、分割統治法を使います。中央の要素を根にし、左半分を左部分木に、右半分を右部分木にします。これにより、高さO(log n)の平衡した木が保証されます。各要素を1回ずつ処理するため、時間計算量はO(n)です。

def sorted_array_to_bst(nums):
    if not nums:
        return None
    mid = len(nums) // 2
    root = TreeNode(nums[mid])
    root.left = sorted_array_to_bst(nums[:mid])
    root.right = sorted_array_to_bst(nums[mid+1:])
    return root

nums = [-10, -3, 0, 5, 9]
root = sorted_array_to_bst(nums)
print(root.val)        # 0 (middle element)
print(root.left.val)   # -3
print(root.right.val)  # 9

確認テスト

このレッスンで扱ったData Structures & Algorithms — Coding Interview Prepの概念について、理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、BSTの性質(左部分木は厳密に小さく、右部分木は厳密に大きい)、O(h)時間で行う検索と挿入の再帰版および反復版、高さがnに等しくなる最悪ケースの偏った木を学びました。次はBSTの削除と、その3つのケースに取り組みます。

よくある質問

「BSTへの挿入と探索」レッスンは無料ですか?

はい。「BSTへの挿入と探索」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Coding Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Coding Interview Prepコースには全4レッスンが含まれています。

「BSTへの挿入と探索」で何を学びますか?

挿入と探索を再帰版・反復版で実装し、さまざまなキーについて木を通る経路を追跡し、非平衡木の最悪時計算量を分析します。 ブラウザで直接実行するハンズオンコードでCoding Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Coding Interview Prepを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのCoding Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「BSTへの挿入と探索」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このCoding Interview Prepレッスンでコードを書いて実行できますか?

はい。すべてのCoding Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. BSTへの挿入と探索
  2. BSTの削除:3つのケース
  3. BSTの検証とin-orderの性質
  4. k番目に小さい値、区間和、BSTからソート済み配列へ
← Coding Interview Prepに戻る