본문 바로가기

CS/자료구조

자료구조 - 트리

C언어로 쉽게 풀어쓴 자료구조를 읽으며 정리


트리

트리는 데이터의 계층적 관계를 표현하는 비선형적 자료구조이다.
노드와 간선으로 이루어져 있으며 사이클을 허용하지 않는 그래프의 한 종류이다.
 

트리 - 구성요소

  • 노드 : 트리를 구성하는 각 요소
  • 간선 : 트리를 구성하기 위해 노드와 노드를 연결하는 선
  • 루트 노드 : 트리 구조에서 최상위 노드
  • 단말 노드, 리프 노드 (Terminal Node, Leaf Node) : 하위에 다른 노드가 연결되어 있지 않은 노드
  • 내부 노드, 비단말 노드(Internal Node) : 단말 노드를 제외한 모든 노드

 

이진 트리

https://ko.wikipedia.org/wiki/이진_트리

루트노드를 중심으로 최대 두 개의 서브 트리로 나눠진다.

  • 나눠진 두 서브 트리 모두 이진 트리이다.
  • 서브 트리는 공집합일 수도 있다. (공집합도 이진 트리이다.)

 
트리의 각 층 별로 숫자를 매겨 이를 레벨(level)이라고 한다.

  • 레벨은 0(또는 1)부터 시작하게 되고 리프 노드의 레벨을 가리켜 높이라고 얘기한다.
  • 루트 노드의 레벨은 0이다.

 
트리의 최고 레벨을 가리켜 해당 트리의 높이(height) 라고 한다.

  • 높이가 h인 이진트리의 경우 노드 개수는 최소 h개, 최대 2^h-1 개이다.
  • 따라서 n개의 노드를 가지는 이진트리의 최대 높이는 최대 n, 최소 floor(log2(n+1)) 이다.

이진 트리 - 분류

포화 이진트리 (Perfect Binary Tree)

  • 모든 레벨이 꽉 찬 이진 트리
  • 높이가 h이면 항상 2^h-1 의 노드 개수

 
완전 이진트리 (Complete Binary Tree)

  • 위에서 아래로, 왼쪽에서 오른쪽 순서대로 차곡차곡 채워진 이진트리
  • 높이가 h이면 노드의 개수는 2^(h-1) ~ 2^h -1

 
정 이진트리 (Full Binary Tree)

  • 모든 노드가 0개 혹은 2개의 자식 노드만을 갖는 이진트리

 

이진 트리 - 배열로 표현

힙을 구현할 때 완전 이진 트리 구조를 사용하게 되는데 일반적으로 배열을 이용하여 구현한다.

  • 새로운 노드를 힙의 마지막 위치에 추가해야 하는데 이 때 배열을 사용하는 것이 더 효율적

 
구현 아이디어

  • 모든 이진트리를 포화 이진트리라고 가정하고 배열의 크기를 정한다.
  • 루트 노드의 위치를 배열의 1번 인덱스로 설정
    • 루트가 1번에서 시작해야 i번째 노드에 대하여 부모 노드는 i/2, 왼쪽 노드는 2i, 오른쪽 노드는 2i+1의 인덱스를 가짐
  • 왼쪽 서브 트리의 루트 노드의 위치를 2i
  • 오른쪽 서브트리의 루트 노드를 2i + 1

C언어로 쉽게 풀어쓴 자료구조 261p

 
 

이진 트리 - 연결 리스트로 표현

참조 값을 이용하여 부모 노드가 자식 노드를 가리키게 하는 방법이다.

  • 노드는 클래스로 표현하고 링크는 참조 값으로 표현
  • Double Linked List로 완전 이진 트리를 구현할 수도 있음

C언어로 쉽게 풀어쓴 자료구조 262p

class Node<T> {
    var value: T
    var left: Node?
    var right: Node?

    init(value: T) {
        self.value = value
        self.left = nil
        self.right = nil
    }
}

class BinaryTree<T> {
    
    private(set) var root: Node<T>?
    
    init() { }
    
    ...
}

이진트리 - 순회

전위 순회

  • 자신, 왼쪽, 오른쪽 순서로 방문
  • 계층적인 구조를 표현하고 싶을  때 활용하기 좋음 ex. 구조화된 문서 출력

 
중위 순회

  • 왼쪽, 자신 오른쪽 순으로 방문
  • 수식 트리를 중위 순위로 표현 > 인간에게 익숙한 표현 식
  • 노드의 크기 순서대로 방문할 필요가 있을 때

후위 순회

  • 왼쪽 오른쪽 자신 순으로 방문
  • 자식 노드를 먼저 방문할 필요가 있는 경우
  • 하위 폴더 용량의 계산
  • 수식 트리의 계산 : 비단말 노드를 방문할 때 양쪽 서브트리의 값을 노드에 저장된 연산자를 이용하여 계산

레벨 순회

  • 각 노드들을 낮은 레벨 부터 높은 레벨까지 순회
  • 다른 트리 순회 방법들이 스택을 사용했던 것에 비해 레벨 순회는 큐를 사용하는 순회 방법
class BinaryTree<T> {
    
    private(set) var root: Node<T>?
    
    init() { }
    
    func preorder(_ node: Node<T>?) {
        guard let value = node?.value else { return }
        
        print(value)
        self.preorder(node?.left)
        self.preorder(node?.right)
    }
    
    func inorder(_ node: Node<T>?) {
        guard let value = node?.value else { return }
        
        self.inorder(node?.left)
        print(value)
        self.inorder(node?.right)
    }
    
    func postorder(_ node: Node<T>?) {
        guard let value = node?.value else { return }
        
        self.postorder(node?.left)
        self.postorder(node?.right)
        print(value)
    }
    
    func levelOrder(_ node: Node<T>?) {
        guard let node else { return }
        
        // https://liam777.tistory.com/25 에서 구현한 Queue 사용
        var queue: LinearQueue<Node<T>> = .init()
        queue.enqueue(node)
        
        while !queue.isEmpty {
            guard let node = queue.dequeue() else { continue }
            print(node.value)
            
            if let left = node.left {
                queue.enqueue(left)
            }
            
            if let right = node.right {
                queue.enqueue(right)
            }
        }
    }
    
    ...
}

BST(Binary Search Tree)

이진 탐색 트리는 효율적인 탐색을 위해 고안한 자료구조이다.
 
데이터 저장 규칙

  • 이진 탐색 트리의 노드에 저장된 키는 유일함
  • 부모의 키가 왼쪽 자식 노드의 키 보다 항상 큼
  • 부모의 키가 오른쪽 자식 노드의 키보다 항상 작음
  • 왼쪽과 오른쪽 서브 트리도 이진 탐색 트리임

시간 복잡도

  • 조회에 대한 O(logn)의 평균 시간 복잡도
  • 엄밀히 얘기하자면 O(h)가 맞음 (트리의 높이 h)
  • 이진 탐색 트리가 한 쪽으로 편향된다면 (worst case) 시간 복잡도는 O(n)
  • 이러한 문제점을 해결하기 위해 Rebalancing 기법들이 등장
  • 트리의 구조를 재조정 (AVL 트리, Red-Black Tree, ...)
class Node<T: Comparable> {
    var value: T
    var left: Node?
    var right: Node?

    init(value: T) {
        self.value = value
        self.left = nil
        self.right = nil
    }
}

class BinarySearchTree<T: Comparable> {
    
    private(set) var root: Node<T>?
    
    init() { }
    
    func search(value: T, _ node: Node<T>?) -> Node<T>? {
        guard let node else { return nil }
        
        if value == node.value {
            return node
        } else if value < node.value {
            return self.search(value: value, node.left)
        } else {
            return self.search(value: value, node.right)
        }
    }
    ...
}

 

삽입 연산

  • 해당 데이터를 트리에서 탐색한다
  • 탐색이 실패한 위치가 바로 새로운 노드 삽입 위치가 된다

C언어로 쉽게 풀어쓴 자료구조 286p

class BinarySearchTree<T: Comparable> {

    ...
    
    func insert(value: T) {
        self.root = self._insert(value: value, self.root)
    }
    
    private func _insert(value: T, _ node: Node<T>?) -> Node<T> {
        guard let node else {
            return Node(value: value)
        }
        
        if value == node.value {
            // do nothing
        } else if value < node.value {
            node.left = self._insert(value: value, node.left)
        } else {
            node.right = self._insert(value: value, node.right)
        }
        
        return node
    }
    
    ...
}

 

삭제 연산
 
세 가지 케이스를 고려해야 함
 
1. 삭제하려는 노드가 단말 노드인 경우 (서브 트리가 0개)

  • 단말 노드의 부모 노드를 찾아서 연결을 끊는다.

C언어로 쉽게 풀어쓴 자료구조 293p

 
 
 
2. 삭제하려는 노드가 하나의 서브 트리만 갖고 있는 경우

  • 해당 노드를 삭제하고 서브 트리는 모두 부모 노드에 붙여준다.

 

C언어로 쉽게 풀어쓴 자료구조 293p

 
3. 삭제하려는 노드가 두 개의 서브 트리를 갖고 있는 경우

  • 왼쪽 서브트리의 가장 큰 값과 오른쪽 서브 트리에서 가장 작은 값 중 하나를 삭제할 노드의 위치로 가져 온다.

C언어로 쉽게 풀어쓴 자료구조 294p

 

class BinarySearchTree<T: Comparable> {
    
    ...
    
    func delete(value: T) {
        self.root = self._delete(value: value, self.root)
    }
    
    private func _delete(value: T, _ node: Node<T>?) -> Node<T>? {
        guard let node else { return nil }
        
        if value == node.value {
            guard let right = node.right, let _ = node.left else {
                // 한쪽 자식만 있는 경우
                if let left = node.left, node.right == nil {
                    return left
                }
                if let right = node.right, node.left == nil {
                    return right
                }
                
                // 노드에 자식이 없는 경우
                return nil
            }
            
            // 두 자식이 있는 경우
            let minAtRight = self.findMin(right)
            node.value = minAtRight.value
            node.right = self._delete(value: minAtRight.value, node.right)
        }
        
        if value < node.value {
            node.left = self._delete(value: value, node.left)
        }
        
        if value > node.value {
            node.right = self._delete(value: value, node.right)
        }
        
        return node
    }
    
    private func findMin(_ node: Node<T>) -> Node<T> {
        var currentNode = node
        while let left = currentNode.left {
            currentNode = left
        }
        return currentNode
    }
    ...
}

 

BST 전체 코드

class Node<T: Comparable> {
    var value: T
    var left: Node?
    var right: Node?

    init(value: T) {
        self.value = value
        self.left = nil
        self.right = nil
    }
}

class BinarySearchTree<T: Comparable> {
    
    private(set) var root: Node<T>?
    
    init() { }
    
    func search(value: T, _ node: Node<T>?) -> Node<T>? {
        guard let node else { return nil }
        
        if value == node.value {
            return node
        } else if value < node.value {
            return self.search(value: value, node.left)
        } else {
            return self.search(value: value, node.right)
        }
    }
    
    func insert(value: T) {
        self.root = self._insert(value: value, self.root)
    }
    
    private func _insert(value: T, _ node: Node<T>?) -> Node<T> {
        guard let node else {
            return Node(value: value)
        }
        
        if value == node.value {
            // do nothing
        } else if value < node.value {
            node.left = self._insert(value: value, node.left)
        } else {
            node.right = self._insert(value: value, node.right)
        }
        
        return node
    }
    
    func delete(value: T) {
        self.root = self._delete(value: value, self.root)
    }
    
    private func _delete(value: T, _ node: Node<T>?) -> Node<T>? {
        guard let node else { return nil }
        
        if value == node.value {
            guard let right = node.right, let _ = node.left else {
                // 한쪽 자식만 있는 경우
                if let left = node.left, node.right == nil {
                    return left
                }
                if let right = node.right, node.left == nil {
                    return right
                }
                
                // 노드에 자식이 없는 경우
                return nil
            }
            
            // 두 자식이 있는 경우
            let minAtRight = self.findMin(right)
            node.value = minAtRight.value
            node.right = self._delete(value: minAtRight.value, node.right)
        }
        
        if value < node.value {
            node.left = self._delete(value: value, node.left)
        }
        
        if value > node.value {
            node.right = self._delete(value: value, node.right)
        }
        
        return node
    }
    
    private func findMin(_ node: Node<T>) -> Node<T> {
        var currentNode = node
        while let left = currentNode.left {
            currentNode = left
        }
        return currentNode
    }
    
    func preorder(_ node: Node<T>?) {
        guard let value = node?.value else { return }
        
        print(value)
        self.preorder(node?.left)
        self.preorder(node?.right)
    }
    
    func inorder(_ node: Node<T>?) {
        guard let value = node?.value else { return }
        
        self.inorder(node?.left)
        print(value)
        self.inorder(node?.right)
    }
    
    func postorder(_ node: Node<T>?) {
        guard let value = node?.value else { return }
        
        self.postorder(node?.left)
        self.postorder(node?.right)
        print(value)
    }
    
    func levelOrder(_ node: Node<T>?) {
        guard let node else { return }
        
        // https://liam777.tistory.com/25 에서 구현한 Queue 사용
        var queue: LinearQueue<Node<T>> = .init()
        queue.enqueue(node)
        
        while !queue.isEmpty {
            guard let node = queue.dequeue() else { continue }
            print(node.value)
            
            if let left = node.left {
                queue.enqueue(left)
            }
            
            if let right = node.right {
                queue.enqueue(right)
            }
        }
    }
}

struct LinearQueue<Element> {
    private var _queue: [Element] = []

    var isEmpty: Bool { return self._queue.isEmpty }
    var count: Int { return self._queue.count }

    mutating func enqueue(_ element: Element) {
        self._queue.append(element)
    }

    mutating func dequeue() -> Element? {
        return self.isEmpty ? nil : self._queue.removeFirst()
    }
}

 

'CS > 자료구조' 카테고리의 다른 글

자료구조 - Heap (2) 정렬  (2) 2024.09.16
자료구조 - Heap (1) 기본  (3) 2024.09.02
자료구조 - 스택, 큐, 덱  (0) 2024.08.17
자료 구조 - 배열, 연결 리스트  (1) 2024.08.14
자료 구조 - 순환 (Recursion)  (1) 2024.08.12