이진 탐색
c++ 같은 경우에 map을 사용할 경우 우리는 아무런 위화감 없이 이진 탐색 트리를 사용할 수 있다. 하지만 면접이나 자료구조를 배울 때는 실제로 구현해 봐야 와닿기 때문에 구현까지 해보겠다.
이진 탐색은 무엇인가. 어떤 배열에 데이터가 정렬되어 있다고 할 때, 특정 데이터를 찾을 때 처음부터 끝까지 순회를 돌아 찾을 경우 O(N)의 시간 복잡도를 가진다.
되게 효율적이지 못하기 때문에 우리가 친구들끼리 놀 때, 업다운게임과 같이 생활의 지혜를 도입한 것이다.
데이터를 찾는 범위를 반으로 줄이고 줄이고 하기 때문에 O(logN)의 시간복잡도를 가진다.
이진 탐색을 구현해보자.
#include <iostream>
#include <vector>
#include <list>
#include <stack>
#include <queue>
using namespace std;
vector<int> numbers;
void BinarySearch(int N)
{
int left = 0;
int right = (int)numbers.size() - 1;
while (left <= right)
{
cout << "탐색 범위 " << left << "~" << right << endl;
int mid = (left + right) / 2;
if (N < numbers[mid])
{
right = mid - 1;
}
else if(N > numbers[mid])
{
left = mid + 1;
}
else
{
cout << " 찾음 " << endl;
break;
}
}
}
int main()
{
numbers = vector<int>{ 1,8,15,23,32,44,56,63,81,91 };
BinarySearch(81);
}
정렬된 연결 리스트로는 불가하다. 임의 접근이 안되기 때문이다.
이진 탐색 트리
위의 이진 탐색의 문제는 데이터를 추가하거나 삭제하는 경우가 빈번한 경우의 벡터를 사용하면 효율적이지 않다는 결론을 내릴 수 있다. 따라서 이진 탐색 트리를 통해 그 문제를 해결 할 수 있다.
root 의 오른쪽은 root보다 무조건 큰 숫자이면 반대는 작은 숫자를 항상 가지고 있어야한다.
#include "BinarySearchTree.h"
#include <iostream>
#include <Windows.h>
using namespace std;
void BinarySearchTree::Insert(int key)
{
Node* newNode = new Node();
newNode->key = key;
if (_root == nullptr)
{
_root = newNode;
return;
}
Node* node = _root;
Node* parent = nullptr;
while (node)
{
parent = node;
if (key < node->key)
node = node->left;
else
node = node->right;
}
newNode->parent = parent;
if (key < parent->key)
parent->left = newNode;
else
parent->right = newNode;
}
void BinarySearchTree::Delete(int key)
{
Node* deleteNode = Search(_root, key);
Delete(deleteNode);
}
void BinarySearchTree::Delete(Node* node)
{
if (node == nullptr)
return;
if (node->left == nullptr)
{
Replace(node, node->right);
}
else if (node->right == nullptr)
Replace(node, node->left);
else
{
Node* next = Next(node);
node->key = next->key;
Delete(next);
}
}
// u 서브트리를 v 서브트리로 교체
void BinarySearchTree::Replace(Node* u, Node* v)
{
if (u->parent == nullptr)
_root = v;
else if (u == u->parent->left)
u->parent->left = v;
else
u->parent->right = v;
if (v)
v->parent = u->parent;
delete u;
}
Node* BinarySearchTree::Search(Node* node, int key)
{
if(node == nullptr || key == node->key)
return node;
if (key < node->key)
return Search(node->left, key);
else
return Search(node->right, key);
}
Node* BinarySearchTree::Min(Node* node)
{
while (node->left)
node = node->left;
return node;
}
Node* BinarySearchTree::Max(Node* node)
{
while (node->right)
node = node->right;
return node;
}
Node* BinarySearchTree::Next(Node* node)
{
if (node->right)
return Min(node->right);
Node* parent = node->parent;
while (parent&& node == parent->right)
{
node = parent;
parent = parent->parent;
}
return parent;
}
void SetCursorPosition(int x, int y)
{
HANDLE output = ::GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { static_cast<SHORT>(x), static_cast<SHORT>(y) };
::SetConsoleCursorPosition(output, pos);
}
void BinarySearchTree::Print(Node* node, int x, int y)
{
if (node == nullptr)
return;
SetCursorPosition(x, y);
cout << node->key;
Print(node->left, x - (5 / (y + 1)), y + 1);
Print(node->right, x + (5 / (y + 1)), y + 1);
}
void BinarySearchTree::Print_Inorer(Node* node)
{
// 전위 순회 (preorder traverse)
// 중위 순회
// 후휘 순회
if (node == nullptr)
return;
cout << node->key << endl;
Print_Inorer(node->left);
Print_Inorer(node->right);
}
#pragma once
struct Node
{
Node* left = nullptr;
Node* right = nullptr;
Node* parent = nullptr;
int key = { };
};
class BinarySearchTree
{
public:
void Print() { Print(_root, 10, 0); }
void Print(Node* node, int x, int y);
void Print_Inorer(Node* node);
void Print_Inorer() { Print_Inorer(_root); }
void Insert(int key);
void Delete(int key);
void Delete(Node* node);
void Replace(Node* u, Node* v);
Node* Search(Node* node, int key);
Node* Min(Node* node);
Node* Max(Node* node);
Node* Next(Node* node);
public:
Node* _root = nullptr;
};
기본적인 동작 방식을 구현해 봤다. 여기서 중요한건 삽입과 삭제 그리고 데이터를 찾는것 까지 모두 중요하다.
하지만 한가지 문제점이 있다면 만약 데이터가 한쪽으로 몰리게 된다면 그건 그냥 연결리스트랑 다른점이 없다.
그렇기에 균형을 맞춰서 넣어야한다.
레드 블랙 트리
트리의 균형이 무너지는 점이 이진 트리의 문제였다. 이 문제를 해결하는데 대표적인 레드 블랙 트리에 대해 알아보자.
레드 블랙 트리는 각각의 노드가 레드나 블랙의 색상 속성을 가지고 있다.
- 노드는 레드 혹은 블랙 중의 하나이다.
- 루트 노드는 블랙이다.
- 모든 리프 노드들은 블랙이다.(NIL, 더미 노드)
- 레드 노드의 자식 노드 양쪽은 언제나 모두 블랙이다. 즉, 레드 노드는 연달아 나타날 수 없으며, 블랙 노드 만이 레드 노드의 부모 노드가 될 수 있다.
- 어떤 노드로부터 시작되어 그에 속학 하위 리프 노드에 도달하는 모든 경로에는 리프 노드를 제외하면 모두 같은 개수의 블랙 노드가 있다.
4번째, 5번째 특성이 레드 블랙 트리를 대표하는 특징이다.
이진 탐색 트리의 일종이기 때문에 위에서 만든 코드를 대부분 재활용할 수 있다.
Red-BlackTree.h
#pragma once
// [10]
// [5] [20]
// [30]
enum class Color
{
Red = 0,
Black = 1,
};
struct Node
{
Node* parent = nullptr;
Node* left = nullptr;
Node* right = nullptr;
int key = {};
Color color = Color::Black;
};
// Red-Black Tree
// 1) 모든 노드는 Red or Black
// 2) Root는 Black
// 3) Leaf(NIL)는 Black
// 4) Red 노드의 자식은 Black (연속해서 Red-Red X)
// 5) 각 노드로부터 ~ 리프까지 가는 경로들은 모두 같은 수의 Black
class BinarySearchTree
{
public:
BinarySearchTree();
~BinarySearchTree();
void Print();
void Print(Node* node, int x, int y);
Node* Search(Node* node, int key);
Node* Min(Node* node);
Node* Max(Node* node);
Node* Next(Node* node);
void Insert(int key);
void InsertFixup(Node* node);
void Delete(int key);
void Delete(Node* node);
void DeleteFixup(Node* node);
void Replace(Node* u, Node* v);
// Red-Black Tree
void LeftRotate(Node* node);
void RightRotate(Node* node);
private:
Node* _root = nullptr;
Node* _nil = nullptr;
};
Red-BlackTree.cpp
#include "BinarySearchTree.h"
#include <iostream>
#include <windows.h>
using namespace std;
enum class ConsoleColor
{
BLACK = 0,
RED = FOREGROUND_RED,
GREEN = FOREGROUND_GREEN,
BLUE = FOREGROUND_BLUE,
YELLOW = RED | GREEN,
WHITE = RED | GREEN | BLUE,
};
void SetCursorColor(ConsoleColor color)
{
HANDLE output = ::GetStdHandle(STD_OUTPUT_HANDLE);
::SetConsoleTextAttribute(output, static_cast<SHORT>(color));
}
void SetCursorPosition(int x, int y)
{
HANDLE output = ::GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { static_cast<SHORT>(x), static_cast<SHORT>(y) };
::SetConsoleCursorPosition(output, pos);
}
void ShowConsoleCursor(bool flag)
{
HANDLE output = ::GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorInfo;
::GetConsoleCursorInfo(output, &cursorInfo);
cursorInfo.bVisible = flag;
::SetConsoleCursorInfo(output, &cursorInfo);
}
BinarySearchTree::BinarySearchTree()
{
_nil = new Node(); // Black
_root = _nil;
}
BinarySearchTree::~BinarySearchTree()
{
delete _nil;
}
void BinarySearchTree::Print(Node* node, int x, int y)
{
if (node == _nil)
return;
SetCursorPosition(x, y);
if (node->color == Color::Black)
SetCursorColor(ConsoleColor::BLUE);
else
SetCursorColor(ConsoleColor::RED);
cout << node->key;
Print(node->left, x - (5 / (y + 1)), y + 1);
Print(node->right, x + (5 / (y + 1)), y + 1);
SetCursorColor(ConsoleColor::WHITE);
}
void BinarySearchTree::Print()
{
::system("cls");
ShowConsoleCursor(false);
Print(_root, 10, 0);
}
Node* BinarySearchTree::Search(Node* node, int key)
{
if (node == _nil || key == node->key)
return node;
if (key < node->key)
return Search(node->left, key);
else
return Search(node->right, key);
}
Node* BinarySearchTree::Min(Node* node)
{
while (node->left != _nil)
node = node->left;
return node;
}
Node* BinarySearchTree::Max(Node* node)
{
while (node->right != _nil)
node = node->right;
return node;
}
Node* BinarySearchTree::Next(Node* node)
{
if (node->right != _nil)
return Min(node->right);
Node* parent = node->parent;
while (parent != _nil && node == parent->right)
{
node = parent;
parent = parent->parent;
}
return parent;
}
void BinarySearchTree::Insert(int key)
{
Node* newNode = new Node();
newNode->key = key;
Node* node = _root;
Node* parent = _nil;
while (node != _nil)
{
parent = node;
if (key < node->key)
node = node->left;
else
node = node->right;
}
newNode->parent = parent;
if (parent == _nil)
_root = newNode;
else if (key < parent->key)
parent->left = newNode;
else
parent->right = newNode;
// 검사
newNode->left = _nil;
newNode->right = _nil;
newNode->color = Color::Red;
InsertFixup(newNode);
}
void BinarySearchTree::InsertFixup(Node* node)
{
// 1) p = red, uncle = red
// -> p = black, uncle = black, pp = red로 바꿈
// 2) p = red, uncle = black (triangle)
// -> 회전을 통해 case 3으로 바꿈
// 3) p = red, uncle = black (list)
// -> 색상 변경 + 회전
while (node->parent->color == Color::Red)
{
if (node->parent == node->parent->parent->left)
{
Node* uncle = node->parent->parent->right;
if (uncle->color == Color::Red)
{
node->parent->color = Color::Black; // p
uncle->color = Color::Black; // u
node->parent->parent->color = Color::Red; // pp
node = node->parent->parent;
}
else
{
// [pp(B)]
// [p(R)] [u(B)]
// [n(R)]
// [pp(B)]
// [p(R)] [u(B)]
// [n(R)]
if (node == node->parent->right) // Triangle 타입
{
node = node->parent;
LeftRotate(node);
}
// List 타입
// [pp(R)]
// [p(B)] [u(B)]
// [n(R)]
// [p(B)]
// [n(R)] [pp(R)]
// [u(B)]
node->parent->color = Color::Black;
node->parent->parent->color = Color::Red;
RightRotate(node->parent->parent);
}
}
else
{
Node* uncle = node->parent->parent->left;
if (uncle->color == Color::Red)
{
node->parent->color = Color::Black; // p
uncle->color = Color::Black; // u
node->parent->parent->color = Color::Red; // pp
node = node->parent->parent;
}
else
{
if (node == node->parent->left) // Triangle 타입
{
node = node->parent;
RightRotate(node);
}
// List 타입
// [p(B)]
// [pp(R)] [n(R)]
// [u(B)]
node->parent->color = Color::Black;
node->parent->parent->color = Color::Red;
LeftRotate(node->parent->parent);
}
}
}
_root->color = Color::Black;
}
void BinarySearchTree::Delete(int key)
{
Node* deleteNode = Search(_root, key);
Delete(deleteNode);
}
// 먼저 BST 삭제 실행
// [20]
// [15(DB)] [30]
// [25][40]
void BinarySearchTree::Delete(Node* node)
{
if (node == _nil)
return;
if (node->left == _nil)
{
Color color = node->color;
Node* right = node->right;
Replace(node, node->right);
if (color == Color::Black)
DeleteFixup(right);
}
else if (node->right == _nil)
{
Color color = node->color;
Node* right = node->left;
Replace(node, node->left);
if (color == Color::Black)
DeleteFixup(right);
}
else
{
// 다음 데이터 찾기
Node* next = Next(node);
node->key = next->key;
Delete(next);
}
}
// 먼저 BST 삭제 실행...
// - Case1) 삭제할 노드가 Red -> 그냥 삭제! 끝!
// - Case2) root가 DB -> 그냥 추가 Black 삭제! 끝!
// - Case3) DB의 sibling 노드가 Red
// -- s = black, p = red (s <-> p 색상 교환)
// -- DB 방향으로 rotate(p)
// -- goto other case
// - Case4) DB의 sibling 노드가 Black && sibling의 양쪽 자식도 Black
// -- 추가 Black을 parent에게 이전
// --- p가 Red이면 Black 됨.
// --- p가 Black이면 DB 됨.
// -- s = red
// -- p를 대상으로 알고리즘 이어서 실행 (DB가 여전히 존재하면)
// - Case5) DB의 sibling 노드가 Black && sibling의 near child = red, far child = black
// -- s <-> near 색상 교환
// -- far 방향으로 rotate(s)
// -- goto case 6
// - Case6) DB의 sibling 노드가 Black && sibling의 far child = red
// - p <-> s 색상 교환
// - far = black
// - rotate(p) (DB 방향으로)
// - 추가 Black 제거
void BinarySearchTree::DeleteFixup(Node* node)
{
Node* x = node;
// [Case1][Case2]
while (x != _root && x->color == Color::Black)
{
// [p(B)]
// [x(DB)] [s(R)]
// [p(R)]
// [x(DB)] [s(B)]
// [1]
// [s(B)]
// [p(R)]
// [x(DB)] [1]
if (x == x->parent->left)
{
// [Case3]
Node* s = x->parent->right;
if (s->color == Color::Red)
{
s->color = Color::Black;
x->parent->color = Color::Red;
LeftRotate(x->parent);
s = x->parent->right; // [1]
}
// [Case4]
if (s->left->color == Color::Black && s->right->color == Color::Black)
{
s->color = Color::Red;
x = x->parent;
}
else
{
// [p]
// [x(DB)] [s(B)]
// [near(R)][far(B)]
// [p]
// [x(DB)] [near(B)]
// [s(R)]
// [far(B)]
// [Case5]
if (s->right->color == Color::Black)
{
s->left->color = Color::Black;
s->color = Color::Red;
RightRotate(s);
s = x->parent->right;
}
// [p]
// [x(DB)] [s(B)]
// [far(R)]
// [Case6]
s->color = x->parent->color;
x->parent->color = Color::Black;
s->right->color = Color::Black;
LeftRotate(x->parent);
x = _root;
}
}
else
{
// [Case3]
Node* s = x->parent->left;
if (s->color == Color::Red)
{
s->color = Color::Black;
x->parent->color = Color::Red;
RightRotate(x->parent);
s = x->parent->left; // [1]
}
// [Case4]
if (s->right->color == Color::Black && s->left->color == Color::Black)
{
s->color = Color::Red;
x = x->parent;
}
else
{
// [Case5]
if (s->left->color == Color::Black)
{
s->right->color = Color::Black;
s->color = Color::Red;
LeftRotate(s);
s = x->parent->left;
}
// [Case6]
s->color = x->parent->color;
x->parent->color = Color::Black;
s->left->color = Color::Black;
RightRotate(x->parent);
x = _root;
}
}
}
x->color = Color::Black;
}
// u 서브트리를 v 서브트리로 교체
// 그리고 delete u
void BinarySearchTree::Replace(Node* u, Node* v)
{
if (u->parent == _nil)
_root = v;
else if (u == u->parent->left)
u->parent->left = v;
else
u->parent->right = v;
v->parent = u->parent;
delete u;
}
// [y]
// [x] [3]
// [1][2]
// [x]
// [1] [y]
// [2][3]
void BinarySearchTree::LeftRotate(Node* x)
{
Node* y = x->right;
x->right = y->left; // [2];
if (y->left != _nil)
y->left->parent = x;
y->parent = x->parent;
if (x->parent == _nil)
_root = y;
else if (x == x->parent->left)
x->parent->left = y;
else
x->parent->right = y;
y->left = x;
x->parent = y;
}
void BinarySearchTree::RightRotate(Node* y)
{
Node* x = y->left;
y->left = x->right; // [2];
if (x->right != _nil)
x->right->parent = y;
x->parent = y->parent;
if (y->parent == _nil)
_root = x;
else if (y == y->parent->left)
y->parent->left = x;
else
y->parent->right = x;
x->right = y;
y->parent = x;
}
'C++ > 알고리즘' 카테고리의 다른 글
[C++ algorithm] 최소 스패닝 트리(Minimum Spanning Tree) (2) | 2023.12.15 |
---|---|
[C++ algorithm] 각종 정렬에 대해 (0) | 2023.12.14 |
[C++ algorithm] 힙과 우선순위 큐, A* 길찾기 알고리즘을 통한 미로게임 (1) | 2023.12.10 |
[C++ algorithm] 그래프, BFS, DFS (0) | 2023.12.09 |
[C++ algorithm] 스택, 큐, 오른손 법칙 개선 (0) | 2023.12.08 |