이진탐색트리(Binary Search Tree)이란?
이진탐색트리란 다음과 같은 특징을 갖는 이진트리를 말한다. ( #이진트리 - 각 노드의 자식 노드가 최대 2개인 트리)
1. 각 노드에 중복되지 않는 키(key)가 있다.
2. 루트노드의 왼쪽 서브 트리는 해당 노드의 키보다 작은 키를 갖는 노드들로 이루어져 있다.
3. 루트노드의 오른쪽 서브 트리는 해당 노드의 키보다 큰 키를 갖는 노드들로 이루어져 있다.
4. 좌우 서브 트리도 모두 이진 탐색 트리여야 한다.
이진 탐색 트리의 특징
이진 탐색 트리는 기존 이진트리보다 탐색이 빠르다. 이진 탐색 트리의 탐색 연산은 트리의 높이(height)가 h라면 O(h)의 시간 복잡도를 갖는다.
이진 탐색 트리 탐색(Search)
이진 탐색 트리의 탐색은 다음과 같은 과정을 거친다.
1. 루트 노드의 키와 찾고자 하는 값을 비교한다. 찾고자 하는 값이라면 탐색을 종료한다.
2. 찾고자 하는 값이 루트 노드의 키보다 작다면 왼쪽 서브 트리로 탐색을 진행한다.
3. 찾고자 하는 값이 루트노드의 키보다 크다면 오른쪽 서브트리로 탐색을 진행한다.
위 과정을 찾고자 하는 값을 찾을 때까지 반복해서 진행한다. 만약 값을 찾지 못한다면 그대로 종료한다.
이러한 탐색 과정을 거치면 최대 트리의 높이(h)만큼의 탐색이 진행되게 된다. 예를 들어보자.
위와 같은 트리에서 키가 5인 노드를 찾고자 한다면, 가장 먼저 루트 노드와의 비교가 이루어진다.
5가 7보다 작으므로 왼쪽 서브 트리로의 탐색이 이루어지고, 이후 5가 3보다 크므로 오른쪽 서브트리로 탐색이 이루어진다. 마지막으로 키가 5인 노드를 찾았으므로 탐색이 종료된다. 즉 트리의 높이인 3번 만큼의 탐색이 이루어졌다. 만약 8을 찾는다면 2번의 연산이 진행되었을 것이다. 즉, 트리 안의 값을 찾는다면 무조건 트리의 높이(h) 이하의 탐색이 이루어지게 된다.
트리 안에 찾고자 하는 값이 없더라도 최대 h 번 만큼만의 탐색이 진행된다. 예를 들어 위 트리에서 6이라는 값을 찾는다고 하면 위 그림과 같은 과정을 거치고 탐색이 종료된다. (직접 위 그림에서 과정을 생각해보자) 마지막으로 탐색하게 되는 5를 키로 갖는 노드에서 6은 5보다 크므로 오른쪽 서브트리로 탐색을 진행해야 하는데 오른쪽 서브 트리가 없으므로 탐색이 종료되는 것이다.
이진 탐색 트리 탐색 소스코드
TreeNode* search(TreeNode* root, int key){
if(root == NULL){ // 값을 찾지 못한 경우
return NULL;
}
if(key == root->key){ // 값을 찾음
return root;
}
else if(key < root->key){ // 왼쪽 서브트리 탐색
search(root->left, key);
}
else if(key > root->key){ // 오른쪽 서브트리 탐색
search(root->right, key);
}
}
이진 탐색 트리의 탐색을 C언어로 구현하면 재귀를 이용할 수 있다. 재귀가 아닌 for문을 통해서도 구현이 가능하지만 직관적으로 이해가 쉽게 재귀로 구현하였다.
이진 탐색 트리 삽입(insert)
이진 탐색트리의 삽입은 다음과 같은 과정을 거친다. 탐색과 과정이 비슷하다.
1. 삽입할 값을 루트 노드와 비교해 같다면 오류를 발생한다( 중복 값 허용 X )
2. 삽입할 값이 루트 노드의 키보다 작다면 왼쪽 서브 트리를 탐색해서 비어있다면 추가하고, 비어있지 않다면 다시 값을 비교한다.
3. 삽입할 값이 루트노드의 키보다 크다면 오른쪽 서브트리를 탐색해서 비어있다면 추가하고, 비어있지 않다면 다시 값을 비교한다.
예를 들어 아래와 같은 트리에 6을 키로 가진 노드를 추가한다고 하자.
탐색과 비슷하게 삽입하고자 하는 값을 계속해서 비교해서 삽입할 위치를 찾는다.
삽입할 위치가 5의 오른쪽 서브 트리인 것을 찾았으므로, 5의 오른쪽 자식으로 6을 추가하면 된다.
이진 탐색 트리 삽입 소스코드
void insert(TreeNode** root, int key){
TreeNode* ptr; // 탐색을 진행할 포인터
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode)); // newNode 생성
newNode->key = key;
newNode->left = newNode->right = NULL;
if(*root == NULL){ // 트리가 비어 있을 경우
*root = newNode;
return;
}
ptr = *root; // root 노드부터 탐색 진행
while(ptr){
if(key == ptr->key){ // 중복값
printf("Error : 중복값은 허용되지 않습니다!\n");
return;
}else if(key < ptr->key){ // 왼쪽 서브트리
if(ptr->left == NULL){ // 비어있다면 추가
ptr->left = newNode;
return;
}else{ // 비어있지 않다면 다시 탐색 진행
ptr= ptr->left;
}
}else{ // key > ptr->key 오른쪽 서브트리
if(ptr->right == NULL){ // 비어있다면 추가
ptr->right = newNode;
return;
}else{ // 비어있지 않다면 다시 탐색 진행
ptr = ptr->right;
}
}
}
}
이진 탐색 트리의 삭제(delete)
이진탐색트리의 삭제는 삽입보다 조금 더 복잡하다. 이진 탐색 트리에서 특정 노드를 삭제할 때 아래와 같은 3가지 상황을 나누어 구현해야 한다.
1. 삭제하려는 노드가 단말 노드(leaf node) 일 경우
2. 삭제하려는 노드의 서브 트리가 하나인 경우(왼쪽 혹은 오른쪽 서브 트리)
3. 삭제하려는 노드의 서브 트리가 두 개인 경우
1. 삭제하려는 노드가 단말 노드(leaf node) 일 경우
자식이 없는 단말 노드의 삭제는 간단하다. 삭제할 노드의 부모 노드가 있다면 부모 노드의 자식 노드를 NULL로 만들고, 삭제할 노드를 삭제(메모리 해제) 해주면 된다.
2. 삭제하려는 노드의 서브 트리가 하나인 경우(왼쪽 혹은 오른쪽 서브 트리)
삭제하려는 노드의 서브 트리가 하나인 경우도 간단하다. 삭제할 노드의 자식노드를 삭제할 노드의 부모노드가 가리키게 하고 해당 노드를 삭제하면 된다.
3. 삭제하려는 노드의 서브트리가 두 개인 경우
삭제하려는 노드의 서브트리가 두 개인 경우는 가장 복잡하다. 이 경우 두 가지 방법을 사용할 수 있다.
1) 삭제할 노드 왼쪽 서브 트리의 가장 큰 자손을 해당 노드의 자리에 올린다.
위와 같이 삭제할 노드의 왼쪽 서브 트리에서 가장 큰 자손을 해당 노드의 자리에 올리면, 이진 탐색 트리의 조건을 만족하면서 트리가 유지되는 것을 확인할 수 있다. 또한 자리를 옮기면서 다른 노드들(4번노드)도 자리가 적절히 이동한 것을 확인 할 수 있다
2) 삭제할 노드 오른쪽 서브 트리의 가장 작은 자손을 해당 노드의 자리에 올린다.
위와 같이 삭제할 노드의 오른쪽 서브 트리에서 가장 작은 자손을 해당 노드의 자리에 올리면, 이진 탐색 트리의 조건을 만족하면서 트리가 유지되는 것을 확인할 수 있다. 또한 자리를 옮기면서 다른 노드들(10번노드)도 자리가 적절히 이동한 것을 확인 할 수 있다.
이진 탐색 트리의 삭제 소스코드
TreeNode* delete_node(TreeNode* root, int key){
TreeNode* del = NULL; // 삭제할 노드
TreeNode* parent = NULL; // 삭제할 노드의 부모 노드
TreeNode* successor = NULL; // 삭제할 노드의 왼쪽 서브트리에서 가장 큰 노드
TreeNode* predecessor = NULL; // successor의 부모노드
TreeNode* child = NULL; // 삭제할 노드의 자식 노드
del= root;
while(del != NULL){ // 삭제할 노드 탐색
if(key == del->key){
break;
}
parent = del;
if(key < del->key){
del = del->left;
}else{
del = del->right;
}
}
if(del == NULL){
printf("Error : 존재하지 않는 키\n");
return root;
}
if(del->left == NULL && del->right == NULL){ // 삭제할 노드의 자식노드가 없는 경우
if(parent != NULL){ // 부모노드가 있는 경우
if(parent->left == del){ // 부모노드의 왼쪽노드가 삭제할 노드일 때
parent->left = NULL;
}else{ // 오른쪽 일 때
parent->right = NULL;
}
}else{ // 부모노드가 없는 경우 = root 노드
root = NULL;
}
}else if(del->left != NULL && del->right != NULL){ // 삭제할 노드의 자식 노드가 2개인 경우
predecessor = del;
successor = del->left;
while(successor->right != NULL){ // 왼쪽 서브트리에서 가장 큰 값 찾기
predecessor = successor;
successor = successor->right;
}
predecessor->right = successor->left; // successor의 자식 노드 위치 변경
successor->left = del->left; // successor를 삭제할 노드의 위치로 옮긴 것과 같음
successor->right = del->right;
if(parent != NULL){ // 삭제할 노드의 부모노드가 있을 때
if(parent->left == del){
parent->left = successor;
}else{
parent->right = successor;
}
}else{
root = successor;
}
}else{ // 삭제할 노드의 자식 노드가 1개인 경우
if(del->left != NULL){ // 왼쪽 노드
child = del->left;
}else{ // 오른쪽 노드
child = del->right;
}
if(parent != NULL){ // 부모노드가 있는 경우
if(parent->left == del){ // 부모노드의 왼쪽 노드로 삭제할 노드의 자식노드 연결
parent->left = child;
}else{ // 부모노드의 오른쪽 노드로 삭제할 노드의 자식노드 연결
parent->right = child;
}
}else{
root = child;
}
}
free(del); // 메모리해제
return root;
}
출처: https://code-lab1.tistory.com/10 [코드 연구소:티스토리]
'CS > 자료구조 & 알고리즘' 카테고리의 다른 글
해시 테이블 (0) | 2022.06.12 |
---|---|
레드-블랙 트리(Red-Black Tree) (0) | 2022.06.12 |
트리 (0) | 2022.06.12 |
트리 구현 (0) | 2022.06.11 |
[C] 인접 행렬로 그래프 구현 (0) | 2022.06.11 |