Python
 Computer >> コンピューター >  >> プログラミング >> Python

Pythonで二分探索木の最小共通祖先(LCA)を求める方法【再帰アルゴリズム解説】

最小共通祖先(LCA)とは

二分探索木(Binary Search Tree)が与えられたとき、指定された2つのノードの最小共通祖先(Lowest Common Ancestor:LCA)を求める問題を考えてみましょう。

ノード p と q の LCA とは、「p と q の両方を子孫として持つノードの中で、最も深い位置にあるノード」のことです。

例えば、次のような二分木があるとします。

[6, 2, 8, 0, 4, 7, 9, null, null, 3, 5]

この木は以下のような構造になります。

Pythonで二分探索木の最小共通祖先(LCA)を求める方法【再帰アルゴリズム解説】

この場合、2 と 8 の LCA は 6 となります。6 は 2 と 8 の両方を子孫に持ち、それより深い共通の祖先は存在しないためです。

解法のアプローチ

この問題は再帰を使うことでシンプルに解くことができます。手順は以下の通りです。

  • 木が空(null)の場合は、None を返します。
  • p または q のどちらかが現在のルートと一致する場合は、そのルートを返します。
  • left := ルートの左部分木に対して p と q の LCA を再帰的に求めます。
  • right := ルートの右部分木に対して p と q の LCA を再帰的に求めます。
  • left と right が両方とも非 null であれば、p と q が左右に分かれていることを意味するため、現在のルートが LCA となり、ルートを返します。
  • それ以外の場合は、非 null になっている側(left または right)を返します。

Pythonでの実装例

それでは、実際の実装を見てみましょう。

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

class Solution():
    def lowestCommonAncestor(self, root, p, q):
        if not root:
            return None
        if p == root or q == root:
            return root
        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)
        if left and right:
            return root
        return left or right

def insert(temp, data):
    que = []
    que.append(temp)
    while len(que):
        temp = que[0]
        que.pop(0)
        if not temp.left:
            temp.left = TreeNode(data)
            break
        else:
            que.append(temp.left)
        if not temp.right:
            temp.right = TreeNode(data)
            break
        else:
            que.append(temp.right)

def make_tree(elements):
    Tree = TreeNode(elements[0])
    for element in elements[1:]:
        insert(Tree, element)
    return Tree

def search_node(root, element):
    if root == None:
        return None
    if root.data == element:
        return root
    res1 = search_node(root.left, element)
    if res1:
        return res1
    res2 = search_node(root.right, element)
    return res2

root = make_tree([6, 2, 8, 0, 4, 7, 9, None, None, 3, 5])
ob1 = Solution()
op = ob1.lowestCommonAncestor(root, search_node(root, 2), search_node(root, 8))
print(op.data)

入力

[6,2,8,0,4,7,9,null,null,3,5]
2
8

出力

6

処理の流れと計算量

このアルゴリズムでは、まず目的のノード p と q を search_node 関数で探索し、そのノードオブジェクトを取得します。その後、lowestCommonAncestor メソッドが再帰的に木をたどり、p と q が左右の部分木に分かれる最初のノードを見つけ出します。

  • 時間計算量: 各ノードを最大1回訪問するため、O(n) となります(n はノード数)。
  • 空間計算量: 再帰呼び出しによるスタック領域として、最悪ケース(木が線形に偏っている場合)で O(n)、バランスの取れた木では O(log n) となります。

補足:BSTの性質を活用した最適化

上記のコードは一般的な二分木にも使える汎用的な手法ですが、対象が厳密な二分探索木(BST)である場合は、さらに効率よく求められます。BSTでは「左の子 < 親 < 右の子」という順序が保証されているため、以下のように判断できます。

  • p と q が両方ともルートより小さい場合は、LCA は左部分木に存在します。
  • p と q が両方ともルートより大きい場合は、LCA は右部分木に存在します。
  • それ以外(p と q がルートを挟んで分かれている、あるいはどちらかがルート)の場合は、現在のルートが LCA です。

この方法では、毎回両方の部分木を探索する必要がないため、平均的により高速に LCA を特定できます。

  1. Pythonで二分木を反転する方法:再帰を使った実装を解説

    二分木の反転とは二分木が与えられたとき、その左右の子ノードを入れ替えて「鏡像」のような木を作ることを二分木の反転(Invert Binary Tree)と呼びます。これはアルゴリズムの学習やコーディング面接でも頻出のトピックです。例えば、次のような二分木があったとします。これを反転すると、すべてのノードの左部分木と右部分木が入れ替わり、以下のような木になります。解き方:再帰的アプローチこの問題は再帰を使うと非常にシンプルに解けます。考え方は以下の3ステップです。ルートが None(null)であれば、そのまま返す(ベースケース)現在のノードの左ポインタと右ポインタを入れ替える左部分木と右部分木

  2. Pythonでソート済み配列を高さバランスの二分探索木に変換する方法

    ソートされた配列 A が与えられたとき、そこから高さバランスの取れた二分探索木(BST)を生成することを考えます。ここで「高さバランスの取れた二分木」とは、すべてのノードにおいて、左右の部分木の深さの差が常に1以下であるような二分木のことを指します。例として、配列が [-10, -3, 0, 5, 9] の場合、出力の一例は [0, -3, 9, -10, null, 5] のようになります。解法のアプローチこの問題は、配列の中央要素をルートに選ぶというシンプルな発想で解くことができます。具体的な手順は以下の通りです。配列 A が空の場合は、Null(None)を返します。配列の中央の要素を見