#include <iostream>
void Test(int& _Data)
{
// 종료되는 시점을 꼭 정하고 return시켜야 한다
if (_Data >= 100)
{
return;
}
// 함수의 실행메모리 크기는 변수의 크기에 영향을 받는다
// 따라서, 같은 크기의 스택 메모리라고 가정했을 때,
// Arr가 없는 경우보다 Arr가 있을 때 Stack Overflow가 발생할 때까지의
// Test 실행 횟수가 적다
int Arr[100];
Arr[0] = _Data;
std::cout << _Data << std::endl;
++_Data;
Test(_Data); // 재귀함수
}
int main()
{
int Value = 0;
Test(Value);
}
💡 자료구조의 분류
기준 | 컨테이너 | 메모리형태(참고) ----- | ----- | ----- vector | 시퀀스 | 배열 list | 시퀀스 | 노드 map | 연관 | 노드(트리) string | 어댑터 | stack | 어댑터 | queue | 어댑터 |
/*
[Map]
노드(트리)를 사용하는 연관 컨테이너
- std::map은 레드블랙 바이너리 서치 트리이다.
- root로부터 나무모양처럼 뻗어나간다고 해서 노드 형식 중 트리 구조이다.
Node > Tree > (이진, 삼진, ...) + (서치, ...)
- 트리 구조 중, 자식의 갯수가 2개일 경우 이진 트리 구조이다. => binary tree
- 트리 구조 중, 자식 끼리 크기비교를 하게 되면 서치 트리 구조이다. => search tree
(+)
탐색과 정렬에 특화되어 있는 자료구조이다.
- 탐색 시, 최대 트리의 depth만큼의 연산만 필요하다.
- 데이터를 추가할 시, 자동으로 정렬된다.
(-)
탐색과 정렬에 특화되어 있다곤 하지만...
- 웬만큼 규모가 크지 않은 경우엔 속도에 특화되어 있는 벡터에게 탐색을 시키면
좀 더 빠르긴 하다(...)
- 그런데 심지어 배열보다 용량도 몇배다 크다.
- 특히 언리얼은 배열형 자료구조를 선호하는 편이다.
한쪽에만 치우쳐 있는 편향트리가 생길 수 있다.
- 레드블랙 알고리즘은 스핀이라는 알고리즘을 통해 적절한 노드에게 루트를 배정하여
트리의 균형을 잡아준다.
*/
#include <iostream>
#include <Windows.h>
#include <assert.h>
#include <map>
typedef int KeyType;
typedef int DataType;
class MyPair
{
public:
MyPair()
{
}
MyPair(KeyType _first, DataType _second)
: Key(_first), Value(_second)
{
}
KeyType Key = KeyType();
DataType Value = DataType();
};
// template<typename KeyType, typename ValueType>
class MyMap
{
private:
class MapNode
{
public:
MyPair Pair;
MapNode* Parent = nullptr;
MapNode* LeftChild = nullptr;
MapNode* RightChild = nullptr;
void insertNode(MapNode* _Node)
{
if ( _Node->Pair.Key < this->Pair.Key)
{
if (this->LeftChild == nullptr)
{
this->LeftChild = _Node;
return;
}
this->LeftChild->insertNode(_Node);
}
if (_Node->Pair.Key > this->Pair.Key)
{
if (this->RightChild == nullptr)
{
this->RightChild = _Node;
return;
}
this->RightChild->insertNode(_Node);
}
return;
}
bool containsNode(const KeyType& _Key)
{
if (_Key == this->Pair.Key)
{
return true;
}
if (_Key < this->Pair.Key)
{
if (this->LeftChild != nullptr)
{
/*
[꼬리 재귀]
리턴과 동시에 재귀함수를 호출하는 것
- 컴파일러가 (가능하다면) while문 형식으로 바꿔준다.
- inline함수와 비슷한 경우로, 최적화에 도움이 된다.
*/
return this->LeftChild->containsNode(_Key);
}
}
if (_Key > this->Pair.Key)
{
if (this->RightChild != nullptr)
{
return this->RightChild->containsNode(_Key);
}
}
return false;
}
MapNode* findNode(const KeyType& _Key)
{
if (_Key == this->Pair.Key)
{
return this;
}
if (_Key < this->Pair.Key)
{
if (this->LeftChild != nullptr)
{
return this->LeftChild->findNode(_Key);
}
}
if (_Key > this->Pair.Key)
{
if (this->RightChild != nullptr)
{
return this->RightChild->findNode(_Key);
}
}
return nullptr;
}
// Parent 쪽에서 나와의 연결 해제
void Release()
{
if (Parent != nullptr)
{
// 내가 LeftChild일 경우
if (this == Parent->LeftChild)
{
Parent->LeftChild = nullptr;
}
// 내가 RightChild일 경우
if (this == Parent->RightChild)
{
Parent->RightChild = nullptr;
}
}
}
// 전위 순회
void firstOrderPrint()
{
std::cout << Pair.Key << std::endl; // 할일
if (LeftChild != nullptr)
{
LeftChild->firstOrderPrint();
}
if (RightChild != nullptr)
{
RightChild->firstOrderPrint();
}
return;
}
// 중위 순회
void midOrderPrint()
{
if (LeftChild != nullptr)
{
LeftChild->midOrderPrint();
}
std::cout << Pair.Key << std::endl; // 할일
if (RightChild != nullptr)
{
RightChild->midOrderPrint();
}
return;
}
// 후위 순회
void lastOrderPrint()
{
if (LeftChild != nullptr)
{
LeftChild->lastOrderPrint();
}
if (RightChild != nullptr)
{
RightChild->lastOrderPrint();
}
std::cout << Pair.Key << std::endl; // 할일
return;
}
// 후위 순회와 유사한 구조이다
// this를 delete하기 전에 필요한 일을 모두 마쳐야하기 때문
void clearNode()
{
if (this->LeftChild != nullptr)
{
LeftChild->clearNode();
}
if (this->RightChild != nullptr)
{
RightChild->clearNode();
}
delete this;
return;
}
MapNode* minnode()
{
if (this->LeftChild == nullptr)
{
return this;
}
return LeftChild->minnode();
}
MapNode* maxnode()
{
if (this->RightChild == nullptr)
{
return this;
}
return RightChild->maxnode();
}
// 재귀함수로도 가능
MapNode* SmallParent()
{
MapNode* PNode = Parent;
while (PNode)
{
// 나의 Parent의 Key값이 나의 Key값보다 작다면,
if (Pair.Key > PNode->Pair.Key)
{
// Parent가 반환되어 PrevNode가 된다
return PNode;
}
// 그렇지 않다면, Parent의 Parent를 탐색
PNode = PNode->Parent;
}
return nullptr;
}
// 재귀함수로도 가능
MapNode* OverParent()
{
MapNode* PNode = Parent;
while (PNode)
{
// 나의 Parent의 Key값이 나의 Key값보다 크다면,
if (Pair.Key < PNode->Pair.Key)
{
// Parent가 반환되어 NextNode가 된다
return PNode;
}
// 그렇지 않다면, Parent의 Parent를 탐색
PNode = PNode->Parent;
}
return nullptr;
}
MapNode* PrevNode()
{
// LeftChild가 존재할 경우
if (this->LeftChild != nullptr)
{
return this->LeftChild->maxnode();
}
// LeftChild가 존재하지 않을 경우, 부모를 탐색
return SmallParent();
}
MapNode* NextNode()
{
// RightChild가 존재할 경우
if (this->RightChild != nullptr)
{
return this->RightChild->minnode();
}
// RightChild가 존재하지 않을 경우, 부모를 탐색
return OverParent();
}
bool IsLeaf()
{
return nullptr == LeftChild && nullptr == RightChild;
}
};
public:
class iterator
{
friend MyMap;
public:
iterator()
{
}
iterator(MapNode* _CurNode)
: CurNode(_CurNode)
{
}
MyPair* operator->()
{
MyPair& MapPair = CurNode->Pair;
return &MapPair; // -> 연산자는 주소값을 반환한다
}
bool operator!=(const iterator& _Other)
{
return CurNode != _Other.CurNode;
}
void operator++()
{
CurNode = CurNode->NextNode();
return;
}
private:
MapNode* CurNode = nullptr;
};
~MyMap()
{
clear();
}
/*
Tip
인자는 레퍼런스로 받는게 이득이다.
어짜피 인자는 8byte씩 할당되기 때문에 bool이라도 레퍼런스가 이득이다.
*/
/*
map은 자료가 무작위일때, 다른 자료구조에 비해 높은 효율을 자랑한다.
자료가 이미 특정 기준 하에 정렬된 상태로 들어간다면 대부분의 경우,
다른 자료구조들이 map보다 빠르다.
*/
void insert(const MyPair& _Value)
{
MapNode* NewNode = new MapNode();
NewNode->Pair = _Value;
// 트리에서 최초 노드는 무조건 뿌리 노드가 된다
if (Root == nullptr)
{
Root = NewNode;
return;
}
Root->insertNode(/*Root, */NewNode);
}
bool contains(const KeyType& _Key)
{
if (Root == nullptr)
{
return false;
}
return Root->containsNode(_Key);
}
iterator find(const KeyType& _Key)
{
if (Root == nullptr)
{
return end();
}
return iterator(Root->findNode(_Key));
}
iterator erase(iterator& _Iter)
{
iterator Return;
if (_Iter.CurNode == nullptr)
{
MessageBoxA(nullptr, "유효하지 않은 원소를 삭제하려고 했습니다.", "치명적 에러", MB_OK);
assert(false);
return Return;
}
// erase()하는 노드의 NextNode를 반환
Return.CurNode = _Iter.CurNode->NextNode();
/*
상황 1. 자식이 없는 리프노드일 경우
- 대체할 노드가 필요하지 않다
- Parent 노드에게 알려야 한다
*/
if (_Iter.CurNode->IsLeaf() == true)
{
// 삭제할 노드의 부모와 연결 끊기
_Iter.CurNode->Release();
delete _Iter.CurNode;
return Return;
}
/*
상황 2. 자식이 있는 노드일 경우
- LeftChild의 maxnode 또는 RightChild의 minnode가 대체한다
- 둘 중 어느것이든 괜찮기 때문에 코드 짜는 사람이 선택 가능하다
*/
MapNode* ChangeNode = nullptr; // 대체할 노드
MapNode* CurNode = _Iter.CurNode; // 삭제할 노드
// RightChild의 minnode를 대체할 노드로 지정
ChangeNode = CurNode->RightChild->minnode();
if (ChangeNode == nullptr)
{
// 안될 경우, LeftChild의 maxnode를 대체할 노드로 지정
ChangeNode = CurNode->LeftChild->maxnode();
if (ChangeNode == nullptr)
{
MessageBoxA(nullptr, "체인지 노드 에러.", "치명적 에러", MB_OK);
assert(false);
return Return;
}
}
// 대체할 노드의 부모와 연결 끊기
ChangeNode->Release();
// 삭제할 노드의 자식과 연결 끊기, 대체할 노드를 새로운 자식과 연결
MapNode* LeftChild = CurNode->LeftChild;
MapNode* RightChild = CurNode->RightChild;
/*
if (LeftChild != nullptr)
{
LeftChild->Parent = nullptr;
}
if (RightChild != nullptr)
{
RightChild->Parent = nullptr;
}
*/
if (LeftChild != nullptr)
{
LeftChild->Parent = ChangeNode;
if (LeftChild != ChangeNode)
{
ChangeNode->LeftChild = LeftChild;
}
}
if (RightChild != nullptr)
{
RightChild->Parent = ChangeNode;
if (RightChild != ChangeNode)
{
ChangeNode->RightChild = RightChild;
}
}
// 삭제할 노드의 부모와 연결 끊기, 대체할 노드를 새로운 부모와 연결
ChangeNode->Parent = CurNode->Parent;
MapNode* Parent = ChangeNode->Parent;
if (Parent != nullptr && Parent->LeftChild == CurNode)
{
Parent->LeftChild = ChangeNode;
}
if (Parent != nullptr && Parent->RightChild == CurNode)
{
Parent->RightChild = ChangeNode;
}
// 삭제할 노드가 root였을 경우
if (Root == CurNode)
{
Root = ChangeNode;
}
delete CurNode;
return Return;
}
iterator begin()
{
if (Root == nullptr)
{
return end();
}
return iterator(Root->minnode());
}
iterator end()
{
return iterator(nullptr);
}
void firstOrderPrint()
{
Root->firstOrderPrint();
return;
}
void midOrderPrint()
{
Root->midOrderPrint();
return;
}
void lastOrderPrint()
{
Root->lastOrderPrint();
return;
}
void clear()
{
Root->clearNode();
Root = nullptr;
return;
}
private:
MapNode* Root = nullptr;
};
int main()
{
_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);
{
std::cout << "std::map" << std::endl;
// [선언] key, value
std::map<int, int> NewMap = std::map<int, int>();
/*
[자료 추가] 방법이 네가지나 있다
1. pair
NewMap.insert(std::pair<int, int>(1, 123456789));
2. make_pair
NewMap.insert(std::make_pair(1, 123456789));
3. 배열 연산자
NewMap[3] = 1;
4. value_type
NewMap.insert(std::map<int, int>::value_type(7, 10));
// std::map<int, int>::value_type == std::pair<int, int>
*/
NewMap.insert(std::pair<int, int>(10, 0));
NewMap.insert(std::pair<int, int>(5, 0));
NewMap.insert(std::pair<int, int>(15, 0));
NewMap.insert(std::pair<int, int>(12, 0));
NewMap.insert(std::pair<int, int>(3, 0));
NewMap.insert(std::pair<int, int>(7, 0));
// 자동으로 오름차순 정렬이 된다
// 3, 5, 7, 10, 12, 15
/*
[탐색]
1. contains() (C++20 에서만 사용 가능)
존재하는지 여부를 bool로 반환
NewMap.contains(15);
if (NewMap.contains(15) == true)
{
// 존재한다
}
2. find()
탐색한 결과를 iterator로 반환
*/
std::map<int, int>::iterator FindIter = NewMap.find(15);
if (FindIter != NewMap.end())
{
// 존재한다
}
// [자료 삭제]
NewMap.erase(FindIter);
// [순회]
// map은 노드의 갯수가 많아질수록 순회를 돌리는 과정이 효율적이지 못하다
// 무수한 반복문 또는 재귀함수를 호출해야 하기 때문
std::map<int, int>::iterator StartIter = NewMap.begin();
std::map<int, int>::iterator EndIter = NewMap.end();
for (; StartIter != EndIter; ++StartIter)
{
std::cout << "Key : " << StartIter->first << std::endl;
// std::cout << "Value : " << StartIter->second << std::endl;
}
}
{
std::cout << "MyMap" << std::endl;
// 선언
MyMap NewMap = MyMap();
// 자료 추가
NewMap.insert(MyPair(10, 0));
NewMap.insert(MyPair(5, 0));
NewMap.insert(MyPair(15, 0));
NewMap.insert(MyPair(12, 0));
NewMap.insert(MyPair(3, 0));
NewMap.insert(MyPair(7, 0));
// 탐색
NewMap.contains(12);
MyMap::iterator FindIter = NewMap.find(12);
// 삭제
// NewMap.erase(FindIter);
// 순회
MyMap::iterator StartIter = NewMap.begin();
MyMap::iterator EndIter = NewMap.end();
for (; StartIter != EndIter; ++StartIter)
{
std::cout << StartIter->Key << std::endl;
//std::cout << StartIter->second << std::endl;
}
std::cout << "first" << std::endl;
NewMap.firstOrderPrint(); // 10 5 3 7 15 12
std::cout << "mid" << std::endl;
NewMap.midOrderPrint(); // 3 5 7 10 12 15
std::cout << "last" << std::endl;
NewMap.lastOrderPrint(); // 3 7 5 12 15 10
}
}