- 삽입/삭제는 BST와 동일하지만, 삽입/삭제 후 RB Tree의 5가지 속성을 만족하기 위한 재조정 작업이 필요하다.
- 스스로 좌/우 서브트리의 균형을 맞춘다
- 서브트리 간 height 차이는 최대 2이다.
RB Tree의 속성 5가지
1. 모든 노드는 red 혹은 black
2. 루트 노드는 black
3. 모든 nil 노드는 black
nil 노드
- 존재하지 않음을 의미하는 노드
- 자녀가 없을 때 자녀를 nil 노드로 표기함
- nil노드는 값이 있는 노드와 동등하게 취급
- RB트리에서 leaf노드는 nil노드이다.
4. red의 자녀는 모두 black이다 = red는 연속적으로 존재할 수 없다
5. 임의의 노드에서 자손 nil 노드까지 가는 경로들의 black 수는 같다 ( 자기 자신은 카운트에서 제외 )
5.1 그 black 수를 black height 라고 한다.
RB Tree 삽입/삭제
- 삽입/삭제 시 주로 속성 4, 5를 위반하며, 이들을 해결하려고 구조를 바꾸다 보면 자연스럽게 트리의 균형이 잡힌다.
- 속성을 위반했을 때 트리를 재조정하는 과정에서, 자연스럽게 트리의 균형이 잡히는 것.
- 회전 자체는 BST의 속성을 만족하기 위한 행위이고, 회전은 RB Tree의 속성을 만족하기 위해 이용되는 것이다.
- 삽입하는 노드는 항상 red이다.
-> red인 이유 : 삽입 후에도 5번 속성을 만족하기 위해
2번 속성을 위반했을 경우( root 노드가 black이 아닐 때 )
- root 노드를 black으로 바꾼다
4번 속성을 위반했을 경우의 3가지 case( red가 연속적으로 존재할 때 )
case1
- 삼촌이 red일 경우에 해당한다. (삼촌 : 부모의 형제 )
-> 할아버지 <-> 부모/삼촌의 색깔을 바꾼다
-> 바꿨으면 할아버지 노드를 기준으로 위반한 속성이 있는지 확인한다.
case2
- 삽입한 노드에서 할아버지까지 가는 경로가 꺾여있고 삼촌 노드가 black일 경우
- 꺾이다 : 할아버지가 부모를 가리키는 방향(left, right)이 부모가 자식을 가리키는 방향과 다른 경우
-> “부모 노드를 기준”으로 회전하여 1자로 펴 준다. case3 상태로 만든다.
-> case3 해결법을 진행한다
case3
- 문제 노드에서 할아버지까지 가는 경로가 1자이고 삼촌 노드가 black일 경우(nil 노드도 포함)
-> 부모와 할아버지의 색깔 바꾼 후 “할아버지 기준”으로 회전하기
아래는 문제 노드에서 할아버지까지의 경로가 꺾여있는 경우와 1자인 경우 예제이다.
노드 D가 삽입되어 4번 속성을 위반한 경우이다. (정확히는 case 2와 case 3)
A는 할아버지, B는 부모, D는 문제 노드이다.
위 그림은 A의 left에 B가 있는데, D는 B의 right에 있다. 이것이 꺾인 것이다.
아래 그림은 A의 left에 B가 있는데, D도 B의 left에 있다. 이것이 1자다.
위쪽 그림의 경우 case 2에 해당되므로, 꺾인 경로를 1자로 펴준 후 case 3을 진행해야 한다.
삽입 예제 : case 1 ( 삼촌이 red인 경우 )
D가 삽입되었을 경우, D기준으로 삼촌(C)이 Red이다. 따라서 case 1에 해당한다. (1번)
case 1은 부모/삼촌 <-> 할아버지 색상을 바꾸면 된다. (2번)
하지만 A는 root이기 때문에 여기서 다시 2번 속성을 위반한다. 따라서 root를 Black으로 바꿔준다. (3번)
이제 RB Tree의 모든 속성을 만족한다.
삽입 예제 : case 2 ( 삼촌이 Black이고 경로가 꺾여있을 경우 )
D가 삽입되었을 경우, D기준 삼촌은 C이고 할아버지는 A이다. A까지의 경로가 꺾여 있으므로(1번) "부모 기준" 왼쪽 회전을 통해 1자로 펴줘야 한다.(2번)
case 2는 1자로 펴주는 회전 연산이 전부다. 하지만 case 2를 수행했다면 반드시 case 3을 수행해야 한다.
삽입 예제 : case 3 ( 삼촌이 Black이고 경로가 1자일 경우 )
위 case 2 예제 수행 결과가 case 3에 해당한다.(1번) 따라서 할아버지와 부모의 색깔을 바꾼 후(2번) "할아버지 기준" 오른쪽으로 회전한다.
간혹 회전을 수행할 때 남는 서브트리를 어디에 붙여야 할 지 헷갈릴 수 있다.
위 그림 1번에서 A를 기준으로 오른쪽 회전할 때, D의 right 서브트리(E)는 D의 right에 A가 위치함으로써 연결이 끊기게 된다.
이 때 A도 left 서브트리(D)를 잃게 되는데, D가 놓친 서브트리(E)를 A의 left에 붙여주면 된다.
RB Tree 삭제 ( 여기서의 자녀는 nil 노드를 의미하지 않는다 즉, 값이 있는 노드를 의미 )
### 삭제되는 색이 red라면 어떠한 속성도 위반하지 않는다!!! ###
삭제되는 색을 먼저 알아야 한다. 삭제할 노드의 색이 삭제되는 색이 아닐 수 있다.
- 삭제하려는 노드의 자녀가 2개일 경우
-> 삭제되는 색 : successor(후임자)의 색, 삭제된 노드 위치로 대체된 후임자는 해당 위치의 색을 물려받는다.
- 삭제하려는 노드의 자녀가 없거나 1개일 경우
-> 삭제되는 색 : 삭제되는 노드의 색
삭제되는 색이 black일 때, 대부분의 상황에서 5번을 위반하게 된다.
5번을 위반했을 경우
-> 삭제된 black노드 위치에 extra black 부여 또는 삭제된 위치를 대체한 노드에게 부여
-> extra black이 부여된 black 노드는 doubly black 이라고 한다.
-> extra black이 부여된 red 노드는 red-and-black 이라고 한다.
-> extra black은 black height를 계산할 때 함께 계산되어, 결과적으로 black을 삭제하더라도 5번을 위반하지 않게 만든다.
red-and-black 해결 방법
- red-and-black을 black으로 바꿔주면 해결된다.
doubly black 해결 방법
- 4가지 case가 존재한다
- case는 doubly black의 형제의 색과, 그 형제의 자녀들의 색에 의해 분류된다.
doubly black을 제거하기 위한 4가지 case
case 4
doubly의 형제가 black이고 형제의 자식 red노드가 1자일 때
-> 부모의 색상을 형제가 받고, 부모와 형제의 자식을 black으로 만든 후 doubly 쪽으로 회전
case 3
doubly의 형제가 black이고 형제의 자식 red노드가 꺾여있을 때
-> 형제 자식과 형제 색깔을 바꾼 후 회전(1자로 만들기)하여 case 4 상태로 만든다.
-> 이후 case 4 해결법 진행
case 2
doubly의 형제가 black이고 자식도 다 black일 때
- doubly와 형제의 black을 부모에게 전달하고, 부모 레벨에서 해결하기( case 1, 2, 3, 4중 하나가 될 수 있다 )
- 이럴 경우 doubly 노드는 일반 black 노드가 되고, 형제는 red 노드가 된다.
case 1
doubly의 형제가 red일 때
-> 형제와 부모의 색상을 바꾼 후 doubly 쪽으로 회전
-> doubly에게 black 형제를 만들어 주는 작업이다. 이후 case 2, 3, 4 중 하나의 해결책을 찾는다.
삭제 예제 : case 1 ( doubly의 형제가 red일 때 )
A가 doubly black이고 형제(D)가 red이다. 따라서 case 1에 해당하며(1번), 형제와 부모의 색깔을 바꾼 후 doubly 쪽으로 회전하면 된다.(2번)
삭제 예제 : case 2 ( doubly의 형제가 black이고 형제의 자식들이 모두 black일 때 )
A가 doubly black이고 형제(D)가 black이다. 그리고 형제의 자식들(C, E)도 black이다.(1번) 이 상황에선 doubly black을 제거할 수 없기 때문에, black을 모아 부모(B)에게 전달하여 부모 레벨부터 올바른 case를 찾아 해결해야 한다.(2번)
이 때 부모(B)는 case 1, 2, 3, 4 중 1개에 해당할 수 있다.
삭제 예제 : case 3 ( doubly의 형제가 black이고 형제의 자식 red가 꺾여있을 때 )
doubly의 형제(D)의 자식 red(C)가 꺾여 있다.(1번) 이 상황에서 바로 해결하는 건 불가능하고, 1자로 펴서 case 3형태로 만든 후 해결해야 한다.
먼저 형제(D)와 red(C)의 색을 바꾸고(2번) 형제(D) 기준으로 회전하여 1자로 펴 준다.(3번) 이후 case 4를 수행한다.
case 3을 수행했다면, 반드시 case 4를 수행해야 한다.
삭제 예제 : case 4 ( doubly의 형제가 black이고 형제의 자식 red가 1자일 때 )
doubly(A)의 형제(D)의 red자식(E)이 1자로 뻗어 있다.(1번) 이 상태는 case 4에 해당하며, 이 상황을 해결할 수 있는 공식이 있다.
형제(D)는 부모(B)의 색깔을 받고, 부모(B)와 red자식(E)는 black으로 만들고(2번) doubly 쪽으로 회전하면 된다.(3번)
// case 4를 했으면 무조건 doubly가 없어지므로, while문 탈출을 위해 root를 넣어준다.
p=t->root;
}
// case 4 : 형제는 black, 형제 red자식이 1자일 경우
else {
// 형제는 부모 색 물려받기
// 부모/형제자식은 black으로 만들고 회전
// 이러면 doubly는 없어진다.
w->color=p->parent->color;
p->parent->color=RBTREE_BLACK;
w->left->color=RBTREE_BLACK;
right_rotate(t, p->parent);
// case 4를 했으면 무조건 doubly가 없어지므로, while문 탈출을 위해 root를 넣어준다.
p=t->root;
}
}
}
p->color=RBTREE_BLACK;
}
이것도 코드가 좀 긴데.. 왼쪽 오른쪽만 바뀐 로직이 2개이기 때문에 그렇다.
우선, 입력받은 노드(p)가 root거나 red였다면 while문을 거치지 않고 맨 아래로 내려와 바로 black으로 바꿔준다.
root 노드는 원래 바로 black으로 바꿔줘도 되고, red였다면 red and black이므로 이것도 바로 black으로 바꿔주면 되기 때문이다.
만약 입력받은 노드(p)가 doubly black이라면 while문을 거치게 된다.
p의 형제 노드는 w에 저장하고 로직을 시작한다. 만약 형제(w)가 red라면 case 1에 해당되어, 형제와 부모의 색깔을 바꾸고 rotate한다. 이제 형제가 바뀌었으므로 w를 재지정해준 후 다음 케이스(2 또는 3 또는 4)로 넘어간다.
형제(w)가 black인데 형제의 왼쪽/오른쪽 자식이 모두 black일 경우 case 2에 해당한다. 이 상황은 p가 doubly black이고 형제인 w가 black인 상황인데, 이 둘의 black을 모아 부모에게 전달해야 한다. 그러면 w는 red가 되고 p는 일반 black이 된다.
이제 부모가 extra black을 갖고 있으므로, p->parent를 기준으로 로직을 다시 시작한다. case 1, 2, 3, 4가 해당될 수 있다.
형제(w)가 black이고 형제의 red자식이 꺾여있을 경우 case 3에 해당한다. case 3은 꺾여있는 걸 1자로 풀어주기만 하면 된다.
형제(w)와 형제의 red자식의 색깔을 바꾸고 회전하여 1자로 만든다. 회전으로 인해 형제가 바뀌었으니 w를 재지정 해준 후 바로 case 4를 진행한다. case 3을 진행했으면 반드시 case 4를 해야 한다.
형제(w)가 black이고 형제의 red자식이 1자일 경우 case 4에 해당한다.
형제(w)는 부모(p->parent)의 색을 물려받고, 부모(p->parent)와 형제의 red자식은 black으로 만든 후 doubly black(p)방향으로 회전한다.