Skip to content

[자료구조] 07. 트리

트리(Tree)의 개념과 Python으로 구현하기


트리의 개념

트리는 노드(Node)와 브랜치(Branch)를 이용해서, 사이클을 이루지 않도록 구성한 데이터 구조로, 다양한 트리 형태의 데이터 구조 중에 이진 트리(Binary Tree, B-tree)는 탐색(검색) 알고리즘 구현을 위해 많이 사용된다.

Note

실제로 대부분의 상용 RDB는 B-tree로 구현된 인덱스를 제공하고 있다.

트리의 개념에서 사용되는 용어들은 다음과 같다.

  • Node: 트리에서 데이터를 저장하는 기본 요소
    • 데이터와 다른 연결된 노드에 대한 브랜치 정보 포함함
    • root Node: 트리 최상위에 존재하는 노드
    • parent/child Node: 어떤 노드의 상/하위 노드
    • leaf/terminal Node: Child 노드가 없는 노드
    • sibling/brother Node: 동일한 Parent 노드를 가진 노드
  • level: 브랜치로 연결된 노드의 깊이를 나타냄. 최상위 노드가 0
    • depth: 트리에서 노드가 가질 수 있는 최대 깊이

주요 트리의 종류는 아래와 같다.

  • 이진 트리(Binary Tree, B-tree)
    • 노드의 최대 브랜치가 2인 트리
  • 이진 탐색 트리(Binary Search Tree, BST)
    • 왼쪽 노드는 상위 노드보다 작은 값, 오른쪽 노드는 상위 노드보다 큰 값이라는 조건이 추가된 이진 트리

Note

이진 탐색 트리는 탐색 속도가 매우 빠르기 때문에 데이터 검색(탐색) 기능을 구현하는데 많이 사용된다.

트리의 시간 복잡도

이진 탐색 트리는 실행 시 한 level을 내려갈 때마다 실행이 필요할 수도 있는 연산의 50%를 배제하여 실행 시간을 50%씩 단축시킬 수 있다.

\(n\)개의 노드를 가질 때 \(depth = log_{2}n\)에 근사하므로 평균적인 시간 복잡도는 \(O(log_{2}n)\)이 된다.

Warning

이진 탐색 트리의 평균적인 시간 복잡도는 \(O(log_{2}n)\)이지만 링크드 리스트와 동일한 모양으로 형성된 최악의 경우 \(O(n)\)의 시간 복잡도를 갖는다.

트리의 구현

이진 탐색 트리를 Python으로 구현하면 아래와 같다.

Incomplete

delete 메서드 버그 수정 필요

class ReprMixin:
    def __repr__(self) -> str:
        attrs = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{self.__class__.__name__}({attrs})"


class Node(ReprMixin):
    def __init__(self, value) -> None:
        self.value = value
        self._left: Node = None
        self._right: Node = None


class Tree(ReprMixin):
    def __init__(self, value) -> None:
        self.head = Node(value=value)

    def insert(self, value) -> None:
        ptr = self.head
        while True:
            if value < ptr.value:
                if ptr._left is not None:
                    ptr = ptr._left
                    continue
                else:
                    ptr._left = Node(value=value)
                    break
            elif value > ptr.value:
                if ptr._right is not None:
                    ptr = ptr._right
                    continue
                else:
                    ptr._right = Node(value=value)
                    break
            break

    def search(self, value) -> bool:
        ptr = self.head
        while ptr:
            if ptr.value == value:
                return True
            elif value < ptr.value:
                ptr = ptr._left
            else:
                ptr = ptr._right
        return False

    def delete(self, value) -> bool:
        is_exist = self.search(value=value)
        if not is_exist:
            return False

        ptr = self.head
        parent = self.head
        while ptr:
            if ptr.value == value:
                break
            elif value < ptr.value:
                parent = ptr
                ptr = ptr._left
            else:
                parent = ptr
                ptr = ptr._right

        # case 1. 리프 노드 삭제
        # 삭제할 노드의 부모 노드가 삭제할 노드를 가리키지 않도록 함
        if not ptr._left and not ptr._right:
            if value == parent.value:
                parent._left = None
            else:
                parent._right = None

        # case 2. 자식 노드가 하나인 노드 삭제
        # 삭제할 노드의 부모 노드가 삭제할 노드의 자식 노드를 가리키도록 함
        elif ptr._left and not ptr._right:
            if value < parent.value:
                parent._left = ptr._left
            else:
                parent._right = ptr._left
        elif not ptr._left and ptr._right:
            if value < parent.value:
                parent._left = ptr._right
            else:
                parent._right = ptr._right

        # case 3. 자식 노드가 두 개인 노드 삭제
        # 삭제할 노드의 오른쪽 자식 중 가장 작은 값이 삭제할 노드를 대체(구현된 방식)
        # 삭제할 노드의 왼쪽 자식 중 가장 큰 값이 삭제할 노드를 대체

        # 삭제할 노드의 부모 노드의 왼쪽 노드로 대체할 노드 할당
        # 대체할 노드의 왼쪽 노드에 삭제할 노드의 왼쪽 노드 할당
        # 대체할 노드의 오른쪽 노드가 존재할 경우 대체할 노드의 부모 노드의 왼쪽 노드로 할당

        elif ptr._left and ptr._right:
            # case 3-1. 삭제할 노드의 자식 노드가 두 개이고, 삭제할 노드가 부모 노드의 왼쪽 노드일 때
            if value < parent.value:
                target_node = ptr._right
                target_parent = ptr._right
                # 3-1-1. 삭제할 노드의 오른쪽 자식 중 가장 작은 값을 가진 노드의 자식 노드가 없을 때
                while ptr._left:
                    target_parent = target_node
                    target_node = target_node._left
                # 3-1-2. 삭제할 노드의 오른쪽 자식 중 가장 작은 값을 가진 노드가 오른쪽 노드를 갖고 있을 때
                if target_node._right:
                    target_parent._left = target_node._right
                else:
                    target_parent._left = None
                parent._left = target_node
                target_node._right = ptr._right
                target_node._left = ptr._left

            # case 3-2. 삭제할 노드의 자식 노드가 두 개이고, 삭제할 노드가 부모 노드의 오른쪽 노드일 때
            else:
                target_node = ptr._right
                target_parent = ptr._right
                # 3-2-1. 삭제할 노드의 오른쪽 자식 중 가장 작은 값을 가진 노드의 자식 노드가 없을 때
                while ptr._left:
                    target_parent = target_node
                    target_node = target_node._left
                # 3-2-2. 삭제할 노드의 오른쪽 자식 중 가장 작은 값을 가진 노드가 오른쪽 노드를 갖고 있을 때
                if target_node._right:
                    target_parent._left = target_node._right
                else:
                    target_parent._left = None
                parent._right = target_node
                target_node._right = ptr._right
                target_node._left = ptr._left
        return True