인공지능 개발자 수다(유튜브 바로가기) 자세히보기

파이썬/코딩테스트

[자료구조] 트리

Suya_03 2024. 5. 17. 01:45

목차

    반응형

     

    1. 트리의 기본 개념

    • 노드(Node): 트리의 기본 구성 요소로, 데이터를 포함.
    • 루트(Root): 트리의 최상위 노드.
    • 간선(Edge): 두 노드를 연결하는 선.
    • 부모 노드(Parent Node): 다른 노드를 가리키는 노드.
    • 자식 노드(Child Node): 부모 노드에 의해 가리켜지는 노드.
    • 리프 노드(Leaf Node): 자식 노드가 없는 노드.
    • 서브트리(Subtree): 트리의 일부로, 특정 노드를 루트로 하는 트리.
    • 깊이(Depth): 루트 노드에서 특정 노드까지의 경로 길이.
    • 높이(Height): 특정 노드에서 리프 노드까지의 가장 긴 경로.

    2. 트리의 종류

    모든 트리의 종류를 다 공부하기에는 양이 많으니, 중요한 트리만 보고 넘어가기로 하자.

    • 이진 탐색 트리(Binary Search Tree): 기본적인 트리 자료구조로, 삽입, 삭제, 탐색 연산에 익숙해지는 것이 중요
    • 이진 힙(Binary Heap): 우선순위 큐 문제와 힙 정렬에 자주 등장
    • 트라이(Trie): 문자열 관련 문제에서 자주 등장
    • 세그먼트 트리(Segment Tree): 고급 문제에서 구간 쿼리 처리를 위해 알아두면 좋음

     

    4. 이진 탐색 트리(Binary Search Tree) 

    4.1. 설명

    이진 탐색 트리(Binary Search Tree, BST)는 이진 트리의 일종으로, 각 노드가 최대 두 개의 자식을 가지며, 특정 속성을 만족하는 트리다. 이 속성은 각 노드의 왼쪽 서브트리에는 그 노드의 값보다 작은 값들을 가지는 노드들로만 구성되고, 오른쪽 서브트리에는 그 노드의 값보다 큰 값들을 가지는 노드들로만 구성된다. 이 속성을 통해 BST는 효율적인 탐색, 삽입, 삭제 연산을 가능하게 함.

     

    주요 연산은 아래와 같다.

    • 탐색(Search): 주어진 값이 트리에 있는지 확인하는 연산.
    • 삽입(Insertion): 트리에 새로운 값을 추가하는 연산.
    • 삭제(Deletion): 트리에서 특정 값을 제거하는 연산.
    • 순회(Traversal): 트리의 모든 노드를 특정 순서로 방문하는 연산 (전위, 중위, 후위, 레벨 순서 등).

    4.2. 주요 특성

    • 왼쪽 서브트리의 값 < 현재 노드의 값
    • 오른쪽 서브트리의 값 > 현재 노드의 값
    • 각 서브트리도 BST여야 한다

    4.3. 트리 순회 (Tree Traversal)

    트리 순회(Tree Traversal)는 트리 자료구조의 모든 노드를 일정한 순서로 방문하는 과정을 의미한다.

    • 전위 순회 (Pre-order Traversal)
      • 순서: 루트 -> 왼쪽 서브트리 -> 오른쪽 서브트리
      • 사용 예: 트리 구조를 복제하거나, 표현식을 전위 표기법으로 변환할 때 사용.
    • 중위 순회 (In-order Traversal)
      • 순서: 왼쪽 서브트리 -> 루트 -> 오른쪽 서브트리
      • 사용 예: 이진 탐색 트리에서 중위 순회를 하면 오름차순으로 정렬된 결과를 얻을 수 있다.
    • 후위 순회 (Post-order Traversal)
      • 순서: 왼쪽 서브트리 -> 오른쪽 서브트리 -> 루트
      • 사용 예: 트리 구조를 삭제하거나, 표현식을 후위 표기법으로 변환할 때 사용.
    • 레벨 순서 순회 (Level-order Traversal)
      • 순서: 루트에서 시작하여 같은 레벨의 노드를 왼쪽에서 오른쪽 순서로 방문
      • 사용 예: 트리의 레벨별 노드 방문이 필요할 때 사용.
    class Node:
        def __init__(self, key):
            self.left = None
            self.right = None
            self.val = key
    
    class BinaryTree:
        def __init__(self):
            self.root = None  # 트리의 루트 노드를 초기화
    
        # 트리에 새로운 키를 삽입하는 메서드
        def insert(self, key):
            if self.root is None:
                self.root = Node(key)  # 루트 노드가 없으면 새로운 노드를 루트로 설정
            else:
                self._insert(self.root, key)  # 그렇지 않으면 재귀적으로 삽입
    
        # 재귀적으로 노드를 삽입하는 헬퍼 메서드
        def _insert(self, current_node, key):
            if key < current_node.val:
                if current_node.left is None:
                    current_node.left = Node(key)  # 왼쪽 자식 노드가 없으면 새로운 노드를 삽입
                else:
                    self._insert(current_node.left, key)  # 왼쪽 자식 노드가 있으면 재귀적으로 삽입
            else:
                if current_node.right is None:
                    current_node.right = Node(key)  # 오른쪽 자식 노드가 없으면 새로운 노드를 삽입
                else:
                    self._insert(current_node.right, key)  # 오른쪽 자식 노드가 있으면 재귀적으로 삽입
    
        # 전위 순회(Pre-order traversal) 메서드
        def preorder_traversal(self, node):
            if node:
                print(node.val, end=" ")  # 현재 노드를 출력
                self.preorder_traversal(node.left)  # 왼쪽 서브트리를 재귀적으로 순회
                self.preorder_traversal(node.right)  # 오른쪽 서브트리를 재귀적으로 순회
    
        # 중위 순회(In-order traversal) 메서드
        def inorder_traversal(self, node):
            if node:
                self.inorder_traversal(node.left)  # 왼쪽 서브트리를 재귀적으로 순회
                print(node.val, end=" ")  # 현재 노드를 출력
                self.inorder_traversal(node.right)  # 오른쪽 서브트리를 재귀적으로 순회
    
        # 후위 순회(Post-order traversal) 메서드
        def postorder_traversal(self, node):
            if node:
                self.postorder_traversal(node.left)  # 왼쪽 서브트리를 재귀적으로 순회
                self.postorder_traversal(node.right)  # 오른쪽 서브트리를 재귀적으로 순회
                print(node.val, end=" ")  # 현재 노드를 출력
    
        # 레벨 순서 순회(Level-order traversal) 메서드
        def level_order_traversal(self, node):
            if not node:
                return
            from collections import deque  # 큐 자료구조를 사용하기 위해 deque 임포트
            queue = deque([node])  # 루트 노드를 큐에 추가
            while queue:
                current_node = queue.popleft()  # 큐에서 노드를 꺼냄
                print(current_node.val, end=" ")  # 현재 노드를 출력
                if current_node.left:
                    queue.append(current_node.left)  # 왼쪽 자식 노드를 큐에 추가
                if current_node.right:
                    queue.append(current_node.right)  # 오른쪽 자식 노드를 큐에 추가
    
        # 트리에서 특정 값을 탐색하는 메서드
        def search(self, key):
            return self._search(self.root, key)
    
        # 재귀적으로 노드를 탐색하는 헬퍼 메서드
        def _search(self, current_node, key):
            if current_node is None or current_node.val == key:
                return current_node
            if key < current_node.val:
                return self._search(current_node.left, key)
            return self._search(current_node.right, key)
    
        # 트리에서 특정 값을 삭제하는 메서드
        def delete(self, key):
            self.root = self._delete(self.root, key)
    
        # 재귀적으로 노드를 삭제하는 헬퍼 메서드
        def _delete(self, current_node, key):
            if current_node is None:
                return current_node
            if key < current_node.val:
                current_node.left = self._delete(current_node.left, key)
            elif key > current_node.val:
                current_node.right = self._delete(current_node.right, key)
            else:
                if current_node.left is None:
                    return current_node.right
                elif current_node.right is None:
                    return current_node.left
                min_larger_node = self._get_min(current_node.right)
                current_node.val = min_larger_node.val
                current_node.right = self._delete(current_node.right, min_larger_node.val)
            return current_node
    
        # 오른쪽 서브트리에서 가장 작은 값을 가진 노드를 찾는 메서드
        def _get_min(self, node):
            current = node
            while current.left is not None:
                current = current.left
            return current
    
        # 전위 순회를 시작하는 메서드
        def print_preorder(self):
            self.preorder_traversal(self.root)
    
        # 중위 순회를 시작하는 메서드
        def print_inorder(self):
            self.inorder_traversal(self.root)
    
        # 후위 순회를 시작하는 메서드
        def print_postorder(self):
            self.postorder_traversal(self.root)
    
        # 레벨 순서 순회를 시작하는 메서드
        def print_level_order(self):
            self.level_order_traversal(self.root)
    
    # 트리 생성 및 사용 예제
    bt = BinaryTree()  # 이진 트리 객체 생성
    bt.insert(10)  # 노드 삽입
    bt.insert(5)
    bt.insert(20)
    bt.insert(3)
    bt.insert(7)
    bt.insert(15)
    bt.insert(25)
    
    print("전위 순회 결과:")
    bt.print_preorder()  # 전위 순회 결과 출력
    print("\n중위 순회 결과:")
    bt.print_inorder()  # 중위 순회 결과 출력
    print("\n후위 순회 결과:")
    bt.print_postorder()  # 후위 순회 결과 출력
    print("\n레벨 순서 순회 결과:")
    bt.print_level_order()  # 레벨 순서 순회 결과 출력
    
    # 탐색
    print("\n탐색 결과:")
    node = bt.search(15)
    if node:
        print(f"노드 {node.val}를 찾았습니다.")
    else:
        print("노드를 찾지 못했습니다.")
    
    # 삭제
    bt.delete(20)
    print("\n삭제 후 중위 순회 결과:")
    bt.print_inorder()  # 삭제 후 중위 순회 결과 출력

     

    5. 이진 힙(Binary Heap) 

    5.1. 설명

    이진 힙(Binary Heap)은 완전 이진 트리의 일종으로, 힙 속성을 만족하는 자료구조

    5.2. 힙의 종류

    • 최소 힙(Min-Heap)
      • 특징: 부모 노드의 값이 자식 노드의 값보다 작거나 같다.
      • 루트 노드: 트리의 최상위 노드는 최솟값을 가진다.
    • 최대 힙(Max-Heap)
      • 특징: 부모 노드의 값이 자식 노드의 값보다 크거나 같다.
      • 루트 노드: 트리의 최상위 노드는 최댓값을 가진다.

    5.3. 주요 연산

    • 삽입(insert)
    • 최솟값/최댓값 추출(Extract)
    • 최솟값/최댓값 조회(Get)

    아래는 최소 힙을 구한 예제 코드이다.

    class BinaryHeap:
        def __init__(self):
            self.heap = []  # 힙을 리스트로 표현
    
        def insert(self, key):
            self.heap.append(key)  # 새로운 키를 힙의 끝에 추가
            self._heapify_up(len(self.heap) - 1)  # 새로운 키를 힙 속성에 맞게 조정
    
        def _heapify_up(self, index):
            parent_index = (index - 1) // 2
            if index > 0 and self.heap[index] < self.heap[parent_index]:
                self.heap[index], self.heap[parent_index] = self.heap[parent_index], self.heap[index]
                self._heapify_up(parent_index)  # 재귀적으로 힙 속성을 만족하도록 조정
    
        def extract_min(self):
            if len(self.heap) == 0:
                return None
            if len(self.heap) == 1:
                return self.heap.pop()
            root = self.heap[0]
            self.heap[0] = self.heap.pop()  # 힙의 마지막 요소를 루트로 이동
            self._heapify_down(0)  # 힙 속성을 만족하도록 조정
            return root
    
        def _heapify_down(self, index):
            smallest = index
            left_child_index = 2 * index + 1
            right_child_index = 2 * index + 2
            if left_child_index < len(self.heap) and self.heap[left_child_index] < self.heap[smallest]:
                smallest = left_child_index
            if right_child_index < len(self.heap) and self.heap[right_child_index] < self.heap[smallest]:
                smallest = right_child_index
            if smallest != index:
                self.heap[index], self.heap[smallest] = self.heap[smallest], self.heap[index]
                self._heapify_down(smallest)  # 재귀적으로 힙 속성을 만족하도록 조정
    
        def get_min(self):
            if len(self.heap) == 0:
                return None
            return self.heap[0]
    
        def print_heap(self):
            print(self.heap)
    
    # 이진 힙 생성 및 사용 예제
    bh = BinaryHeap()  # 이진 힙 객체 생성
    
    # 요소 삽입
    bh.insert(10)
    bh.insert(5)
    bh.insert(20)
    bh.insert(3)
    bh.insert(7)
    bh.insert(15)
    bh.insert(25)
    
    print("힙의 요소들:")
    bh.print_heap()  # 힙의 요소 출력
    
    # 최소값 추출
    print("\n최소값 추출:")
    print(bh.extract_min())  # 최소값 추출 및 출력
    print(bh.extract_min())  # 두 번째 최소값 추출 및 출력
    
    print("\n최소값 추출 후 힙의 요소들:")
    bh.print_heap()  # 추출 후 힙의 요소 출력

     

    6. 트라이(Trie)

    6.1. 설명

    트라이(Trie)는 주로 문자열을 저장하고 효율적으로 검색하기 위해 사용하는 트리 기반의 자료구조. 트라이는 각 노드가 문자열의 한 문자를 나타내며, 노드를 따라 내려가면서 문자열을 구성함. 트라이는 특히 많은 문자열을 저장하고, 특정 문자열이나 접두사로 시작하는 문자열을 빠르게 검색할 때 유용함.

     

    6.2. 주요 연산

    • 삽입(insert)
    • 검색(search)
    • 접두사 검색(Prefix Search)
    class TrieNode:
        def __init__(self):
            self.children = {}  # 자식 노드를 저장하는 딕셔너리
            self.is_end_of_word = False  # 단어의 끝을 표시하는 플래그
    
    class Trie:
        def __init__(self):
            self.root = TrieNode()  # 트라이의 루트 노드 초기화
    
        # 단어를 트라이에 삽입하는 메서드
        def insert(self, word):
            current_node = self.root
            for char in word:
                if char not in current_node.children:
                    current_node.children[char] = TrieNode()
                current_node = current_node.children[char]
            current_node.is_end_of_word = True  # 단어의 끝을 표시
    
        # 단어가 트라이에 존재하는지 확인하는 메서드
        def search(self, word):
            current_node = self.root
            for char in word:
                if char not in current_node.children:
                    return False
                current_node = current_node.children[char]
            return current_node.is_end_of_word
    
        # 주어진 프리픽스로 시작하는 단어가 트라이에 있는지 확인하는 메서드
        def starts_with(self, prefix):
            current_node = self.root
            for char in prefix:
                if char not in current_node.children:
                    return False
                current_node = current_node.children[char]
            return True
    
        # 트라이의 현재 상태를 출력하는 메서드 (디버깅 용도)
        def print_trie(self, node=None, word=""):
            if node is None:
                node = self.root
            if node.is_end_of_word:
                print(word)
            for char, child_node in node.children.items():
                self.print_trie(child_node, word + char)
    
    # 트라이 생성 및 사용 예제
    trie = Trie()  # 트라이 객체 생성
    
    # 단어 삽입
    trie.insert("hello")
    trie.insert("hell")
    trie.insert("heaven")
    trie.insert("heavy")
    
    print("트라이의 단어들:")
    trie.print_trie()  # 트라이의 단어들 출력
    
    # 단어 검색
    print("\n단어 'hell' 검색 결과:")
    print(trie.search("hell"))  # 단어 'hell'이 있는지 검색
    
    print("\n단어 'hello' 검색 결과:")
    print(trie.search("hello"))  # 단어 'hello'가 있는지 검색
    
    print("\n단어 'heav' 검색 결과:")
    print(trie.search("heav"))  # 단어 'heav'가 있는지 검색
    
    # 접두사 검색
    print("\n접두사 'he'로 시작하는 단어가 있는지 확인:")
    print(trie.starts_with("he"))  # 접두사 'he'로 시작하는 단어가 있는지 확인
    
    print("\n접두사 'hea'로 시작하는 단어가 있는지 확인:")
    print(trie.starts_with("hea"))  # 접두사 'hea'로 시작하는 단어가 있는지 확인
    
    print("\n접두사 'hi'로 시작하는 단어가 있는지 확인:")
    print(trie.starts_with("hi"))  # 접두사 'hi'로 시작하는 단어가 있는지 확인

     

    7. 세그먼트 트리(Segment Tree)

    7.1. 설명

    세그먼트 트리(Segment Tree)는 배열의 특정 구간에 대한 질의를 빠르게 처리하기 위한 자료구조다. 주로 구간 합, 구간 최솟값/최댓값, 구간 곱 등을 계산하는 데 사용.

     

    7.2. 구성

    • 리프 노드(Leaf Nodes): 배열의 각 요소를 나타내는 트리의 가장 아래 노드.
    • 내부 노드(Internal Nodes): 각 구간의 합 또는 다른 연산 결과를 저장하는 노드.

    7.3. 주요 연산

    • 구간 합 쿼리(Range Sum Query): 배열의 특정 구간의 합을 계산
    • 구간 업데이트(Update): 배열의 특정 인덱스의 값을 변경하고, 이 변경을 반영하기 위해 세그먼트 트리를 업데이트
    class SegmentTree:
        def __init__(self, data):
            self.n = len(data)
            self.tree = [0] * (2 * self.n)
            self.build(data)
    
        def build(self, data):
            # 리프 노드를 배열의 값으로 초기화
            for i in range(self.n):
                self.tree[self.n + i] = data[i]
            # 내부 노드를 초기화
            for i in range(self.n - 1, 0, -1):
                self.tree[i] = self.tree[i * 2] + self.tree[i * 2 + 1]
    
        def update(self, index, value):
            # 리프 노드 업데이트
            pos = index + self.n
            self.tree[pos] = value
            # 부모 노드 업데이트
            while pos > 1:
                pos //= 2
                self.tree[pos] = self.tree[2 * pos] + self.tree[2 * pos + 1]
    
        def range_query(self, left, right):
            # 쿼리 구간을 [left, right)로 조정
            left += self.n
            right += self.n
            result = 0
            while left < right:
                if left % 2:
                    result += self.tree[left]
                    left += 1
                if right % 2:
                    right -= 1
                    result += self.tree[right]
                left //= 2
                right //= 2
            return result
    
        def print_tree(self):
            print("리프 노드:", self.tree[self.n:self.n*2])  # 리프 노드 출력
            print("내부 노드:", self.tree[1:self.n])  # 내부 노드 출력
    
    # 세그먼트 트리 생성 및 사용 예제
    data = [1, 3, 5, 7, 9, 11]
    seg_tree = SegmentTree(data)  # 세그먼트 트리 객체 생성
    
    print("초기 세그먼트 트리 상태:")
    seg_tree.print_tree()  # 세그먼트 트리의 초기 상태 출력
    
    # 구간 합 쿼리
    print("\n구간 합 쿼리 [1, 5):")
    print(seg_tree.range_query(1, 5))  # 구간 [1, 5)의 합 출력
    
    # 업데이트
    seg_tree.update(2, 10)  # 인덱스 2의 값을 10으로 업데이트
    
    print("\n업데이트 후 세그먼트 트리 상태:")
    seg_tree.print_tree()  # 업데이트 후 세그먼트 트리 상태 출력
    
    # 업데이트 후 구간 합 쿼리
    print("\n구간 합 쿼리 [1, 5) 업데이트 후:")
    print(seg_tree.range_query(1, 5))  # 구간 [1, 5)의 합 출력
    반응형