트리와 이진트리

트리(Tree) 개념

트리는 비선형 자료구조로, 계층적 관계를 표현하는데 최적화된 구조이다. 마치 나무가 뿌리에서 시작해 가지를 뻗어나가는 모습과 유사하여 ‘트리’라는 이름이 붙었다. 실생활에서는 회사의 조직도, 운영체제의 파일 시스템 등에서 트리 구조를 쉽게 찾아볼 수 있다

트리의 기본 용어

  • 노드(Node): 데이터를 저장하는 기본 단위
  • 엣지(Edge): 노드와 노드를 연결하는 선
  • 루트 노드(Root Node): 트리의 최상위 노드
  • 부모 노드(Parent Node): 특정 노드의 상위 노드
  • 자식 노드(Child Node): 특정 노드의 하위 노드
  • 터미널 노드(Terminal Node) 또는 리프 노드(Leaf Node): 자식이 없는 노드
  • 내부 노드(Internal Node): 자식이 하나 이상 있는 노드
  • 서브트리(Sub Tree): 트리 내부의 부분 트리

트리의 레벨과 높이

        A (레벨 1)
       / \
      B   C (레벨 2)
     / \ / \
    D  E F  G (레벨 3)
  • 레벨(Level): 루트 노드를 1로 시작하여, 자식으로 내려갈수록 1씩 증가
  • 높이(Height): 트리에서 가장 깊은 레벨의 값

위 예시에서 트리의 높이는 3이다

이진 트리(Binary Tree)

이진 트리는 각 노드가 최대 2개인 자식 노드만을 가질 수 있는 트리이다.

이진 트리의 조건

  • 각 노드는 0개, 1개 또는 2개의 자식을 가진다
  • 2개를 초과하는 자식을 가지면 이진 트리가 아니다
  • 한쪽 방향으로만 자식이 연결되어도 이진 트리이다

이진 트리의 종류

포화 이진 트리(Full Binary Tree)

모든 레벨이 완전히 채워진 트리이다. 마지막 레벨의 모든 터미널 노드가 꽉 차서 새로운 노드를 삽입할 수 없는 상태이다

        1
       / \
      2   3
     / \ / \
    4  5 6  7

완전 이진 트리(Complete Binary Tree)

마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있고, 마지막 레벨의 노드들은 왼쪽부터 순서대로 채워진 트리이다

        1
       / \
      2   3
     / \ /
    4  5 6
완전 이진 트리가 아닌 경우
  • 중간 레벨이 완전히 채워지지 않은 경우
  • 마지막 레벨이 왼쪽부터 연속적으로 채워지지 않은 경우

이진 트리 구현

BinaryTree 클래스

public class BinaryTree {
    private int data;
    private BinaryTree leftSubTree;
    private BinaryTree rightSubTree;
    
    // 생성자
    public BinaryTree(int data) {
        this(data, null, null);
    }
    
    public BinaryTree(int data, BinaryTree leftTree, BinaryTree rightTree) {
        this.data = data;
        this.leftSubTree = leftTree;
        this.rightSubTree = rightTree;
    }
    
    // Getter 메서드
    public int getData() {
        return this.data;
    }
    
    public BinaryTree getLeftSubTree() {
        return this.leftSubTree;
    }
    
    public BinaryTree getRightSubTree() {
        return this.rightSubTree;
    }
    
    // Setter 메서드
    public void setData(int data) {
        this.data = data;
    }
    
    public void setLeftSubTree(BinaryTree tree) {
        this.leftSubTree = tree;
    }
    
    public void setRightSubTree(BinaryTree tree) {
        this.rightSubTree = tree;
    }
}
설계 특징

이진 트리에서는 각 노드가 곧 트리이기 때문에, 별도의 Node 클래스 없이 BinaryTree 클래스 하나로 노드와 트리를 모두 표현한다

포화 이진 트리 생성 예제

public class BinaryTreeTest {
    public static void main(String[] args) {
        // 1부터 7까지의 노드 생성
        BinaryTree tree1 = new BinaryTree(1);
        BinaryTree tree2 = new BinaryTree(2);
        BinaryTree tree3 = new BinaryTree(3);
        BinaryTree tree4 = new BinaryTree(4);
        BinaryTree tree5 = new BinaryTree(5);
        BinaryTree tree6 = new BinaryTree(6);
        BinaryTree tree7 = new BinaryTree(7);
        
        // 트리 구조 연결
        tree1.setLeftSubTree(tree2);
        tree1.setRightSubTree(tree3);
        tree2.setLeftSubTree(tree4);
        tree2.setRightSubTree(tree5);
        tree3.setLeftSubTree(tree6);
        tree3.setRightSubTree(tree7);
        
        // 테스트 출력
        System.out.println("루트 노드의 오른쪽 자식: " + 
            tree1.getRightSubTree().getData());
        // 루트 노드의 오른쪽 자식: 3

        System.out.println("루트 노드의 오른쪽 자식의 왼쪽 자식: " + 
            tree1.getRightSubTree().getLeftSubTree().getData());
            // 루트 노드의 오른쪽 자식의 왼쪽 자식: 6
    }
}


트리 순회(Tree Traversal)

트리의 모든 노드를 체계적으로 방문하는 방법이다. 재귀를 활용하여 구현하며, 세 가지 방식이 있다

전위 순회(Pre-order Traversal)

방문 순서: 루트 → 왼쪽 서브트리 → 오른쪽 서브트리

전위 순회 동작 과정

        1
       / \
      2   3
     / \ / \
    4  5 6  7

단계별 방문:
1. 루트(1) 방문 → 출력: 1
2. 왼쪽 서브트리(2) 방문
   2-1. 루트(2) 방문 → 출력: 2
   2-2. 왼쪽(4) 방문 → 출력: 4
   2-3. 오른쪽(5) 방문 → 출력: 5
3. 오른쪽 서브트리(3) 방문
   3-1. 루트(3) 방문 → 출력: 3
   3-2. 왼쪽(6) 방문 → 출력: 6
   3-3. 오른쪽(7) 방문 → 출력: 7

결과: 1 → 2 → 4 → 5 → 3 → 6 → 7

전위 순회 구현

public void preOrderTraversal(BinaryTree tree) {
    if (tree == null) return;
    
    System.out.println(tree.getData());           // 루트 방문
    preOrderTraversal(tree.getLeftSubTree());     // 왼쪽 순회
    preOrderTraversal(tree.getRightSubTree());    // 오른쪽 순회
}

중위 순회(In-order Traversal)

방문 순서: 왼쪽 서브트리 → 루트 → 오른쪽 서브트리

중위 순회 동작 과정

        1
       / \
      2   3
     / \ / \
    4  5 6  7

단계별 방문:
1. 왼쪽 서브트리(2)로 이동
   1-1. 왼쪽(4)으로 이동 → 출력: 4
   1-2. 루트(2) 방문 → 출력: 2
   1-3. 오른쪽(5) 방문 → 출력: 5
2. 루트(1) 방문 → 출력: 1
3. 오른쪽 서브트리(3)로 이동
   3-1. 왼쪽(6) 방문 → 출력: 6
   3-2. 루트(3) 방문 → 출력: 3
   3-3. 오른쪽(7) 방문 → 출력: 7

결과: 4 → 2 → 5 → 1 → 6 → 3 → 7

중위 순회 구현

public void inOrderTraversal(BinaryTree tree) {
    if (tree == null) return;
    
    inOrderTraversal(tree.getLeftSubTree());      // 왼쪽 순회
    System.out.println(tree.getData());           // 루트 방문
    inOrderTraversal(tree.getRightSubTree());     // 오른쪽 순회
}

후위 순회(Post-order Traversal)

방문 순서: 왼쪽 서브트리 → 오른쪽 서브트리 → 루트

후위 순회 동작 과정

        1
       / \
      2   3
     / \ / \
    4  5 6  7

단계별 방문:
1. 왼쪽 서브트리(2)로 이동
   1-1. 왼쪽(4) 방문 → 출력: 4
   1-2. 오른쪽(5) 방문 → 출력: 5
   1-3. 루트(2) 방문 → 출력: 2
2. 오른쪽 서브트리(3)로 이동
   2-1. 왼쪽(6) 방문 → 출력: 6
   2-2. 오른쪽(7) 방문 → 출력: 7
   2-3. 루트(3) 방문 → 출력: 3
3. 루트(1) 방문 → 출력: 1

결과: 4 → 5 → 2 → 6 → 7 → 3 → 1

후위 순회 구현

public void postOrderTraversal(BinaryTree tree) {
    if (tree == null) return;
    
    postOrderTraversal(tree.getLeftSubTree());    // 왼쪽 순회
    postOrderTraversal(tree.getRightSubTree());   // 오른쪽 순회
    System.out.println(tree.getData());           // 루트 방문
}

재귀 동작 분석

중위 순회를 예로 들어 재귀 호출 스택을 단계별로 살펴보겠다

트리 구조:
        1
       / \
      2   3
     / \ / \
    4  5 6  7

중위 순회 재귀 호출 스택:

호출 스택                      출력
══════════════════════════════════════════
inOrder(1)                     
├─ inOrder(2)                  
│  ├─ inOrder(4)               
│  │  ├─ inOrder(null)         
│  │  ├─ print(4)              → 4
│  │  └─ inOrder(null)         
│  ├─ print(2)                 → 2
│  └─ inOrder(5)               
│     ├─ inOrder(null)         
│     ├─ print(5)              → 5
│     └─ inOrder(null)         
├─ print(1)                    → 1
└─ inOrder(3)                  
   ├─ inOrder(6)               
   │  ├─ inOrder(null)         
   │  ├─ print(6)              → 6
   │  └─ inOrder(null)         
   ├─ print(3)                 → 3
   └─ inOrder(7)               
      ├─ inOrder(null)         
      ├─ print(7)              → 7
      └─ inOrder(null)

완전한 BinaryTree 클래스 (순회 메서드 포함)

public class BinaryTree {
    private int data;
    private BinaryTree leftSubTree;
    private BinaryTree rightSubTree;
    
    public BinaryTree(int data) {
        this(data, null, null);
    }
    
    public BinaryTree(int data, BinaryTree leftTree, BinaryTree rightTree) {
        this.data = data;
        this.leftSubTree = leftTree;
        this.rightSubTree = rightTree;
    }
    
    public int getData() {
        return this.data;
    }
    
    public BinaryTree getLeftSubTree() {
        return this.leftSubTree;
    }
    
    public BinaryTree getRightSubTree() {
        return this.rightSubTree;
    }
    
    public void setData(int data) {
        this.data = data;
    }
    
    public void setLeftSubTree(BinaryTree tree) {
        this.leftSubTree = tree;
    }
    
    public void setRightSubTree(BinaryTree tree) {
        this.rightSubTree = tree;
    }
    
    // 전위 순회
    public void preOrderTraversal(BinaryTree tree) {
        if (tree == null) return;
        
        System.out.println(tree.getData());
        preOrderTraversal(tree.getLeftSubTree());
        preOrderTraversal(tree.getRightSubTree());
    }
    
    // 중위 순회
    public void inOrderTraversal(BinaryTree tree) {
        if (tree == null) return;
        
        inOrderTraversal(tree.getLeftSubTree());
        System.out.println(tree.getData());
        inOrderTraversal(tree.getRightSubTree());
    }
    
    // 후위 순회
    public void postOrderTraversal(BinaryTree tree) {
        if (tree == null) return;
        
        postOrderTraversal(tree.getLeftSubTree());
        postOrderTraversal(tree.getRightSubTree());
        System.out.println(tree.getData());
    }
}

실행 테스트

public class BinaryTreeTest {
    public static void main(String[] args) {
        // 포화 이진 트리 생성
        BinaryTree tree1 = new BinaryTree(1);
        BinaryTree tree2 = new BinaryTree(2);
        BinaryTree tree3 = new BinaryTree(3);
        BinaryTree tree4 = new BinaryTree(4);
        BinaryTree tree5 = new BinaryTree(5);
        BinaryTree tree6 = new BinaryTree(6);
        BinaryTree tree7 = new BinaryTree(7);
        
        tree1.setLeftSubTree(tree2);
        tree1.setRightSubTree(tree3);
        tree2.setLeftSubTree(tree4);
        tree2.setRightSubTree(tree5);
        tree3.setLeftSubTree(tree6);
        tree3.setRightSubTree(tree7);
        
        // 순회 테스트
        System.out.println("=== 전위 순회 ===");
        tree1.preOrderTraversal(tree1);
        // 출력: 1 2 4 5 3 6 7
        
        System.out.println("\n=== 중위 순회 ===");
        tree1.inOrderTraversal(tree1);
        // 출력: 4 2 5 1 6 3 7
        
        System.out.println("\n=== 후위 순회 ===");
        tree1.postOrderTraversal(tree1);
        // 출력: 4 5 2 6 7 3 1
    }
}

트리 자료구조의 활용

  • 이진 탐색 트리(BST): 효율적인 검색을 위한 정렬된 트리
  • 힙(Heap): 우선순위 큐 구현에 사용되는 완전 이진 트리
  • AVL 트리: 자가 균형 이진 탐색 트리
  • 레드-블랙 트리: 균형 잡힌 탐색 성능 보장

스택과 큐가 연결 리스트에 규칙을 추가하여 탄생한 것처럼, 이진 트리 역시 다양한 규칙과 조건을 추가하여 더 강력한 자료구조로 발전한다

핵심 정리

  • 트리는 계측적 구조를 표현하는 비선형 자료구조이다
  • 이진 트리는 각 노드가 최대 2개의 자식만 가지는 트리이다
  • 트리 순회는 재귀를 활용하여 효율적으로 구현할 수 있다
  • 순회 방식에 따라 방문 순서가 달라지며, 각각 다른 용도로 활용된다
  • 이진 트리는 다양한 고급 자료구조의 기반이 된다

인프런 강의 중 감자님의 강의 ‘그림으로 쉽게 배우는 자료구조와 알고리즘 (심화편)’ 강의 참고