[Data Structure] Tree


Tree 트리


  • 일반적으로 대상 정보의 각 항목들을 계층적으로 연관되도록 구조화시키고자 할 때 사용하는 비선형 자료구조
  • Node와 Branch를 이용해서, 사이클을 이루지 않도록 구성한 데이터 구조
  • 한 개의 루트 노드만이 존재하며, 모든 자식 노드는 한 개의 부모 노드만을 가짐 (즉, 루트에서 어떤 노드로 가는 경로는 유일)
  • 보통 이진 트리(Binary Tree) 형태의 구조로, 탐색(검색) 알고리즘 구현을 위해 많이 사용됨


트리 관련 용어

  • 노드 (Node) : 트리에서 데이터를 저장하는 기본 요소 (데이터와 다른 연결된 노드에 대한 Branch 정보 포함)
  • 간선 (Branch) : 노드를 연결하는 선 (edge, link라고도 부름)
  • 내부 노드 (Internal Node) : 말단 노드가 아닌 노드
  • 루트 노드 (Root Node) : 트리 맨 위에 있는 노드
  • 말단 노드 (Leaf Node) : 자식 노드가 하나도 없는 노드 (단말 노드, 잎 노드)
  • 부모 노드 (Parent Node) : 어떤 노드의 다음 레벨에 연결된 노드
  • 자식 노드 (Child Node) : 어떤 노드의 하위 레벨에 연결된 노드
  • 형제 노드 (Brother Node) : 동일한 부모 노드를 가진 노드 (Sibling)
  • 노드의 레벨 (Level): 최상위 노드를 Level 0으로 하였을 때, 하위 Branch로 연결된 노드의 깊이를 나타냄
  • 노드의 크기 (Size) : 자신을 포함한 모든 자손 노드의 개수
  • 노드의 깊이 (Depth) : 트리에서 노드가 가질 수 있는 최대 레벨
  • 노드의 높이 (height) : 루트 노드에서 가장 깊숙히 있는 노드의 깊이


출처 : https://www.fun-coding.org/Chapter10-tree.html
트리 관련 용어 정리

트리의 종류

  • 이진 트리 (Binary Tree) : 노드의 최대 Branch가 2인 트리
  • 완전 이진 트리 (Complete Binary Tree) : 노드를 삽입할 때 왼쪽부터 차례대로 추가하는 이진 트리
  • 전 이진 트리 (Full Binary Tree) : 모든 노드가 0개 또는 2개의 자식 노드를 갖는 트리
  • 포화 이진 트리 (Perfect Binary Tree) : 모든 내부 노드가 두 개의 자식 노드를 가지며 모든 잎 노드가 동일한 깊이 또는 레벨을 갖는 트리
  • 편향 트리 (Skewed Tree) : 한쪽으로 기울어진 트리, 사향 트리라고도 부름


출처 : https://velog.io/@adam2/TREE
이진 트리의 종류

이진 탐색 트리 (Binary Search Tree, BST)

왼쪽 노드는 해당 노드보다 작거나 같은 값, 오른쪽 노드는 해당 노드보다 큰 값을 가지는 추가적인 조건이 있는 이진 트리

출처 : https://www.mathwarehouse.com/programming/gifs/binary-search-tree.php#binary-search-tree-insertion-node
이진 탐색 트리

이진 탐색 트리의 시간 복잡도

  • 트리의 높이를 h라고 표현 한다면, O(h)의 시간 복잡도를 가짐
  • n개의 노드를 가진다면, h = logn에 가까우므로 시간 복잡도는 O(logn)

장점

  • 기존의 O(n)의 탐색 속도를 O(logn)으로 개선할 수 있음
출처 : https://www.mathwarehouse.com/programming/gifs/binary-search-tree.php#binary-search-tree-insertion-node
이진 탐색 트리와 정렬된 배열간의 탐색 속도 비교

단점

  • 균형이 잡혀 있을 때 기준 시간복잡도는 O(logn)이지만 편향 트리에서는 링크드 리스트등과 동일한 성능을 보여줄 수도 있음
  • 이를 해결하기 위해 균형 이진 탐색 트리를 활용 함
출처 : https://www.fun-coding.org/Chapter10-tree.html
편향 트리



# 노드 클래스 만들기
class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

class BST:
    def __init__(self, head):
        self.head = head
    # 데이터 삽입
    def insert(self, value):
        self.current_node = self.head
        while True:
            if value < self.current_node.value:
                if self.current_node.left != None:
                    self.current_node = self.current_node.left
                else:
                    self.current_node.left = Node(value)
                    break
            else:
                if self.current_node.right != None:
                    self.current_node = self.current_node.right
                else:
                    self.current_node.right = Node(value)
                    break
    # 데이터 탐색
    def search(self, value):
        self.current_node = self.head
        while self.current_node:
            if self.current_node.value == value:
                return True
            elif value < self.current_node.value:
                self.current_node = self.current_node.left
            else:
                self.current_node = self.current_node.right
        return False        
    #데이터 삭제
    def delete(self, value):
        # 삭제할 노드 탐색
        searched = False
        self.current_node = self.head
        self.parent = self.head
        while self.current_node:
            if self.current_node.value == value:
                searched = True
                break
            elif value < self.current_node.value:
                self.parent = self.current_node
                self.current_node = self.current_node.left
            else:
                self.parent = self.current_node
                self.current_node = self.current_node.right

        if searched == False:
            return False    

        # case1 : 삭제할 노드가 Leaf Node일 경우
        if  self.current_node.left == None and self.current_node.right == None:
            if value < self.parent.value:
                self.parent.left = None
            else:
                self.parent.right = None
        
        # case2 : 삭제할 노드가 Child Node를 한 개 가지고 있을 경우
        elif self.current_node.left != None and self.current_node.right == None:
            if value < self.parent.value:
                self.parent.left = self.current_node.left
            else:
                self.parent.right = self.current_node.left
        elif self.current_node.left == None and self.current_node.right != None:
            if value < self.parent.value:
                self.parent.left = self.current_node.right
            else:
                self.parent.right = self.current_node.right        
        
        # case 3 - 삭제할 노드가 Child Node를 두개 가지고 있을 경우
        elif self.current_node.left != None and self.current_node.right != None:
            # case3-1 : 삭제할 Node가 Parent Node 왼쪽에 있을 때
            if value < self.parent.value:
                self.change_node = self.current_node.right
                self.change_node_parent = self.current_node.right
                while self.change_node.left != None:
                    self.change_node_parent = self.change_node
                    self.change_node = self.change_node.left
                if self.change_node.right != None:
                    self.change_node_parent.left = self.change_node.right
                else:
                    self.change_node_parent.left = None
                self.parent.left = self.change_node
                self.change_node.right = self.current_node.right
                self.change_node.left = self.change_node.left
            # case 3-2 : 삭제할 Node가 Parent Node 오른쪽에 있을 때

            else:
                self.change_node = self.current_node.right
                self.change_node_parent = self.current_node.right
                while self.change_node.left != None:
                    self.change_node_parent = self.change_node
                    self.change_node = self.change_node.left
                if self.change_node.right != None:
                    self.change_node_parent.left = self.change_node.right
                else:
                    self.change_node_parent.left = None
                self.parent.right = self.change_node
                self.change_node.right = self.current_node.right
                self.change_node.left = self.current_node.left

        return True
이진 탐색 트리를 파이썬으로 구현



균형 이진 탐색 트리 (Balanced BST)

평균적으로 O(logn)의 연산 속도를 가진 이진 검색 트리가 편향 트리가 되었을 때 O(n)의 연산 속도를 가지는 것을 방지하기 위해 만들어진 자료구조

Red-Black 트리

  • 아래 5가지 조건을 가지는 이진 탐색 트리
    1. 트리의 모든 노드는 Red or Black으로만 구성되어 있음
    2. 루트 노드는 무조건 Black
    3. 모든 리프 노드는 무조건 Black
    4. 루트 노드에서 리프 노드까지 Black의 갯수는 항상 같음
    5. Red 노드의 자식은 모두 Black (Black 노드의 자식은 상관 없음)
  • 자바에서는 treeSet, treeMap이 Red-Black 트리를 구현함

5번 조건으로 인해 Red는 중복될 수 없으니 Red와 Red 사이엔 하나의 블랙 노드를 끼게 되고, 이는 곧 Red-Black 트리의 총 깊이가 됨
결국 블랙 노드만을 가지는 가장 짧은 깊이와 Red-Black의 차이는 무조건 2배 이하의 길이 차이가 남

AVL 트리

  • 모든 노드에 대해서 노드의 왼쪽부 트리와 오른쪽부 트리의 높이 차가 1 이하로 맞춘 이진 탐색 트리
  • -1, 0, 1로 이루어진 Balance Factor를 기준으로 Rotation이 이루어져 균형을 맞춤

Red-Black vs AVL

  • 더 엄격한 균형을 유지하고 있는 AVL이 Red-Black보다 더 빠른 Search 속도를 제공
  • 더 느슨한 균형을 유지하고 있는 Red-Black이 AVL보다 더 빠른 Insert와 Delete 속도를 제공
  • Red-Black은 대부분의 언어 라이브러리에서 자주 사용
  • AVL은 조회에 자주 사용되는 Database에 자주 사용



참고

잔재미코딩
신찬수 교수님 유튜브
위키 백과






© 2021. by hminkim