작성일 :

트리란?

트리(Tree)는 계층적 구조를 표현하는 비선형 자료구조입니다.

하나의 루트 노드에서 시작하여 여러 자식 노드로 뻗어나가는 형태입니다.

그래프 관점에서는 사이클이 없는 연결 그래프로 정의할 수 있습니다.


컴퓨터의 파일 시스템을 생각해보면 이해가 쉽습니다.

최상위에 루트 폴더가 있고, 그 아래에 여러 폴더와 파일이 있습니다.

각 폴더는 다시 하위 폴더와 파일을 가질 수 있습니다.


파일 시스템 외에도 회사의 조직도, HTML 문서의 DOM 구조, 가계도 등이 트리 구조입니다.



트리 용어

트리를 다루기 위해 알아야 할 기본 용어들입니다.

1
2
3
4
5
        A          ← 루트(Root): 최상위 노드
       / \
      B   C        ← A의 자식(Child)
     /|   |\
    D E   F G      ← 리프(Leaf): 자식이 없는 노드


노드 관계

용어 설명
루트(Root) 트리의 최상위 노드. 부모가 없음
부모(Parent) 특정 노드의 바로 위 노드
자식(Child) 특정 노드의 바로 아래 노드
형제(Sibling) 같은 부모를 가진 노드들
리프(Leaf) 자식이 없는 노드 (단말 노드)
내부 노드 자식이 있는 노드 (리프가 아닌 노드)
조상(Ancestor) 루트까지 경로에 있는 모든 노드
자손(Descendant) 특정 노드의 서브트리에 있는 모든 노드


구조 용어

용어 설명
간선(Edge) 노드와 노드를 연결하는 선
경로(Path) 한 노드에서 다른 노드까지 거치는 노드들의 순서
깊이(Depth) 루트에서 특정 노드까지의 간선 수 (루트의 깊이는 0)
높이(Height) 특정 노드에서 가장 먼 리프까지의 간선 수
레벨(Level) 같은 깊이에 있는 노드들의 집합
서브트리 특정 노드를 루트로 하는 부분 트리


트리의 속성

트리는 사이클이 없는 연결 그래프로 볼 수 있기 때문에, n 개의 노드를 가진 트리는 항상 n - 1 개의 간선을 가집니다.

또한, 임의의 두 노드 사이에는 유일한 경로가 존재하게 됩니다.



이진 트리 (Binary Tree)

이진 트리는 각 노드가 최대 2개의 자식을 가지는 트리입니다.

왼쪽 자식(Left Child)과 오른쪽 자식(Right Child)으로 구분합니다.


이진 트리는 가장 기본적이면서도 중요한 트리 형태입니다.

이진 탐색 트리, 힙, 세그먼트 트리 등 많은 자료구조가 이진 트리를 기반으로 합니다.


이진 트리의 종류

정 이진 트리 (Full Binary Tree)

모든 노드가 0개 또는 2개의 자식을 가집니다. 자식이 1개인 노드가 없습니다.

1
2
3
4
5
      A
     / \
    B   C
   / \
  D   E


완전 이진 트리 (Complete Binary Tree)

마지막 레벨을 제외한 모든 레벨이 완전히 채워져 있고, 마지막 레벨은 왼쪽부터 순서대로 채워집니다.

배열로 구현하기에 적합합니다.

1
2
3
4
5
      A
     / \
    B   C
   / \  /
  D  E F


포화 이진 트리 (Perfect Binary Tree)

모든 레벨이 완전히 채워진 이진 트리입니다. 높이가 h인 포화 이진 트리는 2^(h+1) - 1개의 노드를 가집니다.

1
2
3
4
5
      A
     / \
    B   C
   /\   /\
  D  E F  G



이진 트리 순회 (Traversal)

트리의 모든 노드를 특정 순서로 방문하는 방법입니다. 순회 방식에 따라 노드 방문 순서가 달라집니다.


전위 순회 (Preorder)

루트 → 왼쪽 서브트리 → 오른쪽 서브트리 순서로 방문합니다.

루트를 가장 먼저 방문하기 때문에 전위(前位)라고 합니다.

1
2
3
4
5
6
7
8
void preorder(Node* node) {
  if (node == nullptr)
    return;

  cout << node->val << " ";  // 1. 루트 방문
  preorder(node->left);       // 2. 왼쪽 서브트리
  preorder(node->right);      // 3. 오른쪽 서브트리
}


활용: 트리를 복사하거나, 전위 표기식을 출력할 때 사용합니다.


중위 순회 (Inorder)

왼쪽 서브트리 → 루트 → 오른쪽 서브트리 순서로 방문합니다.

이진 탐색 트리(BST)에서 중위 순회를 하면 오름차순 정렬 결과를 얻습니다.

1
2
3
4
5
6
7
8
void inorder(Node* node) {
  if (node == nullptr)
    return;

  inorder(node->left);        // 1. 왼쪽 서브트리
  cout << node->val << " ";  // 2. 루트 방문
  inorder(node->right);       // 3. 오른쪽 서브트리
}


활용: 이진 탐색 트리에서 정렬된 순서로 원소를 출력할 때 사용합니다.


후위 순회 (Postorder)

왼쪽 서브트리 → 오른쪽 서브트리 → 루트 순서로 방문합니다.

자식을 모두 처리한 후 부모를 처리합니다.

1
2
3
4
5
6
7
8
void postorder(Node* node) {
  if (node == nullptr)
    return;

  postorder(node->left);      // 1. 왼쪽 서브트리
  postorder(node->right);     // 2. 오른쪽 서브트리
  cout << node->val << " ";  // 3. 루트 방문
}


활용: 트리를 삭제하거나, 서브트리의 결과를 먼저 계산해야 할 때 사용합니다.


레벨 순회 (Level Order)

레벨별로 왼쪽에서 오른쪽으로 방문합니다.

BFS(너비 우선 탐색) 를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void levelOrder(Node* root) {
  if (root == nullptr)
    return;

  queue<Node*> q;
  q.push(root);

  while (!q.empty()) {
    Node* node = q.front();
    q.pop();

    cout << node->val << " ";

    if (node->left)
      q.push(node->left);
    if (node->right)
      q.push(node->right);
  }
}


순회 예시

1
2
3
4
5
      1
     / \
    2   3
   / \
  4   5
순회 방식 방문 순서 설명
전위 1 → 2 → 4 → 5 → 3 루트 먼저
중위 4 → 2 → 5 → 1 → 3 왼쪽, 루트, 오른쪽
후위 4 → 5 → 2 → 3 → 1 루트 마지막
레벨 1 → 2 → 3 → 4 → 5 위에서 아래로



트리 구현

포인터 기반 구현

동적으로 노드를 생성하여 연결합니다. 일반적인 이진 트리 구현에 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Node {
  int val;
  Node* left;
  Node* right;

  Node(int v) : val(v), left(nullptr), right(nullptr) {}
};

// 트리 생성 예시
Node* root = new Node(1);
root->left = new Node(2);
root->right = new Node(3);
root->left->left = new Node(4);
root->left->right = new Node(5);


배열 기반 구현

완전 이진 트리는 배열로 효율적으로 표현할 수 있습니다.

힙과 세그먼트 트리에서 주로 사용됩니다.


1-based indexing을 사용할 때 인덱스 규칙은 다음과 같습니다.

루트는 인덱스 1이고, 노드 i의 왼쪽 자식은 2×i, 오른쪽 자식은 2×i+1, 부모는 i/2입니다.

1
2
3
4
5
6
7
int tree[100];  // 인덱스 1부터 사용

tree[1] = 1;    // 루트
tree[2] = 2;    // 루트의 왼쪽 자식
tree[3] = 3;    // 루트의 오른쪽 자식
tree[4] = 4;    // 노드 2의 왼쪽 자식
tree[5] = 5;    // 노드 2의 오른쪽 자식


인접 리스트 구현

자식의 수가 정해지지 않은 일반 트리에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <bits/stdc++.h>
using namespace std;

typedef vector<int> vi;

vi children[100];  // children[i]: 노드 i의 자식들

void addChild(int parent, int child) {
  children[parent].push_back(child);
}

// 트리 순회
void dfs(int node) {
  cout << node << " ";
  for (int child : children[node])
    dfs(child);
}



트리의 활용

이진 탐색 트리 (BST)

왼쪽 서브트리 < 루트 < 오른쪽 서브트리 조건을 만족하는 트리입니다.

탐색, 삽입, 삭제가 평균 O(log n) 에 가능합니다. 다만, 한쪽으로 치우친 트리가 되면 O(n)까지 느려질 수 있어서, 균형 잡힌 트리(AVL, Red-Black Tree 등)가 사용됩니다.


힙 (Heap)

부모가 자식보다 항상 크거나(최대 힙) 작은(최소 힙) 완전 이진 트리입니다.

우선순위 큐 구현에 사용되며, 삽입과 삭제가 \(O(\log n)\)에 가능합니다.

참고 : 힙 정렬 - soo:bak


트라이 (Trie)

문자열 집합을 저장하는 트리입니다.

문자열 검색이 \(O(\text{문자열 길이})\)에 가능하며, 자동완성이나 사전 구현 등에 활용됩니다.


세그먼트 트리

구간 쿼리를 처리하는 트리입니다.

구간 합, 구간 최솟값/최댓값 등을 \(O(\log n)\)에 구할 수 있습니다.

참고 : 세그먼트 트리 - soo:bak



트리 문제 풀이 팁

재귀로 생각하기

트리 문제는 대부분 재귀로 풀 수 있습니다.

현재 노드에서 해야 할 일을 정의하고, 자식 노드에 대해 같은 함수를 호출합니다.

1
2
3
4
5
6
7
8
9
10
// 트리의 높이 구하기
int getHeight(Node* node) {
  if (node == nullptr)
    return -1;  // 빈 트리의 높이는 -1

  int leftHeight = getHeight(node->left);
  int rightHeight = getHeight(node->right);

  return max(leftHeight, rightHeight) + 1;
}


부모 정보 저장하기

루트에서 시작하는 문제가 아니라면, 부모 노드 정보를 저장해두면 편리합니다.

1
2
3
4
5
6
7
8
9
int parent[100];

void dfs(int node, int par) {
  parent[node] = par;
  for (int child : children[node]) {
    if (child != par)
      dfs(child, node);
  }
}



마무리

트리는 계층적 데이터를 표현하는 자료구조입니다.


순회 방식 순서 활용
전위 루트 → 왼쪽 → 오른쪽 트리 복사, 전위 표기식
중위 왼쪽 → 루트 → 오른쪽 BST 정렬 출력
후위 왼쪽 → 오른쪽 → 루트 트리 삭제, 서브트리 계산
레벨 위에서 아래로 BFS 기반 탐색


트리 문제는 재귀적 사고가 중요합니다.

현재 노드에서 무엇을 하고, 자식에게 무엇을 위임할지 생각하면 대부분의 문제를 풀 수 있습니다.


관련 문제