컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
ds · 05

이진 탐색 트리

자료구조탐색 트리이진 탐색

이진 탐색 트리(BST)는 모든 노드가 "왼쪽 서브트리 < 현재 노드 < 오른쪽 서브트리" 조건을 만족하는 이진 트리입니다. 이 정렬 조건 덕분에 탐색·삽입·삭제 연산을 평균 O(log n)에 수행할 수 있으며, 정렬된 순서로 데이터를 순회할 수 있습니다.

핵심 개념

BST 조건

모든 노드 N에 대해 N의 왼쪽 서브트리에 있는 모든 값은 N보다 작고, 오른쪽 서브트리에 있는 모든 값은 N보다 큽니다. 이 조건이 재귀적으로 모든 서브트리에도 성립해야 합니다. 중복 값을 허용하지 않는 것이 일반적입니다.

핵심 개념

삽입 (Insert)

루트부터 시작해 삽입 값과 현재 노드를 비교합니다. 작으면 왼쪽, 크면 오른쪽으로 이동하며, null을 만나는 위치에 새 노드를 추가합니다. 비교는 트리 높이만큼만 수행하므로 키가 고르게 들어오는 평균적인 경우 O(log n)입니다.

핵심 개념

탐색 (Search)

삽입과 동일한 경로로 타깃 값을 찾습니다. 현재 노드보다 작으면 왼쪽, 크면 오른쪽으로 이동합니다. 값을 찾거나 null에 도달하면 종료합니다. 이진 탐색과 동일한 원리로 평균 O(log n)입니다.

핵심 개념

삭제 3케이스

① 리프 노드: 단순 제거. ② 자식이 1개: 삭제 노드의 부모와 자식을 직접 연결. ③ 자식이 2개: 중위 후계자(Inorder Successor — 오른쪽 서브트리의 최솟값)로 삭제 노드의 값을 대체한 뒤 후계자를 삭제합니다.

핵심 개념

불균형과 편향 트리

정렬된 순서(1→2→3→…)로 삽입하면 모든 노드가 한쪽 방향으로만 추가되어 연결 리스트와 같은 편향 트리(Skewed Tree)가 됩니다. 이 경우 높이가 n이 되어 탐색이 O(n)으로 저하됩니다. 이를 해결하기 위해 AVL 트리, Red-Black 트리 같은 자가 균형 BST가 사용됩니다.

핵심 개념

중위 순회와 정렬

BST를 중위 순회(Left → Root → Right)하면 저장된 값이 오름차순으로 정렬되어 나옵니다. 이 성질을 이용해 BST는 정렬된 데이터의 동적 유지, 범위 탐색(특정 범위의 모든 값 조회)에 활용됩니다.

시간복잡도 — 평균 O(log n) 탐색·삽입·삭제. 균형 잡힌 트리에서는 높이 h = log n이므로 각 연산이 O(log n)입니다. 최악(편향 트리)은 O(n). Java의 TreeMap은 내부적으로 Red-Black Tree를 사용하고 문서가 log(n) 시간을 보장한다고 설명합니다.
BST vs 해시 테이블 — 해시 테이블은 평균 O(1) 탐색이지만 순서를 보장하지 않습니다. BST는 O(log n)이지만 중위 순회로 정렬된 순서 접근, 범위 탐색, 최솟값/최댓값 조회가 가능합니다. 순서가 필요하면 BST, 빠른 단순 탐색만 필요하면 해시 테이블이 적합합니다.
자가 균형 BST — AVL 트리는 모든 노드에서 왼쪽/오른쪽 서브트리 높이 차이가 1 이하로 엄격하게 균형을 유지합니다. Red-Black 트리는 색 규칙으로 높이를 2log(n+1) 이하로 제한합니다. Java TreeMap/TreeSet과, libstdc++ 같은 대표적인 C++ 구현의 std::set/std::map이 보통 Red-Black Tree를 사용합니다(C++ 표준이 구조를 정한 것은 아닙니다).
언제 BST를 쓰나 — 정렬 순서 유지가 필요하면서도 삽입·삭제가 빈번한 경우. 범위 탐색(예: 100~200 사이의 모든 키), 전임자/후임자 조회가 필요한 경우. Java의 TreeMap/TreeSet, C++의 std::map/std::set이 대표적인 정렬 컬렉션이고 주로 BST로 구현됩니다.

이진 탐색 트리가 푸는 문제

이진 탐색 트리(BST)는 '정렬된 상태를 유지하면서 삽입과 삭제도 빠르게' 하려는 자료구조입니다. 정렬된 배열은 이진 탐색으로 빨리 찾지만 중간에 끼워 넣기 어렵고, 연결 리스트는 끼워 넣기는 쉽지만 찾기가 느립니다. BST는 트리 모양으로 둘의 장점을 노립니다. 다만 트리가 한쪽으로 치우치면 이점이 사라지므로 균형이 중요합니다.

숫자로 확인하기

균형이 잡혔을 때 — 키가 1,000,000개일 때 이상적으로 균형 잡힌 BST의 높이(간선 기준)는 약 19이고 비교는 최대 20번 안팎입니다.
편향되었을 때 — 같은 키를 1, 2, 3, … 순서로 넣으면 한쪽으로만 이어져 높이가 999,999가 되고, 마지막 키를 찾는 데 1,000,000번 비교합니다. 이렇게 n개를 모두 넣는 비용의 합은 1 + 2 + … + n = n(n + 1) / 2로 10⁶개일 때 약 5×10¹¹입니다.
Red-Black 트리의 높이 상한 — 노드가 n개이면 높이가 2 log₂(n + 1) 이하입니다. n = 10⁶이면 최대 약 40입니다.

자주 하는 오해

BST는 항상 O(log n)이다 — 높이에 비례하므로 균형이 깨지면 O(n)입니다. 정렬된 순서로 넣으면 연결 리스트처럼 됩니다. 평균 O(log n)은 키가 무작위로 들어올 때 이야기입니다.
각 노드를 자식과만 비교하면 BST인지 알 수 있다 — 왼쪽 서브트리의 모든 값이 현재 노드보다 작아야 합니다. 자식과만 비교하면 손자 이하에서 위반된 경우를 놓칩니다. 검증할 때는 허용 범위(최솟값과 최댓값)를 내려보내야 합니다.
삭제 때는 후계자만 쓸 수 있다 — 자식이 둘이면 오른쪽 서브트리의 최솟값(중위 후계자)이나 왼쪽 서브트리의 최댓값(중위 전임자) 둘 중 하나로 대체할 수 있습니다.
C++ std::map은 해시 테이블이다 — std::map과 std::set은 정렬된 컨테이너이고, 해시 기반 컨테이너는 std::unordered_map과 std::unordered_set입니다.

면접에서 이렇게 물어요

면접 질문

주어진 트리가 BST인지 어떻게 확인하나요?

중위 순회 결과가 오름차순인지 보거나, 재귀로 내려가며 각 노드가 허용 범위 안에 있는지 확인합니다. 자식과만 비교하면 안 됩니다.

면접 질문

BST와 해시 테이블은 언제 각각 쓰나요?

단순한 키 조회가 많으면 평균 O(1)인 해시 테이블이 유리합니다. 정렬된 순서 순회, 범위 조회, 최솟값·최댓값, 전임자·후임자가 필요하면 BST가 맞습니다.

면접 질문

자가 균형 BST는 왜 필요한가요?

삽입과 삭제 때 회전 같은 연산으로 높이를 로그 수준으로 유지해서, 입력 순서와 상관없이 최악의 경우에도 O(log n)을 보장하려는 것입니다.

면접 질문

AVL과 Red-Black 트리의 차이는 무엇인가요?

AVL은 모든 노드에서 두 서브트리 높이 차이를 1 이하로 엄격하게 유지해 탐색에 유리하고, Red-Black은 색 규칙으로 느슨하게 균형을 맞춰 삭제 때 재균형(회전) 횟수의 상한이 작습니다.

실제 시스템에서는

Java — TreeMap 문서는 Red-Black 트리 기반의 NavigableMap 구현이며 containsKey, get, put, remove에 log(n) 시간을 보장한다고 설명합니다.
C++ — 표준은 std::map의 주요 연산을 로그 시간으로 요구하고, libstdc++ 같은 대표적인 구현은 보통 Red-Black 트리를 씁니다(표준이 구조를 정한 것은 아닙니다).
Linux 커널 — 커널 문서는 Red-Black 트리(rbtree)를 정렬 가능한 키/값 쌍을 저장하는 자체 균형 이진 탐색 트리로 설명하며 커널 곳곳에서 쓰인다고 합니다. 스케줄러 문서는 CFS가 실행 시각 순의 rbtree로 작업의 타임라인을 만든다고 설명하는데, 최근 커널은 EEVDF로 전환 중이라고 덧붙입니다. CPU 스케줄링 토픽에서 스케줄링을 봅니다.

함께 보면 좋은 토픽

트리 — 이진 트리와 순회를 먼저 봅니다.
해시 테이블 — 순서가 없는 대신 평균 O(1)인 대안을 봅니다.
힙 — 트리 기반이지만 규칙이 다른 구조를 비교합니다.
이진 탐색 — BST 탐색의 바탕 원리를 봅니다.
인덱스 — 디스크 기반 균형 트리인 B-Tree 인덱스를 봅니다.

참고 자료

Java TreeMap — TreeMap — Java SE 21 API
교재 — Cormen, Leiserson, Rivest, Stein, "Introduction to Algorithms" 4판, "Binary Search Trees", "Red-Black Trees" 장

Java — BST 노드 정의 및 삽입

/* ── BST 노드 ───────────────────────────────────────────── */
public class BSTNode {
    int val;
    BSTNode left, right;
    BSTNode(int val) { this.val = val; }
}

/* ── 삽입: 루트부터 비교하며 null 위치를 찾아 삽입 ─────── */
public BSTNode insert(BSTNode root, int val) {
    if (root == null) return new BSTNode(val); // 삽입 위치
    if (val < root.val)
        root.left  = insert(root.left,  val);    // 왼쪽으로
    else if (val > root.val)
        root.right = insert(root.right, val);    // 오른쪽으로
    return root;                                   // 이미 존재하면 무시
}

/* ── 탐색 ────────────────────────────────────────────────── */
public boolean search(BSTNode root, int val) {
    if (root == null) return false;  // 탐색 실패
    if (val == root.val) return true; // 탐색 성공
    return val < root.val
        ? search(root.left,  val)
        : search(root.right, val);
}

삽입과 탐색 모두 현재 노드와 값을 비교해 왼쪽/오른쪽을 결정하는 동일한 패턴을 씁니다.
재귀 구조 덕분에 각 서브트리를 독립적으로 처리하며, 균형 잡힌 트리에서 높이는 log n이므로 평균 O(log n)입니다.

Java — 삭제 3케이스

public BSTNode delete(BSTNode root, int val) {
    if (root == null) return null;  // 값이 없음

    if (val < root.val) {
        root.left  = delete(root.left,  val); // 왼쪽 서브트리에서 삭제
    } else if (val > root.val) {
        root.right = delete(root.right, val); // 오른쪽 서브트리에서 삭제
    } else {
        // 삭제 대상 발견
        if (root.left  == null) return root.right; // Case 1,2: 왼쪽 없음
        if (root.right == null) return root.left;  // Case 2: 오른쪽 없음

        // Case 3: 자식 2개 → 중위 후계자(오른쪽 서브트리의 최솟값)로 대체
        BSTNode successor = findMin(root.right);
        root.val   = successor.val;                       // 값만 교체
        root.right = delete(root.right, successor.val); // 후계자 제거
    }
    return root;
}

private BSTNode findMin(BSTNode node) {
    while (node.left != null) node = node.left; // 가장 왼쪽 = 최솟값
    return node;
}

Case 1(리프): 자식이 없으므로 null을 반환해 부모와의 연결을 끊습니다.
Case 2(자식 1개): 남은 자식을 반환해 부모가 손자를 직접 가리키게 합니다.
Case 3(자식 2개): 중위 후계자(오른쪽 서브트리 최솟값)의 값을 현재 노드에 복사한 뒤, 후계자를 재귀적으로 삭제합니다.
후계자는 왼쪽 자식이 없으므로 Case 1 또는 2로 처리됩니다.
이 재귀 패턴은 BST 조건을 유지하면서 O(h) 시간 안에 삭제를 완료합니다.

Java — TreeMap으로 BST 활용

/* ── TreeMap: Red-Black Tree 기반 BST — 항상 O(log n) 보장 */
TreeMap<Integer, String> map = new TreeMap<>();

map.put(50, "root");
map.put(30, "left");
map.put(70, "right");

// 정렬 순서 보장
map.firstKey();          // 30  — 최솟값 O(log n)
map.lastKey();           // 70  — 최댓값 O(log n)

// 범위 탐색: 30 이상 70 미만 키를 정렬 순서로
map.subMap(30, 70).forEach((k, v) ->
    System.out.println(k + " → " + v)
);

// 50보다 작은 가장 큰 키 (전임자)
map.lowerKey(50);        // 30

// 50보다 크거나 같은 가장 작은 키 (후임자)
map.ceilingKey(50);     // 50

/* ── HashMap vs TreeMap 비교 ─────────────────────────────── */
// HashMap : 평균 O(1) 탐색/삽입, 순서 없음
// TreeMap : O(log n) 탐색/삽입, 정렬 순서 보장, 범위 탐색 지원

Java의 TreeMap/TreeSet은 Red-Black Tree를 사용해 편향 트리 문제 없이 항상 O(log n)을 보장합니다.
정렬 순서가 필요하거나 범위 탐색(subMap, headMap, tailMap), 전임자/후임자 조회가 필요하면 TreeMap을, 순서가 필요 없고 단순 탐색 속도만 중요하면 HashMap을 사용합니다.

Q1

이진 탐색 트리(BST)의 핵심 규칙은?

탐색이 빨라지려면 값의 크기 순서를 이용해야 합니다.
BST는 왼쪽 서브트리의 모든 노드가 루트보다 작고 오른쪽 서브트리는 모두 커야 한다는 규칙 덕분에, 매번 절반씩 범위를 좁혀가며 탐색할 수 있습니다.

Q2

BST가 한쪽으로 치우쳐 삽입되면 어떤 문제가 생기나요?

균형이 깨지면 트리 높이가 노드 수만큼 길어질 수 있습니다.
정렬된 데이터를 순서대로 삽입하는 등 한쪽으로 치우치면 트리가 사실상 연결 리스트가 되어, 탐색 시간이 O(log n)이 아닌 O(n)으로 나빠집니다.

Q3

BST에서 삭제하려는 노드가 자식을 2개 가진 경우 일반적인 처리 방법은?

대체할 값도 BST 규칙을 지켜야 합니다.
자식이 2개인 노드를 삭제할 때는 오른쪽 서브트리의 최솟값(또는 왼쪽 서브트리의 최댓값)을 가져와 그 자리를 대체하고, 옮겨온 노드를 원래 자리에서 삭제해 BST 규칙을 유지합니다.