アルゴリズム概要

問題の説明

二分木の根ノードが与えられたとき、それが有効なBST(Binary Search Tree)であるかを判定します。

BST の定義:

  • 各ノードの左部分木には、そのノードより厳密に小さい値のノードのみが含まれる
  • 各ノードの右部分木には、そのノードより厳密に大きい値のノードのみが含まれる
  • 左右の部分木もそれぞれBSTである

入出力例

Example 1:

Input: root = [2,1,3]
    2
   / \
  1   3
Output: true

Example 2:

Input: root = [5,1,4,null,null,3,6]
      5
     / \
    1   4
       / \
      3   6
Output: false (4は5より小さいべき)

制約条件

戦略

核心アイデア:

BSTの中順巡回(Inorder Traversal)厳密単調増加列を生成します(必要十分条件)。 スタックを使った反復的な巡回で全ノードを訪問し、prev(直前の値)と比較して node.val > prev を確認します。

主要ポイント

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

Python実装

from typing import Optional, List

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

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        """
        反復的な中順巡回でBSTを検証

        時間計算量: O(n)
        空間計算量: O(h) where h is tree height
        """
        stack: List[TreeNode] = []
        cur: Optional[TreeNode] = root
        prev: Optional[int] = None

        # ローカル束縛による最適化
        push = stack.append
        pop = stack.pop

        while cur is not None or stack:
            # 左部分木を全てスタックに積む
            while cur is not None:
                push(cur)
                cur = cur.left

            # 最左端のノードを取り出す
            node = pop()
            val = node.val

            # 厳密単調増加チェック
            if prev is not None and val <= prev:
                return False

            # prev を更新
            prev = val

            # 右部分木へ移動
            cur = node.right

        return True

フローチャート

開始 初期化 stack=[], cur=root, prev=None cur≠None or stack≠空? いいえ True を返却 はい cur≠None? はい push(cur) cur = cur.left 左探索ループ いいえ node = pop() val = node.val prev≠None and val≤prev? はい False を返却 いいえ prev = val cur = node.right 次のノードへ

フローの説明:
1. スタック、cur、prevを初期化
2. 緑の矢印(はい):curがNoneでないかスタックに要素があればループ継続
3. 左部分木探索:curがNoneでない限りスタックに積み、左へ移動
4. 赤の矢印(いいえ):最左端到達後、ノードを取り出す
5. BST違反チェック:val ≤ prev なら即座にFalse返却
6. 紫の矢印(ループバック):prevを更新後、右部分木へ移動してループ再開
7. 全ノード通過でTrueを返却

計算量分析

項目 計算量 説明
時間計算量 O(n) 全ノードを1回ずつ訪問。各ノードでの処理はO(1)
空間計算量 O(h) スタックの最大深さは木の高さh。最悪(一本道)でO(n)、平衡木でO(log n)
追加メモリ O(1) prev変数のみ(整数1つ)

代替手法との比較

手法 時間 空間 備考
本実装(Inorder反復) O(n) O(h) ✓ 最適。メモリ効率が高い
Inorder配列化 O(n) O(n) 全要素を配列に格納。無駄が多い
再帰DFS(範囲チェック) O(n) O(h) Pythonでは深い木で再帰限界リスク
Morris Traversal O(n) O(1) 木を一時改変。業務では避けがち

最適化のポイント:

  • 全要素を配列に格納せず、prevのみで比較
  • ローカル変数束縛(push = stack.append)でCPython最適化
  • 違反検出時の早期リターンで無駄な探索を回避
  • 再帰を使わないため、任意の深さに対応可能