Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

파이썬(Python)으로 이진 트리에서 가장 큰 완전 서브트리 찾기

문제 소개

이진 트리(Binary Tree)가 주어졌을 때, 그 안에서 가장 큰 완전 서브트리(Complete Subtree)의 크기를 구하는 것이 이번 글의 목표입니다. 여기서 완전 이진 트리란 마지막 레벨을 제외한 모든 레벨이 노드로 가득 차 있고, 마지막 레벨의 노드들은 가능한 한 왼쪽에 치우쳐 배치된 이진 트리를 의미합니다.

예를 들어 다음과 같은 이진 트리가 입력으로 주어진다고 가정해 보겠습니다.

파이썬(Python)으로 이진 트리에서 가장 큰 완전 서브트리 찾기

이 경우 정답은 크기 4이며, 해당 서브트리의 중위 순회(inorder traversal) 결과는 10, 45, 60, 70입니다.

알고리즘 접근 방식

이 문제는 후위 순회(post-order traversal) 방식으로 효율적으로 해결할 수 있습니다. 각 노드에서 왼쪽과 오른쪽 서브트리의 정보를 먼저 확인한 뒤, 현재 노드를 루트로 하는 서브트리가 완전 이진 트리인지 판단합니다. 이를 위해 다음 네 가지 값을 담는 반환 타입(returnType)을 정의합니다.

  • isComplete: 현재 서브트리가 완전 이진 트리인지 여부 (초기값 False)
  • isPerfect: 현재 서브트리가 포화 이진 트리(perfect binary tree)인지 여부 (초기값 False)
  • size: 지금까지 발견된 가장 큰 완전 서브트리의 노드 개수 (초기값 0)
  • rootTree: 가장 큰 완전 서브트리의 루트 노드 (초기값 None)

구체적인 처리 순서는 다음과 같습니다.

  1. 기저 조건: 루트가 null이면 isPerfect와 isComplete를 True로, size는 0, rootTree는 None으로 설정한 뒤 반환합니다.
  2. 왼쪽 서브트리와 오른쪽 서브트리에 대해 각각 재귀적으로 checkCompleteness를 호출합니다.
  3. 경우 1: 왼쪽 서브트리가 포화(perfect)이고 오른쪽 서브트리가 완전(complete)이며 두 서브트리의 높이가 같다면, 현재 노드를 루트로 하는 전체 서브트리가 완전 이진 트리가 됩니다. 이때 isPerfect 값은 오른쪽 서브트리의 isPerfect를 따르며, size는 양쪽 size의 합에 1을 더한 값입니다.
  4. 경우 2: 왼쪽 서브트리가 완전(complete)이고 오른쪽 서브트리가 포화(perfect)이며, 왼쪽 높이가 오른쪽 높이보다 정확히 1만큼 크다면 현재 서브트리 역시 완전 이진 트리입니다. 단, 이 경우에는 포화 이진 트리가 아니므로 isPerfect는 False가 됩니다.
  5. 그 외의 경우: 현재 노드를 루트로 하는 서브트리는 완전 이진 트리가 될 수 없으므로, 왼쪽과 오른쪽 자식 중 더 큰 완전 서브트리를 최종 결과로 선택합니다.

Python 구현 코드

다음은 위 알고리즘을 파이썬으로 구현한 전체 코드입니다.

import math

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

class returnType:
    def __init__(self):
        self.isPerfect = None
        self.isComplete = None
        self.size = 0
        self.rootTree = None

def getHeight(size):
    return int(math.ceil(math.log(size + 1) / math.log(2)))

def checkCompleteness(root):
    ret_type = returnType()

    # 기저 조건: 빈 트리는 포화이자 완전 이진 트리
    if root is None:
        ret_type.isPerfect = True
        ret_type.isComplete = True
        ret_type.size = 0
        ret_type.rootTree = None
        return ret_type

    left_tree = checkCompleteness(root.left)
    right_tree = checkCompleteness(root.right)

    # 경우 1: 왼쪽은 포화, 오른쪽은 완전, 높이가 같은 경우
    if (left_tree.isPerfect and right_tree.isComplete
            and getHeight(left_tree.size) == getHeight(right_tree.size)):
        ret_type.isComplete = True
        ret_type.isPerfect = right_tree.isPerfect
        ret_type.size = left_tree.size + right_tree.size + 1
        ret_type.rootTree = root
        return ret_type

    # 경우 2: 왼쪽은 완전, 오른쪽은 포화, 왼쪽 높이가 1 더 큰 경우
    if (left_tree.isComplete and right_tree.isPerfect
            and getHeight(left_tree.size) == getHeight(right_tree.size) + 1):
        ret_type.isComplete = True
        ret_type.isPerfect = False
        ret_type.size = left_tree.size + right_tree.size + 1
        ret_type.rootTree = root
        return ret_type

    # 그 외: 현재 서브트리는 완전 이진 트리가 될 수 없음
    ret_type.isPerfect = False
    ret_type.isComplete = False
    ret_type.size = max(left_tree.size, right_tree.size)
    if left_tree.size > right_tree.size:
        ret_type.rootTree = left_tree.rootTree
    else:
        ret_type.rootTree = right_tree.rootTree
    return ret_type

def print_tree(root):
    if root is not None:
        print_tree(root.left)
        print(root.data, end=', ')
        print_tree(root.right)

root = TreeNode(50)
root.left = TreeNode(30)
root.right = TreeNode(60)
root.left.left = TreeNode(5)
root.left.right = TreeNode(20)
root.right.left = TreeNode(45)
root.right.right = TreeNode(70)
root.right.left.left = TreeNode(10)

ans = checkCompleteness(root)
print("Size:", ans.size)
print("Inorder Traversal: ", end='')
print_tree(ans.rootTree)

입력 예시

root = TreeNode(50)
root.left = TreeNode(30)
root.right = TreeNode(60)
root.left.left = TreeNode(5)
root.left.right = TreeNode(20)
root.right.left = TreeNode(45)
root.right.right = TreeNode(70)
root.right.left.left = TreeNode(10)

실행 결과

Size: 4
Inorder Traversal: 10, 45, 60, 70,

복잡도 및 정리

이 알고리즘은 트리의 모든 노드를 정확히 한 번씩만 방문하므로 시간 복잡도는 O(n)입니다. 공간 복잡도는 재귀 호출 스택에 의해 결정되며, 균형 잡힌 트리에서는 O(log n), 편향된 트리에서는 최악의 경우 O(n)이 됩니다.

핵심은 각 노드가 "포화 여부", "완전 여부", "크기"라는 세 가지 상태 정보를 부모 노드에게 전달한다는 점입니다. 이처럼 후위 순회와 상태 추적을 결합하면 이진 트리의 구조적 성질을 활용하는 다양한 문제를 선형 시간 안에 깔끔하게 해결할 수 있습니다.