레드-블랙 트리 완전 정복 (Red-Black Tree Implementation)

레드-블랙 트리란?
자가 균형 이진 탐색 트리의 한 종류로, 각 노드가 빨강(RED) 또는 검정(BLACK) 색상을 가지며, 색상 규칙을 통해 균형을 유지하는 트리입니다.
AVL vs 레드-블랙 트리 비교
비교 항목 | AVL 트리 | 레드-블랙 트리 |
균형 조건 | 엄격 (높이 차이 ≤ 1) | 느슨 (검정 높이 동일) |
트리 높이 | ~1.44 log n (더 낮음) | ~2 log n |
탐색 속도 | ||
삽입 속도 | ||
삭제 속도 | ||
메모리 | 각 노드에 높이 정보 저장 | 각 노드에 색상 (1비트) |
주요 사용처 | 탐색 중심 (DB 인덱스) | 삽입/삭제 중심 (C++ STL) |
레드-블랙 트리의 장점
1. 삽입/삭제 성능이 우수
•
AVL은 엄격한 균형 때문에 회전이 빈번함 (최악 log n번)
•
레드-블랙은 느슨한 균형으로 회전 횟수를 최소화 (최대 2~3번)
•
실무에서 삽입/삭제가 빈번한 경우 5배 이상 빠름
2. 실용적인 균형
•
탐색은 AVL보다 약간 느리지만 O(log n) 보장
•
전체적으로 균형잡힌 성능
3. C++ STL 표준
•
std::map, std::set, std::multimap, std::multiset 모두 레드-블랙 트리 사용
•
업계 표준으로 검증된 자료구조
💡 언제 어떤 트리를 선택할까?
AVL 선택:
✓ 탐색이 매우 빈번하고 삽입/삭제가 드문 경우
✓ 데이터베이스 인덱스처럼 조회 성능이 최우선인 경우
레드-블랙 선택:
✓ 삽입/삭제/탐색이 고르게 발생하는 경우 (대부분의 실무)
✓ 범용적인 자료구조가 필요한 경우
✓ C++ STL과 호환이 중요한 경우
Plain Text
복사
레드-블랙 트리 5가지 규칙
레드-블랙 트리의 모든 규칙은 "트리의 높이를 2 log n 이하로 제한"하여 O(log n) 성능을 보장하기 위해 존재합니다.
1. 노드 색상 규칙
모든 노드는 빨강(RED) 또는 검정(BLACK)이다.
Plain Text
복사
2. 루트 규칙
루트 노드는 항상 검정(BLACK)이다.
Plain Text
복사
왜? 검정 높이 계산의 기준점을 명확히 하기 위해.
•
루트를 검정으로 고정하면 모든 경로의 검정 높이 계산이 명확해짐
3. 리프 규칙
모든 리프 노드(NIL)는 검정(BLACK)이다.
Plain Text
복사
4. 빨강 노드 규칙 (핵심!)
빨강(RED) 노드의 자식은 반드시 검정(BLACK)이다.
(연속으로 빨강 노드가 올 수 없다)
Plain Text
복사
왜? 이것이 높이 제한의 핵심 제약입니다!
만약 빨강이 연속된다면?
최악의 경우: 검정-빨강-빨강-검정-빨강-빨강...
→ 검정 노드 사이에 빨강이 여러 개 끼어들 수 있음
→ 트리 높이가 무한정 커질 수 있음!
빨강 연속 금지 시:
검정-빨강-검정-빨강-검정...
→ 빨강은 검정 사이에만 1개씩만 가능
→ 최악의 경우도 "검정 높이 × 2" 이하로 제한!
Plain Text
복사
수학적 증명:
•
경로의 검정 높이가 h라면 최소 h개의 검정 노드 존재
•
빨강은 검정 사이에만 올 수 있으므로 최대 h개 추가 가능
•
∴ 최대 높이 = 2h ≤ 2 log(n+1)
5. 검정 높이 규칙
루트에서 모든 리프까지의 경로에서
검정 노드의 개수는 동일해야 한다.
Plain Text
복사
왜? 트리의 균형을 보장하기 위해.
만약 검정 높이가 다르다면?
경로 A: 검정 3개 → 최소 3단계
경로 B: 검정 10개 → 최소 10단계
→ 심각한 불균형!
검정 높이 동일 시:
모든 경로의 검정 노드 수가 같음
→ 규칙 4와 결합하여 모든 경로가 2배 이내
→ 균형 보장!
Plain Text
복사
규칙의 상호작용
규칙 4 (빨강 연속 금지) + 규칙 5 (검정 높이 동일)
↓
최단 경로 : 최장 경로 = 1 : 2 이내로 제한
↓
트리 높이 ≤ 2 log n
↓
O(log n) 보장!
Plain Text
복사
구체적 예시:
❌ 잘못된 트리 (규칙 5 위반):
⚫10
/ \
⚫5 ⚫30 ← 검정 2개
/ \
⚫2 ⚫7
/
⚫1 ← 검정 4개! 불균형!
✅ 올바른 레드-블랙 트리:
⚫10
/ \
⚫5 ⚫30 ← 모든 경로 검정 2개
/ \ / \
🔴2 🔴7 🔴25 🔴35
→ 검정 높이 = 2
→ 최대 높이 ≤ 4 (검정 2 × 2)
→ O(log n) 보장!
Plain Text
복사
기본 구조 정의
#include <iostream>
#include <vector>
enum class eColor
{
Red,
Black,
End,
};
struct Node
{
int Data;
eColor Color;
Node* Left;
Node* Right;
Node* Parent;
Node(int data)
: Data(data)
, Color(eColor::Red)
, Left(nullptr)
, Right(nullptr)
, Parent(nullptr)
{
}
};
class RedBlackTree
{
public:
RedBlackTree()
{
NIL = new Node(0);
NIL->Color = eColor::Black;
mRoot = NIL;
}
private:
Node* mRoot;
Node* NIL; // 센티널 노드 (모든 리프를 대체)
}
C++
복사
기본 회전 연산
회전 연산이 왜 필요한가?
레드-블랙 트리에서 회전(Rotation)은 트리 구조를 재배치하여 균형을 맞추는 핵심 메커니즘입니다.
회전이 필요한 이유:
•
노드를 삽입하거나 삭제할 때 트리가 한쪽으로 치우칠 수 있음
•
회전을 통해 높이를 줄이고 균형을 복원
•
균형 잡힌 트리 = O(log n) 성능 보장
•
Double Red(빨강-빨강 연속) 발생 시 Restructuring으로 해결
•
삼촌이 검정일 때 회전 + 색상 변경으로 규칙 복원
•
회전 없이는 검정 높이 균형을 맞출 수 없음
•
회전 후에도 "왼쪽 < 부모 < 오른쪽" 순서 유지
•
트리 구조만 바뀌고 정렬 순서는 변하지 않음
•
회전은 O(1) 시간에 완료됨 (포인터 몇 개만 변경)
•
최소한의 연산으로 최대 효과
회전 없이 삽입만 한다면?
10
\
20 ← 한쪽으로 치우침
\
30 ← O(n) 성능으로 퇴화!
회전으로 균형 맞춘 결과:
20 ← 균형 잡힘
/ \
10 30 ← O(log n) 보장!
Plain Text
복사
좌회전 (Left Rotation)
개념: 오른쪽 자식이 부모 위치로 올라가는 회전
단계별 과정:
초기 상태:
x
/ \
A y
/ \
B C
1단계: y의 왼쪽 서브트리(B)를 x의 오른쪽으로 이동
x y
/ \ / \
A B B C
2단계: y를 x의 위치로 올리고, x를 y의 왼쪽 자식으로
y
/ \
x C
/ \
A B
최종: x와 y의 부모-자식 관계가 바뀜
Plain Text
복사
구현 코드:
void LeftRotate(Node* x)
{
Node* y = x->Right; // y는 x의 오른쪽 자식
// 1단계: y의 왼쪽 서브트리를 x의 오른쪽으로 이동
x->Right = y->Left;
if (y->Left != NIL)
{
y->Left->Parent = x;
}
// 2단계: y의 부모를 x의 부모로 설정
y->Parent = x->Parent;
if (x->Parent == NIL)
{
mRoot = y; // x가 루트였다면 y가 새 루트
}
else if (x == x->Parent->Left)
{
x->Parent->Left = y;
}
else
{
x->Parent->Right = y;
}
// 3단계: x를 y의 왼쪽 자식으로 설정
y->Left = x;
x->Parent = y;
}
C++
복사
우회전 (Right Rotation)
개념: 왼쪽 자식이 부모 위치로 올라가는 회전
단계별 과정:
초기 상태:
y
/ \
x C
/ \
A B
1단계: x의 오른쪽 서브트리(B)를 y의 왼쪽으로 이동
y x
/ \ / \
B C A B
2단계: x를 y의 위치로 올리고, y를 x의 오른쪽 자식으로
x
/ \
A y
/ \
B C
최종: x와 y의 부모-자식 관계가 바뀜
Plain Text
복사
구현 코드:
void RightRotate(Node* y)
{
Node* x = y->Left; // x는 y의 왼쪽 자식
// 1단계: x의 오른쪽 서브트리를 y의 왼쪽으로 이동
y->Left = x->Right;
if (x->Right != NIL)
{
x->Right->Parent = y;
}
// 2단계: x의 부모를 y의 부모로 설정
x->Parent = y->Parent;
if (y->Parent == NIL)
{
mRoot = x; // y가 루트였다면 x가 새 루트
}
else if (y == y->Parent->Left)
{
y->Parent->Left = x;
}
else
{
y->Parent->Right = x;
}
// 3단계: y를 x의 오른쪽 자식으로 설정
x->Right = y;
y->Parent = x;
}
C++
복사
회전 연산 핵심 정리
📌 핵심 개념:
- 좌회전: 오른쪽 자식이 부모가 됨
- 우회전: 왼쪽 자식이 부모가 됨
📌 중요 속성:
✓ 회전 후에도 BST 속성 유지 (왼쪽 < 부모 < 오른쪽)
✓ 시간 복잡도: O(1) (포인터 조작만)
✓ 트리 균형 복원의 핵심 도구
✓ 중위 순회 결과는 회전 전후가 동일
📌 회전의 역관계:
- LeftRotate(x) 후 RightRotate(y) → 원상복구
- RightRotate(y) 후 LeftRotate(x) → 원상복구
Plain Text
복사
삽입 (Insert)
노드를 삽입하면 처음에는 빨강(Red) 으로 색칠합니다.
이때 규칙을 어길 수 있어서 리컬러링(Recoloring) 과 리스트럭처링(Restructuring, 회전) 이 필요합니다.
1) 리컬러링(Recoloring)
•
삽입한 노드의 부모와 삼촌이 모두 빨강인 경우 → 부모와 삼촌을 검정, 조부모를 빨강으로 바꿉니다.
•
만약 루트까지 올라가서 충돌이 발생하면 다시 같은 규칙을 적용합니다.
2) 리스트럭처링(Restructuring, 회전)
•
삽입한 노드의 부모는 빨강인데, 삼촌은 검정이거나 없는 경우.
•
이때 조부모, 부모, 새 노드의 위치 관계에 따라 회전(Rotation) 을 합니다.
삽입 시 기본 규칙
•
새로운 노드는 항상 빨강(Red) 으로 삽입한다.
•
이때 부모가 빨강이면 Double Red 문제가 발생할 수 있다.
•
Double Red 해결에는 Restructuring(회전) 과 Recoloring(색변경) 두 가지 전략이 있다.
Double Red 해결 전략
(A) 삼촌(U)이 검정일 때 → Restructuring
삼촌이 검정일 때 Restructuring(회전)을 사용하는 이유:
삼촌이 검정이면 Recoloring만으로는 검정 노드 개수 균형을 맞출 수 없기 때문입니다.
구체적으로:
•
검정 높이 규칙 위반: 부모(P)와 삼촌(U) 중 하나만 검정이면, 단순히 색을 바꾸는 것만으로는 "루트에서 모든 리프까지의 경로에서 검정 노드 개수가 동일해야 한다"는 규칙을 유지할 수 없습니다.
•
구조 변경 필요: 따라서 노드들의 위치 관계 자체를 바꿔야 하며, 이때 회전(Rotation)을 사용합니다.
•
해결 방법: 새 노드(N), 부모(P), 조상(G)을 값 기준으로 오름차순 정렬하여 중간값을 새로운 부모로 두고, 새로운 부모는 검정, 자식 둘은 빨강으로 색을 바꿉니다.
반대로 삼촌이 빨강일 때는 부모와 삼촌을 모두 검정으로, 조상을 빨강으로 바꿔도 검정 노드 개수 균형이 유지되므로 간단한 Recoloring으로 해결할 수 있습니다.
구조를 바꿔서 해결한다.
새 노드(N), 부모(P), 조상(G)을 값 기준으로 오름차순 정렬한다.
중간값을 새로운 부모로 두고, 나머지 둘을 자식으로 만든다.
새로운 부모는 검정, 자식 둘은 빨강으로 색을 바꾼다.
(B) 삼촌(U)이 빨강일 때 → Recoloring
삼촌이 빨강일 때 Recoloring을 하는 이유는 구조 변경 없이 색상만으로 문제를 해결할 수 있기 때문입니다.
구체적으로:
•
부모(P)와 삼촌(U)이 모두 빨강이면, 둘 다 검정으로 바꾸고 조상(G)을 빨강으로 만들어도 검정 노드 개수 균형이 유지됩니다.
•
회전(Restructuring)은 트리 구조를 바꾸는 복잡한 작업인 반면, Recoloring은 단순히 색만 바꾸면 되므로 더 효율적입니다.
•
반대로 삼촌이 검정이면 색상만으로는 균형을 맞출 수 없어서 Restructuring(회전)이 필요합니다.
핵심 원리:
레드-블랙 트리는 "루트에서 모든 리프까지 경로의 검정 노드 개수가 동일해야 한다"는 규칙이 있습니다. 삼촌이 빨강일 때는 부모와 삼촌을 검정으로, 조상을 빨강으로 바꿔도 이 규칙이 깨지지 않기 때문에 간단한 Recoloring으로 해결할 수 있는 것입니다.
색을 바꿔서 해결한다.
부모(P)와 삼촌(U)을 검정으로, 조상(G)을 빨강으로 바꾼다.
만약에 G가 루트라면 반드시 검정으로 다시 바꿔준다.
Recoloring은 간단해 보이지만 문제는 조상 노드(G)가 루트 노드가 아니면서, 조상 노드(G)가 또다시 Double Red 문제가 발생하는 경우이다.
조상(G)이 빨강이 되어 또 Double Red가 생기면, 같은 방식(Restructuring/Recoloring)을 반복한다.
해결 과정
1.
현재 상태
•
G=5가 빨강, P=9도 빨강 → Double Red 발생.
•
삼촌 U=14는 빨강.
2.
Recoloring 수행
•
P(9)와 U(14)를 검정으로 변경.
•
G(11)을 빨강으로 변경.
3.
문제 확인
•
이제 루트인 11이 빨강이 되었는데, 규칙상 루트는 반드시 검정이어야 한다.
4.
최종 조정
•
루트 11을 검정으로 바꾸면 모든 규칙이 만족된다.
•
Recoloring은 단순히 색을 바꾸는 작업이지만, 조상(G)이 빨강이 되면서 새로운 Double Red를 만들 수 있다.
•
이 경우 Recoloring을 반복하거나, 상황에 따라 Restructuring(회전) 으로 넘어가야 한다.
•
루트 노드는 무조건 검정이 되도록 마지막에 조정한다.
1단계: 일반적인 BST 삽입
void Insert(int data)
{
Node* newNode = new Node(data);
newNode->Left = NIL;
newNode->Right = NIL;
Node* y = NIL;
Node* x = mRoot;
// 새로운 노드를 삽입할 위치를 찾음
// 재귀 함수로 대체 가능
while (x != NIL)
{
y = x;
if (newNode->Data < x->Data)
{
x = x->Left;
}
else
{
x = x->Right;
}
}
// 부모노드 설정
newNode->Parent = y;
// 트리가 비어있다면 새 노드가 루트
if (y == NIL)
{
mRoot = newNode;
}
else if (newNode->Data < y->Data)
{
y->Left = newNode;
}
else
{
y->Right = newNode;
}
newNode->Color = eColor::Red; // 새 노드는 빨간색으로 삽입
InsertFixup(newNode); // 삽입 후 균형 맞추기
}
C++
복사
삽입 후 수정 (Insert Fix-Up)
// 모든 노드는 빨강(RED) 또는 검정(BLACK)이다.
// 루트 노드는 항상 검정(BLACK)이다.
// 모든 리프 노드(NIL)는 검정(BLACK)이다.
// 빨강(RED) 노드의 자식은 반드시 검정(BLACK)이다.
// (연속으로 빨강 노드가 올 수 없다)
// 루트에서 모든 리프까지의 경로에서
// 검정 노드의 개수는 동일해야 한다.
void InsertFixup(Node* z)
{
while (z->Parent->Color == eColor::Red)
{
if (z->Parent == z->Parent->Parent->Left)
{
Node* y = z->Parent->Parent->Right; // 삼촌 노드
if (y->Color == eColor::Red) // Case 1: 삼촌이 빨간색
{
z->Parent->Color = eColor::Black; //recoloring
y->Color = eColor::Black;
z->Parent->Parent->Color = eColor::Red;
z = z->Parent->Parent;
}
else
{
// restruturing
if (z == z->Parent->Right) // Case 2: 삼촌이 검은색이고, z가 오른쪽 자식
{
z = z->Parent;
LeftRotate(z);
}
// Case 3: 삼촌이 검은색이고, z가 왼쪽 자식
z->Parent->Color = eColor::Black;
z->Parent->Parent->Color = eColor::Red;
RightRotate(z->Parent->Parent);
}
}
else
{
Node* y = z->Parent->Parent->Left; // 삼촌 노드
if (y->Color == eColor::Red) // Case 1: 삼촌이 빨간색
{
// recoloring
z->Parent->Color = eColor::Black;
y->Color = eColor::Black;
z->Parent->Parent->Color = eColor::Red;
z = z->Parent->Parent;
}
else
{
//restruturing
if (z == z->Parent->Left) // Case 2: 삼촌이 검은색이고, z가 왼쪽 자식
{
z = z->Parent;
RightRotate(z);
}
// Case 3: 삼촌이 검은색이고, z가 오른쪽 자식
z->Parent->Color = eColor::Black;
z->Parent->Parent->Color = eColor::Red;
LeftRotate(z->Parent->Parent);
}
}
}
mRoot->Color = eColor::Black; // 루트는 항상 검은색
}
C++
복사
실제 삽입 예제: 20, 10, 30, 5, 15 순서대로
1. 20 삽입:
⚫20 ← 루트는 항상 BLACK
2. 10 삽입:
⚫20
/
🔴10 ← 문제없음
3. 30 삽입:
⚫20
/ \
🔴10 🔴30 ← 문제없음
4. 5 삽입:
⚫20
/ \
🔴10 🔴30
/
🔴5 ← RED-RED 위반! (부모 10도 RED)
삼촌 30이 RED이므로 Case 1:
🔴20
/ \
⚫10 ⚫30
/
🔴5
루트를 BLACK으로: ⚫20
5. 15 삽입:
⚫20
/ \
⚫10 ⚫30
/
🔴5
15는 10의 오른쪽에 삽입:
⚫20
/ \
⚫10 ⚫30
/ \
🔴5 🔴15 ← 문제없음 (부모 10이 BLACK)
최종 결과:
⚫20
/ \
⚫10 ⚫30
/ \
🔴5 🔴15
Plain Text
복사
탐색 연산
Node* Search(int data)
{
Node* current = mRoot;
while (current != NIL && data != current->Data)
{
if (data < current->Data)
{
current = current->Left;
}
else
{
current = current->Right;
}
}
return current; // 찾으면 해당 노드, 못 찾으면 NIL 반환
}
C++
복사
중위 순회 (정렬된 출력)
void InOrderTraversal(Node* node)
{
if (node != NIL)
{
InOrderTraversal(node->Left);
std::cout << node->Data << " ";
InOrderTraversal(node->Right);
}
}
C++
복사
컬러 트리 시각화 (터미널 색상 지원)
void PrintColorTree(Node* node, int depth = 0)
{
// ANSI 색상 코드 정의
const std::string RED_COLOR = "\033[31m"; // 빨강
const std::string BLACK_COLOR = "\033[30m"; // 검정 (기본)
const std::string RESET_COLOR = "\033[0m"; // 색상 리셋
const std::string BOLD = "\033[1m"; // 굵게
if (node != NIL)
{
PrintColorTree(node->Right, depth + 1);
for (int i = 0; i < depth; ++i) std::cout << " ";
std::cout << (node->Color == eColor::Red ? RED_COLOR : BLACK_COLOR)
<< BOLD << node->Data << "(" << (node->Color == eColor::Red ? "R" : "B") << ")"
<< RESET_COLOR << "\n";
PrintColorTree(node->Left, depth + 1);
}
}
void DisplayColorTree()
{
PrintColorTree(mRoot);
}
C++
복사
유니코드 색상 시각화
void printUnicodeColorTree(RBNode* node, int depth = 0)
{
if (node != NIL)
{
printUnicodeColorTree(node->right, depth + 1);
for (int i = 0; i < depth; i++)
{
std::cout << " ";
}
// 유니코드 색상 원과 함께 출력
if (node->color == RED)
{
std::cout << "🔴 " << node->data << std::endl;
}
else
{
std::cout << "⚫ " << node->data << std::endl;
}
printUnicodeColorTree(node->left, depth + 1);
}
}
void displayUnicodeTree()
{
std::cout << "🔴⚫ Red-Black Tree (유니코드 버전):" << std::endl;
if (root == NIL)
{
std::cout << "Empty tree" << std::endl;
}
else
{
printUnicodeColorTree(root);
}
std::cout << std::endl;
}
C++
복사
완전한 사용 예제 & 시뮬레이션
int main()
{
RedBlackTree rbt;
std::cout << "🔴⚫ Red-Black Tree 완전 정복!" << std::endl;
std::cout << "==============================\n" << std::endl;
// 데이터 삽입
std::vector<int> data = { 20, 10, 30, 5, 15, 25, 35 };
std::cout << "\n=== 레드-블랙 트리 삽입 과정 ===" << std::endl;
for (int value : data)
{
std::cout << "\n🔄 삽입: " << value << std::endl;
rbt.Insert(value);
// 기본 출력 (R/B 표기)
std::cout << "기본 출력:" << std::endl;
rbt.DisplayColorTree();
// 이모지 출력 (🔴⚫)
//std::cout << "이모지 출력:" << std::endl;
//rbt.displayUnicodeTree();
std::cout << "------------------------" << std::endl;
}
// 정렬된 결과 출력
//std::cout << "\n=== 🎯 정렬된 결과 (중위 순회) ===" << std::endl;
//std::vector<int> sorted = rbt.getSortedArray();
//for (int i = 0; i < sorted.size(); i++)
//{
// std::cout << sorted[i];
// if (i < sorted.size() - 1) std::cout << " → ";
//}
//std::cout << std::endl;
// 탐색 테스트
std::cout << "\n=== 🔍 탐색 테스트 ===" << std::endl;
std::vector<int> targets = { 15, 40, 25, 1, 30 };
for (int target : targets)
{
Node* result = rbt.Search(target);
if (result != rbt.GetNIL())
{
std::cout << "✅ 값 " << target << " 발견! (색상: "
<< (result->Color == eColor::Red ? "Red" : "Black") << ")" << std::endl;
}
else
{
std::cout << "❌ 값 " << target << " 없음!" << std::endl;
}
}
//// 트리 속성 검증
//std::cout << "\n=== ✅ 트리 검증 ===" << std::endl;
//if (rbt.validateRedBlackTree())
//{
// std::cout << "✅ Red-Black 트리 속성 모두 만족!" << std::endl;
//}
//else
//{
// std::cout << "❌ Red-Black 트리 속성 위반!" << std::endl;
//}
//int blackHeight = rbt.getBlackHeight(rbt.root);
//if (blackHeight != -1)
//{
// std::cout << "✅ 검정 높이: " << blackHeight << " (모든 경로 동일)" << std::endl;
//}
//else
//{
// std::cout << "❌ 검정 높이 불일치!" << std::endl;
//}
//std::cout << "\n🎉 Red-Black Tree 학습 완료!" << std::endl;
//std::cout << "💡 🔴⚫ 이모지로 색상을 구분할 수 있습니다." << std::endl;
return 0;
}
C++
복사
이모지 실행 결과 시뮬레이션
=== 🔴⚫ 레드-블랙 트리 삽입 과정 ===
삽입: 20
Red-Black Tree:
⚫ 20
삽입: 10
Red-Black Tree:
⚫ 20
/
🔴 10
삽입: 30
Red-Black Tree:
⚫ 20
/ \
🔴 10 🔴 30
삽입: 5 (🔴-🔴 위반 발생! → 색상 조정)
Red-Black Tree:
⚫ 20
/ \
⚫ 10 ⚫ 30 ← 자동 색상 조정!
/
🔴 5
삽입: 15
Red-Black Tree:
⚫ 20
/ \
⚫ 10 ⚫ 30
/ \
🔴 5 🔴 15
삽입: 25
Red-Black Tree:
⚫ 20
/ \
⚫ 10 ⚫ 30
/ \ /
🔴 5 🔴 15 🔴 25
삽입: 35
Red-Black Tree:
⚫ 20
/ \
⚫ 10 ⚫ 30
/ \ / \
🔴 5 🔴 15 🔴 25 🔴 35
=== 정렬된 결과 (중위 순회) ===
5 10 15 20 25 30 35
=== 탐색 테스트 ===
15: 발견 🔴
40: 없음 ❌
25: 발견 🔴
Plain Text
복사
간단 테스트 함수
void testColorDisplay()
{
std::cout << "\n=== 🎨 이모지 색상 테스트 ===" << std::endl;
// 이모지 테스트
std::cout << "🔴 RED 노드" << std::endl;
std::cout << "⚫ BLACK 노드" << std::endl;
// 실제 트리에서 색상 확인
RedBlackTree colorTest;
colorTest.insert(10);
colorTest.insert(5);
colorTest.insert(15);
std::cout << "\n실제 트리 색상 예시:" << std::endl;
colorTest.displayUnicodeTree();
}
C++
복사
회전 연산 실습 도구
// 회전 테스트를 위한 간단한 함수
void demonstrateRotation()
{
std::cout << "\n=== 🔄 회전 연산 데모 ===\n" << std::endl;
RedBlackTree demo;
// 좌회전을 유발하는 패턴
std::cout << "좌회전 유발 패턴: 10 → 20 → 30" << std::endl;
demo.insert(10);
demo.insert(20);
demo.insert(30);
std::cout << "결과 (자동으로 균형 조정됨):" << std::endl;
demo.displayUnicodeTree();
// 우회전을 유발하는 패턴
RedBlackTree demo2;
std::cout << "\n우회전 유발 패턴: 30 → 20 → 10" << std::endl;
demo2.insert(30);
demo2.insert(20);
demo2.insert(10);
std::cout << "결과 (자동으로 균형 조정됨):" << std::endl;
demo2.displayUnicodeTree();
}
C++
복사
트리 속성 검증 , 정렬
std::vector<int> getSortedArray()
{
std::vector<int> result;
InOrderCollect(mRoot, result);
return result;
}
void InOrderCollect(Node* node, std::vector<int>& arr)
{
if (node != NIL)
{
InOrderCollect(node->Left, arr);
arr.push_back(node->Data);
InOrderCollect(node->Right, arr);
}
}
int getBlackHeight(Node* node)
{
if (node == NIL)
{
return 1; // NIL 노드는 검정색이므로 높이 1
}
int leftBlackHeight = getBlackHeight(node->Left);
int rightBlackHeight = getBlackHeight(node->Right);
if (leftBlackHeight == -1 || rightBlackHeight == -1 || leftBlackHeight != rightBlackHeight)
{
return -1; // 불일치 또는 오류
}
return leftBlackHeight + (node->Color == eColor::Black ? 1 : 0);
}
bool validateRedBlackTree()
{
return validateProperties(mRoot) != -1;
}
int validateProperties(Node* node)
{
if (node == NIL)
{
return 1; // NIL 노드는 검정색이므로 높이 1
}
// 속성 4: 빨강 노드의 자식은 반드시 검정
if (node->Color == eColor::Red)
{
if (node->Left->Color == eColor::Red || node->Right->Color == eColor::Red)
{
return -1; // 위반
}
}
int leftBlackHeight = validateProperties(node->Left);
int rightBlackHeight = validateProperties(node->Right);
if (leftBlackHeight == -1 || rightBlackHeight == -1 || leftBlackHeight != rightBlackHeight)
{
return -1; // 불일치 또는 오류
}
return leftBlackHeight + (node->Color == eColor::Black ? 1 : 0);
}
C++
복사
심층 비교: AVL vs 레드-블랙 트리
핵심 차이: 균형 조건의 엄격함
AVL: 엄격한 균형 (높이 차이 ≤ 1)
삽입 예시: 1, 2, 3, 4, 5 순서대로
1 삽입: 1
2 삽입: 1 → 회전! → 2
\ / \
2 1
3 삽입: 2 → 회전! → 2
/ \ / \
1 3 1 3
4 삽입: 연쇄 회전 발생...
→ 최악의 경우 log n번 회전 필요
Plain Text
복사
레드-블랙: 느슨한 균형 (검정 높이 동일)
같은 삽입: 1, 2, 3, 4, 5
1 삽입: ⚫1
2 삽입: ⚫2 (1회 회전 + 색상 변경)
/ \
🔴1
3 삽입: ⚫2 (색상 변경만!)
/ \
🔴1 🔴3
→ 각 삽입마다 최대 2회 회전
Plain Text
복사
성능 비교: 100개 노드 삽입 시
AVL 트리:
- 평균 회전: ~50회
- 최악 회전: ~664회
- 높이: ~7
- 탐색: ⭐⭐⭐⭐⭐
레드-블랙 트리:
- 평균 회전: ~10회 ✓ 5배 빠름!
- 최악 회전: ~200회 ✓ 3배 빠름!
- 높이: ~10
- 탐색: ⭐⭐⭐⭐
결론: 삽입/삭제 빈번하면 레드-블랙이 압도적!
Plain Text
복사
왜 C++ STL은 레드-블랙을 선택했나?
map/set의 실제 사용 패턴:
✓ 삽입: 매우 빈번
✓ 삭제: 빈번
✓ 탐색: 빈번 (but O(log n)이면 충분)
AVL을 선택했다면:
✓ 탐색 10% 빠름
✗ 삽입/삭제 5배 느림 ← 치명적!
✗ 실무 전체 성능 저하
레드-블랙을 선택:
✓ 삽입/삭제 5배 빠름 ← 결정적 장점!
✓ 실무 전반적 성능 우수
△ 탐색 약간 느림 (실용적으로 무시 가능)
Plain Text
복사
BST 정렬 속성 (둘 다 동일)
AVL이든 레드-블랙이든:
1. 둘 다 이진 탐색 트리 (BST)
→ 왼쪽 < 부모 < 오른쪽 유지
2. 중위 순회 = 오름차순 정렬
→ 자동 정렬!
예시: 20, 10, 30, 5, 15 삽입 후
AVL: 레드-블랙:
20 20
/ \ / \
10 30 10 30
/ \ / \
5 15 🔴5 🔴15
중위 순회: 5→10→15→20→30 (동일!)
차이는 밸런싱 방식일 뿐,
BST 속성은 둘 다 유지!
Plain Text
복사
최종 완성 코드










