트리 (Tree)
프로그래밍에서 트리(Tree)는 계층적 구조를 나타내는 자료구조로, 노드와 노드를 연결하는 간선으로 구성된다. 트리는 파일 시스템, 조직도, 의사결정 트리, 검색 트리 등 다양한 분야에서 활용된다.
트리를 이해하면 계층적 데이터를 효율적으로 관리하고 복잡한 문제를 체계적으로 해결할 수 있다. 하나씩 살펴보자.
트리의 정의와 특징
트리는 노드(Node)와 간선(Edge)으로 구성된 비선형 자료구조로, 하나의 루트 노드(Root Node)에서 시작해 계층적으로 연결된다. 트리는 사이클(Cycle)이 없는 연결 그래프이며, 각 노드는 부모 노드와 자식 노드로 계층적 관계를 형성한다.
트리의 주요 특징은 다음과 같다:
- 계층 구조: 루트에서 시작해 부모-자식 관계로 데이터를 조직화한다.
- 단일 경로: 두 노드 간에는 유일한 경로가 존재한다.
- 효율적 탐색: 이진 검색 트리 같은 경우 O(log n) 시간에 탐색이 가능하다.
단점은 다음과 같다:
- 복잡한 구현: 노드 간 연결 관리와 균형 조정이 필요하다.
- 메모리 사용: 포인터나 참조를 사용해 메모리 소모가 크다.
- 불균형 문제: 트리가 한쪽으로 치우치면 성능이 저하된다.
트리의 종류
트리는 구조와 특성에 따라 여러 가지로 나뉜다. 아래는 주요 트리 종류와 도식이다.
1. 이진 트리(Binary Tree)
각 노드가 최대 두 개의 자식 노드를 가지는 트리다.
[Root] / \ [Left] [Right]
2. 이진 검색 트리(Binary Search Tree, BST)
왼쪽 서브트리의 값은 부모보다 작고, 오른쪽 서브트리의 값은 부모보다 크다.
[5] / \ [3] [7] / \ [1] [4]
3. AVL 트리
각 노드의 좌우 서브트리 높이 차가 1 이하인 균형 이진 검색 트리다.
4. 레드-블랙 트리(Red-Black Tree)
노드에 색상(빨강/검정)을 추가해 균형을 유지하는 트리다.
5. 힙(Heap)
완전 이진 트리로, 부모 노드가 자식 노드보다 크거나(최대 힙) 작다(최소 힙).
[10] / \ [5] [7] / \ [2] [3]
본 포스팅은 이진 트리와 이진 검색 트리를 중심으로 다룬다.
자바에서 이진 트리 구현
이진 트리는 노드와 좌우 자식 노드로 구성된다. 아래는 자바로 구현한 이진 트리다.
class BinaryTree {
class Node {
int data;
Node left;
Node right;
Node(int data) {
this.data = data;
left = right = null;
}
}
private Node root;
public BinaryTree() {
root = null;
}
// 노드 삽입 (이진 검색 트리 기준)
public void insert(int data) {
root = insertRec(root, data);
}
private Node insertRec(Node root, int data) {
if (root == null) {
root = new Node(data);
return root;
}
if (data < root.data) {
root.left = insertRec(root.left, data);
} else if (data > root.data) {
root.right = insertRec(root.right, data);
}
return root;
}
// 전위 순회 (Preorder Traversal)
public void preorder() {
preorderRec(root);
System.out.println();
}
private void preorderRec(Node root) {
if (root != null) {
System.out.print(root.data + " ");
preorderRec(root.left);
preorderRec(root.right);
}
}
}
이 코드는 이진 검색 트리로, 삽입과 전위 순회 기능을 포함한다.
트리의 기본 연산
트리의 주요 연산은 다음과 같다:
1. 삽입(Insert)
새로운 노드를 적절한 위치에 추가한다.
private Node insertRec(Node root, int data) {
if (root == null) {
root = new Node(data);
return root;
}
if (data < root.data) {
root.left = insertRec(root.left, data);
} else if (data > root.data) {
root.right = insertRec(root.right, data);
}
return root;
}
2. 삭제(Delete)
노드를 제거하고 트리의 구조를 유지한다.
private Node deleteRec(Node root, int data) {
if (root == null) return root;
if (data < root.data) {
root.left = deleteRec(root.left, data);
} else if (data > root.data) {
root.right = deleteRec(root.right, data);
} else {
if (root.left == null) return root.right;
else if (root.right == null) return root.left;
root.data = minValue(root.right);
root.right = deleteRec(root.right, root.data);
}
return root;
}
private int minValue(Node root) {
int min = root.data;
while (root.left != null) {
min = root.left.data;
root = root.left;
}
return min;
}
3. 순회(Traversal)
트리를 순회하는 방법은 전위, 중위, 후위 순회가 있다.
public void inorder() {
inorderRec(root);
System.out.println();
}
private void inorderRec(Node root) {
if (root != null) {
inorderRec(root.left);
System.out.print(root.data + " ");
inorderRec(root.right);
}
}
4. 검색(Search)
특정 값을 가진 노드를 찾는다.
public boolean search(int data) {
return searchRec(root, data);
}
private boolean searchRec(Node root, int data) {
if (root == null || root.data == data) return root != null;
if (data < root.data) return searchRec(root.left, data);
return searchRec(root.right, data);
}
트리의 시간 복잡도
이진 검색 트리의 주요 연산 시간 복잡도는 다음과 같다:
- 삽입(Insert): 평균 O(log n), 최악 O(n)
- 삭제(Delete): 평균 O(log n), 최악 O(n)
- 검색(Search): 평균 O(log n), 최악 O(n)
- 순회(Traversal): O(n)
불균형 트리에서는 최악의 경우 O(n)이 되므로 AVL 트리나 레드-블랙 트리로 균형을 유지해야 한다.
트리를 활용한 알고리즘
트리는 다양한 알고리즘에 활용된다.
1. 이진 검색 트리 탐색
값을 빠르게 검색한다.
public boolean bstSearch(int data) {
Node current = root;
while (current != null) {
if (current.data == data) return true;
if (data < current.data) current = current.left;
else current = current.right;
}
return false;
}
2. 트리 순회
전위, 중위, 후위 순회로 노드를 방문한다.
public void postorder() {
postorderRec(root);
System.out.println();
}
private void postorderRec(Node root) {
if (root != null) {
postorderRec(root.left);
postorderRec(root.right);
System.out.print(root.data + " ");
}
}
3. 트리의 높이 계산
트리의 최대 깊이를 계산한다.
public int height() {
return heightRec(root);
}
private int heightRec(Node root) {
if (root == null) return 0;
int leftHeight = heightRec(root.left);
int rightHeight = heightRec(root.right);
return Math.max(leftHeight, rightHeight) + 1;
}
심화 주제: 트리 균형과 최적화
트리의 효율성을 높이려면 균형과 최적화 기법을 적용해야 한다.
1. AVL 트리 균형
노드 삽입/삭제 후 좌우 서브트리 높이 차를 1 이하로 유지한다.
class AVLTree {
class Node {
int data, height;
Node left, right;
Node(int data) {
this.data = data;
height = 1;
left = right = null;
}
}
private int height(Node node) {
if (node == null) return 0;
return node.height;
}
private int balanceFactor(Node node) {
if (node == null) return 0;
return height(node.left) - height(node.right);
}
private Node rightRotate(Node y) {
Node x = y.left;
Node T2 = x.right;
x.right = y;
y.left = T2;
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
return x;
}
}
2. 레드-블랙 트리
색상과 규칙을 통해 균형을 유지한다.
3. 세그먼트 트리
구간 쿼리를 효율적으로 처리한다.
class SegmentTree {
private int[] tree;
private int n;
public SegmentTree(int[] arr) {
n = arr.length;
tree = new int[4 * n];
build(arr, 0, 0, n - 1);
}
private void build(int[] arr, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
} else {
int mid = (start + end) / 2;
build(arr, 2 * node + 1, start, mid);
build(arr, 2 * node + 2, mid + 1, end);
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
}
}
트리의 한계와 대안
트리는 불균형 문제와 메모리 소모가 크다. 자바에서는 java.util.TreeMap
과 java.util.TreeSet
을 제공한다.
import java.util.TreeMap;
TreeMap<Integer, Integer> map = new TreeMap<>();
map.put(1, 100);
map.put(2, 200);
int value = map.get(1); // 100
TreeMap
은 레드-블랙 트리로 구현되어 균형을 유지한다.
문제
트리를 활용한 알고리즘을 연습하기 위해 난이도별 문제를 제시하고, 자바 코드로 해답을 제공한다.
난이도 하: 이진 검색 트리 삽입과 순회
시간 제한: 1 초 | 메모리 제한: 256 MB
정수들을 이진 검색 트리에 삽입한 뒤 중위 순회 결과를 출력한다.
입력
첫째 줄에 정수 개수 N (1 ≤ N ≤ 100,000)이 주어진다. 다음 N줄에 삽입할 정수 x (-1,000 ≤ x ≤ 1,000)가 주어진다.
출력
중위 순회 결과를 공백으로 구분해 출력한다.
입력 예시
5
3
1
4
2
5
출력 예시
1 2 3 4 5
문제 출처: 자체 제작
해답
해답 펼치기
import java.util.Scanner;
public class BSTTraversal {
static class BinaryTree {
static class Node {
int data;
Node left, right;
Node(int data) {
this.data = data;
left = right = null;
}
}
Node root;
void insert(int data) {
root = insertRec(root, data);
}
Node insertRec(Node root, int data) {
if (root == null) {
root = new Node(data);
return root;
}
if (data < root.data) {
root.left = insertRec(root.left, data);
} else if (data > root.data) {
root.right = insertRec(root.right, data);
}
return root;
}
void inorder() {
inorderRec(root);
System.out.println();
}
void inorderRec(Node root) {
if (root != null) {
inorderRec(root.left);
System.out.print(root.data + " ");
inorderRec(root.right);
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
BinaryTree bst = new BinaryTree();
for (int i = 0; i < N; i++) {
int x = sc.nextInt();
bst.insert(x);
}
bst.inorder();
sc.close();
}
}
난이도 중: 트리의 높이 계산
이진 트리의 노드 정보를 받아 트리의 높이를 계산한다.
입력
첫째 줄에 노드 수 N (1 ≤ N ≤ 100,000)이 주어진다. 다음 N줄에 각 노드의 값과 왼쪽, 오른쪽 자식 노드 인덱스(-1은 자식 없음)가 주어진다.
출력
트리의 높이를 출력한다.
입력 예시
5
1 2 3
2 4 -1
3 -1 5
4 -1 -1
5 -1 -1
출력 예시
3
문제 출처: 자체 제작
해답
해답 펼치기
import java.util.Scanner;
public class TreeHeight {
static class Node {
int data, leftIdx, rightIdx;
Node(int data, int leftIdx, int rightIdx) {
this.data = data;
leftIdx = leftIdx;
rightIdx = rightIdx;
}
}
static int height(Node[] nodes, int idx) {
if (idx == -1) return 0;
int leftHeight = height(nodes, nodes[idx].leftIdx);
int rightHeight = height(nodes, nodes[idx].rightIdx);
return Math.max(leftHeight, rightHeight) + 1;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
Node[] nodes = new Node[N];
for (int i = 0; i < N; i++) {
int data = sc.nextInt();
int left = sc.nextInt();
int right = sc.nextInt();
nodes[i] = new Node(data, left, right);
}
System.out.println(height(nodes, 0));
sc.close();
}
}
난이도 상: 세그먼트 트리 구간 합
배열의 구간 합을 빠르게 계산하고 업데이트를 처리한다.
입력
첫째 줄에 배열 크기 N (1 ≤ N ≤ 100,000)과 쿼리 수 M (1 ≤ M ≤ 100,000)이 주어진다. 둘째 줄에 N개의 정수가 주어진다. 다음 M줄에 쿼리가 주어진다. 쿼리는 “0 i v” (i번째 값을 v로 업데이트) 또는 “1 l r” (l부터 r까지 합 계산)이다.
출력
각 구간 합 쿼리의 결과를 한 줄에 출력한다.
입력 예시
5 3
1 2 3 4 5
1 1 3
0 2 10
1 1 3
출력 예시
6
14
문제 출처: 백준 온라인 저지 "구간 합 구하기"
해답
해답 펼치기
import java.util.Scanner;
public class SegmentTreeSum {
static class SegmentTree {
long[] tree;
int n;
SegmentTree(int[] arr) {
n = arr.length;
tree = new long[4 * n];
build(arr, 0, 0, n - 1);
}
void build(int[] arr, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
} else {
int mid = (start + end) / 2;
build(arr, 2 * node + 1, start, mid);
build(arr, 2 * node + 2, mid + 1, end);
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
}
void update(int idx, int val, int node, int start, int end) {
if (start == end) {
tree[node] = val;
} else {
int mid = (start + end) / 2;
if (start <= idx && idx <= mid) {
update(idx, val, 2 * node + 1, start, mid);
} else {
update(idx, val, 2 * node + 2, mid + 1, end);
}
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
}
long query(int l, int r, int node, int start, int end) {
if (r < start || end < l) return 0;
if (l <= start && end <= r) return tree[node];
int mid = (start + end) / 2;
long leftSum = query(l, r, 2 * node + 1, start, mid);
long rightSum = query(l, r, 2 * node + 2, mid + 1, end);
return leftSum + rightSum;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int M = sc.nextInt();
int[] arr = new int[N];
for (int i = 0; i < N; i++) {
arr[i] = sc.nextInt();
}
SegmentTree segTree = new SegmentTree(arr);
for (int i = 0; i < M; i++) {
int type = sc.nextInt();
if (type == 0) {
int idx = sc.nextInt() - 1;
int val = sc.nextInt();
segTree.update(idx, val, 0, 0, N - 1);
} else {
int l = sc.nextInt() - 1;
int r = sc.nextInt() - 1;
System.out.println(segTree.query(l, r, 0, 0, N - 1));
}
}
sc.close();
}
}
마무리
트리는 계층적 데이터를 관리하는 강력한 자료구조로, 이진 트리, 이진 검색 트리, AVL 트리, 세그먼트 트리 등을 다뤘다. 삽입, 삭제, 순회, 검색 등의 연산과 시간 복잡도, 최적화 기법, 활용 사례를 살펴봤다. 자바의 TreeMap
과 TreeSet
을 통해 구현의 편리함을 확인했다. 문제를 통해 트리의 동작을 익혔으니, 이를 바탕으로 더 복잡한 문제를 해결해보자.
'코딩 공부 > 알고리즘(자바)' 카테고리의 다른 글
기본 자료구조 - 우선순위 큐 (Priority Queue) (2) | 2025.06.03 |
---|---|
기본 자료구조 - 맵 (Map) (2) | 2025.05.14 |
기본 자료구조 - 집합 (Set) (0) | 2025.05.09 |
기본 자료구조 - 해시 테이블 (Hash Table) (0) | 2025.05.08 |
기본 자료구조 - 덱 (Deque) (3) | 2025.05.06 |