본문으로 건너뛰기
김신건의 로그

Binary Search Tree (BST): 정렬된 이진 트리

· 수정 · 📖 약 4분 · 1,153자/단어 #algorithm #data-structure #tree #bst
Binary Search Tree, BST, 이진 탐색 트리, binary-search-tree

정의

Binary Search Tree (BST) 는 각 노드가 다음 BST 불변식을 만족하는 이진 트리:

  • 왼쪽 서브트리의 모든 키 < 노드 키
  • 오른쪽 서브트리의 모든 키 > 노드 키

연산: 탐색, 삽입, 삭제 모두 평균 O(log N). 최악은 O(N) (편향 트리).

핵심 성질: 중위 순회 (inorder: 왼 -> 루트 -> 오른) 하면 키가 오름차순 정렬된 순서로 출력된다.

문제 상황

정렬 배열:

  • 탐색 O(log N) (이진 탐색)
  • 삽입/삭제 O(N) (원소 shift)

연결 리스트:

  • 삽입/삭제 O(1) (포인터 조작)
  • 탐색 O(N) (선형 탐색)

BST 는 두 자료구조의 트레이드오프를 해결: 탐색/삽입/삭제 모두 평균 O(log N).

시각화

flowchart TD
    A["5"]
    B["3"]
    C["7"]
    D["1"]
    E["4"]
    F["6"]
    G["9"]
    A --> B
    A --> C
    B --> D
    B --> E
    C --> F
    C --> G

탐색(4): 루트(5) -> 왼쪽(3) -> 오른쪽(4) 찾음. 비교 3회.

삽입(8): 루트(5) -> 오른(7) -> 오른(9) -> 9의 왼쪽에 삽입.

중위 순회: 1, 3, 4, 5, 6, 7, 9 (오름차순).

핵심 아이디어

현재 노드와 목표 키를 비교해 재귀:

search(node, k):
    if node is null or node.key == k: return node
    if k < node.key: return search(node.left, k)
    else:            return search(node.right, k)

트리 높이 H 에 비례 = O(H). 균형 트리이면 H = O(log N).

삽입 (insert)

탐색과 동일한 경로로 내려가다 null 을 만나면 노드 생성:

insert(node, k):
    if node is null: return new Node(k)
    if k < node.key: node.left  = insert(node.left,  k)
    else:            node.right = insert(node.right, k)
    return node

삭제 (delete)

삭제 케이스 3가지:

  1. 자식 없음 (leaf): 그냥 제거.
  2. 자식 1개: 자식으로 대체.
  3. 자식 2개: inorder successor (오른쪽 서브트리의 최솟값) 를 현재 노드로 복사, successor 삭제.

알고리즘

전체 구현 (C++)

struct Node {
    int key;
    Node *l, *r;
    Node(int k) : key(k), l(nullptr), r(nullptr) {}
};

Node* search(Node* r, int k) {
    if (!r || r->key == k) return r;
    return k < r->key ? search(r->l, k) : search(r->r, k);
}

Node* insert(Node* r, int k) {
    if (!r) return new Node(k);
    if (k < r->key) r->l = insert(r->l, k);
    else if (k > r->key) r->r = insert(r->r, k);
    return r;
}

Node* minNode(Node* r) {
    while (r->l) r = r->l;
    return r;
}

Node* remove(Node* r, int k) {
    if (!r) return nullptr;
    if (k < r->key) r->l = remove(r->l, k);
    else if (k > r->key) r->r = remove(r->r, k);
    else {
        // 삭제 케이스
        if (!r->l) { Node* tmp = r->r; delete r; return tmp; }
        if (!r->r) { Node* tmp = r->l; delete r; return tmp; }
        // 자식 2개: inorder successor
        Node* succ = minNode(r->r);
        r->key = succ->key;
        r->r = remove(r->r, succ->key);
    }
    return r;
}

중위 순회

void inorder(Node* r, vector<int>& res) {
    if (!r) return;
    inorder(r->l, res);
    res.push_back(r->key);
    inorder(r->r, res);
}

구현

#include <bits/stdc++.h>
using namespace std;

struct Node {
  int key;
  Node *l, *r;
  Node(int k) : key(k), l(nullptr), r(nullptr) {}
};

Node* insert(Node* r, int k) {
  if (!r) return new Node(k);
  if (k < r->key) r->l = insert(r->l, k);
  else if (k > r->key) r->r = insert(r->r, k);
  return r;
}

Node* minNode(Node* r) { while (r->l) r = r->l; return r; }

Node* remove(Node* r, int k) {
  if (!r) return nullptr;
  if (k < r->key) r->l = remove(r->l, k);
  else if (k > r->key) r->r = remove(r->r, k);
  else {
      if (!r->l) { Node* t = r->r; delete r; return t; }
      if (!r->r) { Node* t = r->l; delete r; return t; }
      Node* s = minNode(r->r);
      r->key = s->key;
      r->r = remove(r->r, s->key);
  }
  return r;
}

void inorder(Node* r, vector<int>& v) {
  if (!r) return;
  inorder(r->l, v);
  v.push_back(r->key);
  inorder(r->r, v);
}

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);

  int n; cin >> n;
  Node* root = nullptr;
  for (int i = 0; i < n; i++) {
      int x; cin >> x;
      root = insert(root, x);
  }
  // 삭제
  int d; cin >> d;
  root = remove(root, d);
  // 중위 순회 출력
  vector<int> res;
  inorder(root, res);
  for (int i = 0; i < (int)res.size(); i++) {
      cout << res[i];
      if (i + 1 < (int)res.size()) cout << " ";
  }
  cout << "\n";
}
stdin
6
5 3 7 1 4 9
3
결과
1 4 5 7 9

복잡도

연산평균최악 (편향)
탐색O(log N)O(N)
삽입O(log N)O(N)
삭제O(log N)O(N)
중위 순회O(N)O(N)
공간O(N)O(N)

왜 균형이 필요한가

정렬된 입력 1, 2, 3, ..., N 을 순서대로 삽입하면 오른쪽으로만 뻗는 일자 트리 가 되어 높이 H = N, 모든 연산이 O(N) 로 퇴화.

flowchart TD
    A1["1"]
    A2["2"]
    A3["3"]
    A4["4"]
    A1 --> |"right"| A2
    A2 --> |"right"| A3
    A3 --> |"right"| A4

이를 해결하는 균형 BST:

자료구조균형 전략사용
[[avl-treeAVL Tree]]높이 차 <= 1, 회전
Red-Black Tree색 기반 완화 균형C++ STL set/map
[[bbstTreap]]우선순위 무작위화
Splay Tree최근 접근 노드 루트로캐시 패턴 편향 시
[[order-statistics-treeOST]]BST + 서브트리 크기

함정

1. 중복 원소 처리

k < r->key / k > r->key 만 처리하면 중복은 삽입 무시. 중복 허용이 필요하면 k <= r->key 로 왼쪽에 넣거나 카운터 필드 추가.

WARNING

중복 원소를 왼/오른쪽에 일관성 없이 넣으면 삭제 시 버그 발생. 방향을 통일해야 한다.

2. 삭제 후 inorder successor 재귀 삭제

Node* s = minNode(r->r);
r->key = s->key;
r->r = remove(r->r, s->key);  // s->key 로 삭제해야 정확

s 포인터를 직접 삭제하면 안 된다. remove 재귀로 처리해야 구조 유지.

3. 메모리 누수 (C++)

delete r 을 leaf 삭제 시점에 해야 한다. 재귀 삭제 시 자식 포인터가 남아 있으면 누수.

4. 랜덤 입력 가정

BST 평균 O(log N) 는 입력이 랜덤 순서일 때만. 정렬/역순 입력이면 O(N). 실전에서는 균형 BST 사용 권장.

BOJ 연습 문제

번호제목비고
BOJ 5639이진 검색 트리전위 순회로 BST 복원
BOJ 2104부분배열 고르기BST 응용
BOJ 1991트리 순회전위/중위/후위 순회

참고

이 글의 용어 (5개)
세그먼트 트리 (Segment Tree)algorithm
정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
자료구조 (Data Structures)algorithm
정의 자료구조 (Data Structure) 는 데이터를 효율적으로 저장하고 접근하기 위한 조직화 방법. 문제 풀이에서는 시간 복잡도와 공간 복잡도의 trade-off 를 정확히…
BBST (Splay Tree, Treap)algorithm
정의 BBST (Balanced Binary Search Tree) 는 균형이 amortized / expected 로 보장되는 이진 탐색 트리. PS 에서는 split / me…
Fenwick Tree (Binary Indexed Tree): 구간 합 O(log N)algorithm
정의 Fenwick Tree (또는 BIT, Binary Indexed Tree) 는 배열의 prefix sum 을 O(log N) 에 갱신·조회 하는 자료구조입니다. Peter…
Order Statistics Tree (OST): rank/select 지원 BSTalgorithm
정의 Order Statistics Tree (OST) 는 각 노드에 서브트리 크기 (size) 를 추가로 저장한 균형 BST. 두 가지 새 연산을 O(log N) 에 지원한다.…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기