22.6 균형 잡힌 이진 검색 트리 직접 구현하기:트립
문제는 AVL트리나 레드 블랙 트리 등 교과서에 실리는 대부분의 균형 잡힌 이진 검색 트리들은 구현이 매우 까다로움. 구현이 간단한 이진 검색 트리를 사용함. 대표적인 것은 트립(treap).
트립의 정의
트립은 입력이 특정 순서로 주어질 때 그 성능이 떨어진다는 이진 검색 트리의 단점을 해결하기 위해 고안된 일종의 랜덤화 된 이진 검색 트리임. 트리의 형태가 원소들의 추가 순서에 따라 결정되지 않고 난수에 의해 임의대로 결정됨. 이와 같은 속성을 유지하기 위해 트립은 새 노드를 추가될 때마다 해당 노드에 우선순위를 부여함. 트립은 항상 부모의 우선순위가 자식의 우선순위보다 높은 이진 검색 트리를 만듭니다. 트립의 조건을 다음 두 가지로 정리할 수 있음. 트립을 이해하는 또 다른 방법은 노드들을 우선순위가 높은 것부터 순서대로 추가한 이진 검색 트리라고 생각하는 것임.
트립의 높이
랜덤화가 의미가 있으려면 노드의 순서를 임의로 바꾸었을 때 트리 높이의 기대치가 O(N) 보다 작아야 함. 이것을 증명하는 한 가지 방법은 트리를 한 단계 내려갈 때마다 후보가 되는 원소의 수가 선형으로 줄어드는 것이 아니라 지수적으로 줄어든다는 점을 보이는 것임. 우선수위를 임의로 부여하기 때문에 모든 값은 루트가 될 가능성이 같고, 따라서 1부터 ㅇN까지의 모든 r의 출현 확률이 같음. 따라서 모든 r에 대한 값의 평균을 취하도록 함. 다시 말해 한 단계 내려갈 때마다 후보의 수가 평균적으로 2/3으로 줄어든다는 것임.
트립의 구현
typedef int KeyType;
struct Node {
KeyType key;
int priority, size;
Node* left, * right;
Node(const KeyType& _key) :key(_key), priority(rand()), size(1), left(NULL), right(NULL) {}
void setLeft(Node* newLeft) { left = newLeft; calcSize(); }
void setRight(Node* newRight) { right = newRight; calcSize(); }
void calcSize() {
size = 1;
if (left) size += left->size;
if (right) size += right->size;
}
};
트립의 노드 객체는 포함하는 원소 key 외에도 우선순위 priority를 가진다는 점임. 또 하나 유의할 점은 자신을 루트로 하는 서브트리에 포함된 노드의 수를 저장하는 size멤버임.
노드의 추가와 '쪼개기' 연산
typedef pair<Node*, Node*> NodePair;
NodePair split(Node* root, KeyType key) {
if (root == NULL) return NodePair(NULL, NULL);
if (root->key < key) {
NodePair rs = split(root->right, key);
root->setRight(rs.first);
return NodePair(root, rs.second);
}
NodePair ls = split(root->left, key);
root->setLeft(ls.second);
return NodePair(ls.first, root);
}
Node* insert(Node* root, Node* node) {
if (root == NULL) return node;
if (root->priority < node->priority) {
NodePair splitted = split(root, node->key);
node->setLeft(splitted.first);
node->setRight(splitted.second);
return node;
}
else if (node->key < root->key)
root->setLeft(insert(root->left, node));
else
root->setRight(insert(root->right, node));
return root;
}
노드의 삭제와 '합치기'연산
merge() 내부에서 두 노드 중 하나가 NULL인지 확인하기 때문에 root가 한쪽 자손만 갖고 있는 경우, 그리고 root가 자손이 없는 경우 등을 모두 별도의 예외 처리 없이 처리할 수 있음.
Node* merge(Node* a, Node* b) {
if (a == NULL) return b;
if (b == NULL) return a;
if (a->priority < b->priority) {
b->setLeft(merge(a, b->left));
return b;
}
a->setRight(merge(a->right, b));
return a;
}
Node* erase(Node* root, KeyType key) {
if (root == NULL) return root;
if (root->key == key) {
Node* ret = merge(root->left, root->right);
delete root;
return ret;
}
if (key < root->key)
root->setLeft(erase(root->left, key));
else
root->setRight(erase(root->right, key));
return root;
}
K번째 원소 찾기
주어진 서브트리의 노드들을 포함한 원소의 크기 순으로 나열했을 때 번째로 오는 노드를 찾는 연산임. 왼쪽 서브트리의 크기가 l이라고 할 때, 다음과 같이 세 개의 경우로 나눌 수 있음.
k<=l : k번째 노드는 왼쪽 서브트리에 있음
k = l +1 : 루트가 k번째 노드임
k > l + 1 : k번째 노드는 오른쪽 서브트리에서 k-l-1번째 노드가 됨.
kth() 함수의 시간 복잡도는 트리의 높이에 비례하기 때문에, 트립 등의 균형 잡힌 이진 검색 트리에 적용해야만 O(lgN) 시간에 수행할 수 있음.
Node* kth(Node* root, int k) {
int leftSize = 0;
if (root->left != NULL) leftSize = root->left->size;
if (k <= leftSize) return kth(root->left, k);
if (k == leftSize + 1) return root;
return kth(root->right, k - leftSize - 1);
}
X보다 작은 원소 세기
int countLessThan(Node* root, KeyType key) {
if (root == NULL) return 0;
if (root->key >= key)
return countLessThan(root->left, key);
int ls = (root->left ? root->left->size : 0);
return ls + 1 + countLessThan(root->right, key);
}
'Algorithms > tree' 카테고리의 다른 글
알고리즘 문제 해결 전략 2 6장 트리 (6) (0) | 2021.07.05 |
---|---|
알고리즘 문제 해결 전략 2 6장 트리 (5) (0) | 2021.07.05 |
알고리즘 문제 해결 전략 2 06장 트리 (3) (0) | 2021.07.03 |
알고리즘 문제 해결 전략 2 06장 트리 (2) (0) | 2021.07.03 |
알고리즘 문제 해결 전략 2 06장 트리 (1) (0) | 2021.07.01 |
댓글