크래프톤 정글
RB-Tree 구현하기 (C언어) - Part.6 삭제 구현
고웅
2025. 4. 22. 11:21
지난 시간에 간단하게 RB-Tree에서의 삭제를 이론적으로 살펴봤다. 이제 실제 구현을 통해 삭제에 대한 것을 설명하겠다.
1. 구현 목표
우리는 삭제 기능을 구현하기에 앞서 다음과 같은 기능들을 구현해야 한다.
- 노드 삭제
- 노드 삭제 시 노드 연결
- 삭제 시 fixup 함수
- 노드 기준 최소 값 찾기 (successor 탐색)
2. 기능 구현
🔨 노드 삭제
int rbtree_erase(rbtree *t, node_t *p) {
// TODO: implement to_erase
}
먼저 노드 삭제의 진입 함수인 erase 함수를 구현할 것이다. 이 함수는 BST의 삭제 방법에 따라 노드를 삭제하는 역할을 한다.
구현 아이디어:
- successor, replacement 라는 노드를 가리킬 포인터 변수 생성
- 삭제되는 노드의 색상을 담을 변수 생성(우리는 열거형-enum으로 색상을 관리한다)
- 조건문 if (삭제하는 노드의 왼쪽 자식이 nil일때)
- p노드가 삭제되고 대체될 변수는 p의 오른쪽 자식이 될 것이다.
- transplant() 함수를 사용한다. -> 이 함수는 추 후 구현하겠다.
- 조건문 else if (삭제하는 노드의 오른쪽 자식이 nil일 때)
- p노드가 삭제되고 대체될 변수는 p의 왼쪽 자식이 될 것이다.
- transplant() 함수를 사용한다. -> 이 함수는 추 후 구현한다.
- 조건문 else (삭제하려는 노드의 자식이 둘일 경우)
- 삭제하려는 노드의 후임자를 찾아야 한다. -> 이 함수도 추후 구현한다.
- 삭제되는 노드의 후임자 색을 저장한다.
- replacement는 실제로 successor 자리에 올라올 노드이다.
우리가 삭제를 한다는 것은 리프 노드에서 일어난다. 즉 트리 중간의 값을 삭제하는 경우 해당 노드가 삭제되는 것이 아니라 그중 후임자가 삭제되는 것이다. - 조건문 if (후임자의 부모가 p인 경우)
- replacement의 부모는 successor가 맞기 때문에 그대로 둔다.
- 조건문 else (후임자의 부모가 p가 아닌 경우)
- 먼저 successor가 p의 오른쪽 서브트리로 치환해서 트리에서 successor를 제거한다.
- 그다음 successor에 p의 오른쪽 자식을 붙인다.
- successor를 successor의 오른쪽 자식의 부모로 설정한다.
- 삭제 대상 p를 successor로 교체한다. 즉 p 자리에 successor가 올라온다.
- p의 왼쪽 자식을 successor의 왼쪽으로 붙인다.
- 이제 successor는 p의 왼쪽, 오른쪽 자식을 모두 가지게 된다.
- successor는 원래 RED일 수도 있는데, p의 색을 그대로 승계시켜야 한다.
- 삭제되는 노드의 색이 검은색이면 속성 위반이 발생하니 fixup 함수를 호출한다.
- 삭제하고자 하는 노드 p의 메모리를 해제한다.
굉장히 복잡하다. 위 설명만을 보고 이해하기 어려울 수 있으니 필요하다면 확인할 수 있게 완성 코드를 첨부한다.
더보기
int rbtree_erase(rbtree *t, node_t *p) {
// TODO: implement to_erase
node_t *successor = p;
node_t *replacement;
color_t original_color = p->color;
if (p->left == t->nil) {
replacement = p->right;
rb_transplant(t, p, replacement);
} else if (p->right == t->nil) {
replacement = p->left;
rb_transplant(t, p, replacement);
} else { // 삭제하려는 노드의 자녀가 둘인 경우
// 삭제하려는 노드 기준 successor 즉 후임자를 찾는 과정
successor = rb_tree_min_subtree(t, p->right);
// 삭제되는 노드의 successor의 색을 저장
original_color = successor->color;
replacement = successor->right;
// successor 노드를 기존 노드에서 분리하기 위한 작업
if (successor->parent == p) {
replacement->parent = successor;
}
else {
rb_transplant(t, successor, replacement);
successor->right = p->right;
successor->right->parent = successor;
}
// 분리된 successor 노드를 삭제하려는 노드에 대체하는 작업
rb_transplant(t, p, successor);
successor->left = p->left;
successor->left->parent = successor;
successor->color = p->color;
}
if (original_color == RBTREE_BLACK) {
rb_delete_fixup(t, replacement);
}
free(p);
return 0;
}
🔨 노드 바꾸기
이제 위 코드에서 누락했던 함수 기능을 하나씩 구현해 보자. 먼저 노드를 바꾸는 함수다.
void rb_transplant(rbtree *t, node_t *p, node_t *replacement) {
}
이 함수의 목적은 트리 내에서 어떤 노드 p를 다른 노드 replacement로 바꾸는 함수다.
- p 노드 자리에 replacement 노드를 올리는 작업
- 부모 포인터만 바꿔주는 함수
- 실제로 자식 연결이나 노드 데이터 교체는 하지 않음
구현 아이디어:
- 삭제 대상 p 가 루트 노드일 경우 - 트리의 루트를 replacement로 교체
- p가 부모의 왼쪽 자식일 경우 - 부모의 왼쪽 포인터를 replacement 로 변경
- p가 부모의 오른쪽 자식일 경우 - 부모의 오른쪽 포인터를 replacement 로 변경
- replacement의 부모 포인터도 p의 부모로 바꾼다.
더보기
void rb_transplant(rbtree *t, node_t *p, node_t *replacement) {
if (p->parent == t->nil) {
t->root = replacement;
} else if (p == p->parent->left) {
p->parent->left = replacement;
} else {
p->parent->right = replacement;
}
replacement->parent = p->parent;
}
🔨 후임자 찾기 Successor
node_t *rb_tree_min_subtree(rbtree *t, node_t *start) {
}
이 함수는 삭제 대상 노드의 후임자를 찾는 함수이다.
구현 아이디어:
- 현재 노드를 지정할 cur 선언
- 반복문을 진행한다. - 현재 노드의 왼쪽 자식이 nil일 때까지
- 현재 노드를 현재 노드의 왼쪽 자식으로 선택한다.
- 선택된 노드를 반환한다.
더보기
node_t *rb_tree_min_subtree(rbtree *t, node_t *start) {
node_t *cur = start;
while (cur->left != t->nil) {
cur = cur->left;
}
return cur;
}
🔨 fixup 함수 (가장 중요)
이제 삭제 기능에서 가장 중요한 속성 위반을 고쳐줄 함수를 구현한다.
void rb_delete_fixup(rbtree *t, node_t *target) {
}
삭제를 진행할 때 double black 인 경우가 발생하는데 이를 해결해야 한다.
구현 아이디어 :
- while을 사용하여 현재 노드가 루트가 아니면 서 삭제할 노드의 색상이 Black 일 때만 반복한다.
- node_parent, sibling 이란 노드 포인터 변수를 선언한다.
- 조건문 if (삭제된 노드가 부모의 왼쪽 자식인 경우)
- 형재 노드를 부모의 오른쪽 노드로 선택한다.
- 조건문 if (형제의 색깔이 RED 인경우) -> case 1
- 형재의 색을 Black으로 바꾼다.
- 타깃의 부모의 색을 RED로 바꾼다.
- 부모를 기준으로 왼쪽으로 회전시킨다.
- 조건문 else (형제의 색깔이 Black인 경우)
- 조건문 if (형제의 왼쪽 자식의 색이 Black이면서 형제의 오른쪽 자식의 색이 Black인 경우) -> case 2
- 형제의 색을 RED로 설정
- 타깃을 타깃의 부모로 변경
- 조건문 else (형제의 왼쪽 자식 중 RED인 자식이 있는 경우)
- 조건문 if (형제의 오른쪽 자식이 Black일 경우) -> case 3
- 형제가 Black이고, 왼쪽 자식만 Red면 → 오른쪽 Red로 만드는 구조로 바꾸기 위해 sibling을 회전한다.
- 이 회전은 Case 4로 진입하기 위한 전처리다
- 형제의 오른쪽 자식이 Red이면 최종적으로 균형을 복구할 수 있다.
- 색을 재조정하고, 부모를 중심으로 회전하여 Double Black 상태를 완전히 제거
- target = t->root;로 설정하여 루프 종료 시킨다 (루트에 도달한 것으로 처리)
- 조건문 if (형제의 오른쪽 자식이 Black일 경우) -> case 3
- 조건문 if (형제의 왼쪽 자식의 색이 Black이면서 형제의 오른쪽 자식의 색이 Black인 경우) -> case 2
- 조건문 else(삭제된 노드가 부모의 오른쪽 자식인 경우)
- 위의 과정을 반대로 진행한다.
- 타깃의 색상을 Black으로 만들어주면서 끝이 난다.
더보기
void rb_delete_fixup(rbtree *t, node_t *target) {
while (target != t->root && target->color != RBTREE_RED) {
node_t *node_parent = target->parent;
node_t *sibling;
if (target == node_parent->left) {
// target이 부모의 왼쪽 자식인 경우
sibling = node_parent->right;
if (sibling->color == RBTREE_RED) { // case 1
sibling->color = RBTREE_BLACK;
target->parent->color = RBTREE_RED;
rb_left_rotation(t, target->parent);
sibling = target->parent->right;
}else {
// 형제의 색이 검정색일 때 조건문 진입
// case 2
if (sibling->left->color == RBTREE_BLACK && sibling->right->color == RBTREE_BLACK) {
sibling->color = RBTREE_RED;
target = target->parent;
} else {
// case 3 처리
if (sibling->right->color == RBTREE_BLACK) {
sibling->left->color = RBTREE_BLACK;
sibling->color = RBTREE_RED;
rb_right_rotation(t,sibling);
sibling = node_parent->right;
}
// case 4 처리
sibling->color = node_parent->color;
node_parent->color = RBTREE_BLACK;
sibling->right->color = RBTREE_BLACK;
rb_left_rotation(t, node_parent);
target = t->root;
}
}
}
else {
// target이 부모의 오른쪽 자식인 경우
sibling = node_parent->left;
if (sibling->color == RBTREE_RED) { // case 1
sibling->color = RBTREE_BLACK;
target->parent->color = RBTREE_RED;
rb_right_rotation(t, target->parent);
sibling = target->parent->left;
}else {
// 형제의 색이 검정색일 때 조건문 진입
// case 2
if (sibling->left->color == RBTREE_BLACK && sibling->right->color == RBTREE_BLACK) {
sibling->color = RBTREE_RED;
target = target->parent;
} else {
// case 3 처리
if (sibling->left->color == RBTREE_BLACK) {
sibling->right->color = RBTREE_BLACK;
sibling->color = RBTREE_RED;
rb_left_rotation(t,sibling);
sibling = node_parent->left;
}
// case 4 처리
sibling->color = node_parent->color;
node_parent->color = RBTREE_BLACK;
sibling->left->color = RBTREE_BLACK;
rb_right_rotation(t, node_parent);
target = t->root;
}
}
}
}
target->color = RBTREE_BLACK;
}
기능 구현이 종료되었다. 만약 설명을 봐도 이해가 안 된다면 정상이다. 쉽게 이해할 수 없을 정도로 보기만 하면 매우 어렵게 느껴졌다. 위 코드를 작성한 본인도 설명하라고 하면 버벅거리거나 막히는 부분이 많을 것이다. 그럴 때는 아래 출처 혹은 참조에 있는 자료를 반복해서 보다 보면 이해가 될 것 같다.
출처
https://youtu.be/6drLl777k-E?si=zPEX5ev8GapzBIE2