트리(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개의 자식만 가지는 트리이다
- 트리 순회는 재귀를 활용하여 효율적으로 구현할 수 있다
- 순회 방식에 따라 방문 순서가 달라지며, 각각 다른 용도로 활용된다
- 이진 트리는 다양한 고급 자료구조의 기반이 된다