나도 자료구조가 좀 재밌으면 좋겠다. 이런 거 별로 재미없어 하는 거 보면 겜콘이 그립기도 하다. 나는 왜 자료구조나 알고리즘이 재미없을까. 기계학습은 그래도 재밌어 했는데. 나도 알다가도 모르겠다.
< Binary Tree의 특성 >
1. 각 노드는 최대 2개의 자식 노드를 가질 수 있다.
< 트리와 관련된 용어 >
< Binary Tree의 종류 >
< Searching a binary tree >
1. start at the root
2. Search the tree level by level, until you find the element you are searching for
linked list처럼 생긴 경우 O(N)이 걸리므로 N개를 전부 살펴보는 것은 linked list와 마찬가지이다.
< BST에서 사용하는 Binary Tree의 특징 >
1. Each node contains a distinct data value : 중복 값 없다.
2. The key values in the tree can be compared using "greater than" and "less than" : 대소 비교가 가능하다. 크거나 작거나.
3. The key value of each node in the tree is less than every key value in its right subtree, and greater than every key value in its left subtree. : 모든 노드들이 오른쪽에는 자신보다 큰 값, 왼쪽에는 자신보다 작은 값들을 가진다.
< Binary Tree Search >
: 절반씩 버리면서 진행하므로 시간 복잡도는 O(logN)이 된다. linked list(: O(logN) )보다 낫다.
< BST Implementation >
#include <fstream.h>
template<class ItemType>
struct TreeNode;
enum OrderType {PRE_ORDER, IN_ORDER, POST_ORDER};
template<class ItemType>
class TreeType {
public:
TreeType(); // constructor
~TreeType(); // deconstructor
TreeType(const TreeType<ItemType>&); // copy constructor
void operator=(const TreeType<ItemType>&) // operator overloading
void MakeEmpty();
bool IsEmpty() const;
bool IsFull() const;
int LengthIs() const; // node 개수
void RetrieveItem(ItemType&, bool& found); // copy 방지로 reference type으로 받는다.
void InsertItem(ItemType);
void DeleteItem(ItemType);
void ResetTree(ItemType);
void GetNextItem(ItemType&, OrderType, bool&); // enum에 정의. tree를 리스트 형태로 바꿔서 넣음
// Next란게 존재하려면 순서대로 정렬되어있어야 하므로 list 형태로 바꿔야 한다.
void PrintTree(ofstream&) const;
private:
TreeNode<ItemType>* root;
};
bool TreeType::IsFull() const
{
NodeType* location;
try
{
location=new NodeType;
delete location;
return false;
}
catch(std::bad_alloc exception)
{
return true;
}
}
bool TreeType::IsEmpty() const
{
return root==NULL;
}
먼저 잘못 구현한 경우들을 살펴보자.
// 잘못 구현한 경우 1
if ((Left(tree) == NULL) && (Right(tree) == NULL)
return 1;
else
return CountNods(Left(tree))+CountNodes((Right(tree))+1;
이와 같이 구현하면 Left(tree)가 NULL일 때 문제가 발생한다. 즉 Left subtree는 NULL인데 Right Subtree는 NULL이 아닌 경우 base case가 없기 때문이다.
// 잘못 구현한 경우 2
if ((Left(tree) == NULL) && (Right(tree) == NULL) // 둘 다 NULL일 때
return 1
else if (Left(tree) == NULL) // right은 NULL 아니고 left가 NULL일 때
return CountNodes(Right(tree))+1;
else if (Right(tree) == NULL) // right는 NULL이고 left가 NULL 아닐 때
return CountNOdes(Left(tree))+1;
else // general case
return CountNods(Left(tree))+CountNodes((Right(tree))+1;
이와 같이 구현하면 initial tree가 NULL일 때 문제가 발생한다. 비어있는데 노드 개수를 1이라고 반환하는 것이다.
if (tree == NULL )
return 0;
else if ( (Left(tree) == NULL ) && (Right(tree) == NULL)
return 1;
else if (Left(tree) == NULL )
return CountNodes(Right(tree))+1;
else if ( Right(tree) == NULL )
return CountNodes(Left(tree))+1;
else
return CountNods(Left(tree))+CountNodes((Right(tree))+1;
이를 간단하게 하면 다음과 같다. 재귀 호출 횟수는 1번 더 늘어나지만 코드가 simplify 된다.
if (tree == NULL )
return 0;
else
return CountNods(Left(tree))+CountNodes((Right(tree))+1;
이제 정말로 CountNodes 함수와 이를 통해 LengthIs 함수까지 해보자.
int CountNodes(TreeNode* tree);
int TreeType::LengthIs() const
{
return CountNodes(root);
}
int CountNodes(TreeNode* tree)
{
if (tree == NULL)
return 0;
else
return CountNodes(tree->left) + CountNodes(tree->right)+1;
}
void TreeType::RetreiveItem(ItemType& item, bool& found)
{
Retrieve(root, item, found);
}
void Retrieve(TreeNode* tree, ItemType& item, bool& found)
{
if(tree==NULL) // base case 1 : 없을 때
found==false;
else if (item<tree->info) // 찾고자 하는 값이 작으므로 왼쪽 subtree 살핀다.
Retrieve(tree->left, item, found);
else if (item>tree->info) // 찾고자 하는 값이 크므로 오른쪽 subtree 살핀다.
Retrieve(tree->right, item, found);
else // base case 2 : 찾았을 때
{
item=tree->info;
found=true;
}
}
void Insert(TreeNOde*& tree, ItemType item) // ** reference type!! **
{
if(tree==NULL)
{
tree=new TreeNode; // **호출한 애의 tree->left/right가 바뀌게 된다. **
tree->right=NULL;
tree->left=NULL;
tree->info=item;
}
else if(item<tree->info)
Insert(tree->left, item);
else
Insert(tree->right, item);
}
트리에 값을 Inserting 할 때 순서가 중요할까?
: 그렇다. A random mix of the elements prodives a shorter, bushy tree. Search가 간편해진다. Insert Order에 따라 unbalanced한 tree가 만들어질 수 있는데, 이러면 Search 시간이 증가한다.
DeleteItem Implementation
지울 아이템을 찾고 지우는 과정을 수행한다. 그러나 이 경우 3가지로 case를 나눠야 한다.
/*
if ((Left(tree) == NULL ) && ( Right(tree) == NULL )
Set tree to NULL
else if ( Left(tree) == NULL)
Set tree to Right(tree)
else if ( Right(tree) == NULL )
Set tree to Left(tree)
else
Find predecessor
Set Info(free) to Info(predecessor)
Delete predecessor
*/
void DeleteNode(TreeNode*& tree) // reference type!!!!!!!!!!!!!!!!!!
{
ItemType data;
TreeNode* tempPtr;
tempPtr=tree;
if(tree->left==NULL)
{
tree=tree->right; // 0이면 tree->right이 NULL이기 때문
delete tempPtr; // 0 or 1 children
}
else if (tree->right==NULL)
{
tree=tree->left;
delete tempPtr; // 0 or 1 children
}
else
{
GetPredecessor(tree->left, data);
tree->info=data;
Delete(tree->left, data); // // 2 children 재귀로 구현. 덮어씌운 애의 원래 위치 삭제.
}
}
/*
Definition : Removes item from tree
Size : The number of nodes in the path from the root to the node to be deleted
Base case : If item's key matches key in Info(tree), delete node pointed to by tree
General case : If item < Info(tree), Delete(Left(tree), item);
else Delete(Right(tree), item);
*/
void Delete(TreeNode*& tree, ItemType item)
{
if(item<tree->info)
Delete(tree->left, item);
else if(item>tree->info)
Delete(tree->right, item);
else
DeleteNode(tree); // Node found
}
void GetPredecessor(TreeNode* tree, ItemType& data) // Recursive하게도 구현 가능
{
while(tree->right!=NULL)
tree=tree->right;
data=tree->info;
}
Q. 필기되어있는 것처럼 Delete node with 2 children에서 왼쪽 subtree의 가장 rightmost 노드가 left child를 가지고 있으면 어떡하지?