*본 내용은 [윤성우의 열혈 자료구조] 책과 강의를 보고 공부하면서 요점 정리한 내용입니다.
트리
는 계층적 관계(Hierarchical Relationship)(비선형)를 표현하는 자료구조이다.노드(node)
트리의 구성요소에 해당하는 A, B, C, D, E, F와 같은 요소
간선(edge)
노드와 노드를 연결하는 연결선
루트 노드(root node)
트리 구조에서 최상위에 존재하는 A와 같은 노드
단말 노드(terminal node)
아래로 또 다른 노드가 연결되어 있지 않은 E, F, C, D와 같은 노드
내부 노드(internal node)
단말 노드를 제외한 모든 노드로 A, B와 같은 노드
부모 노드(parent node)
이다.자식 노드(child node)
이다.형제노드(sibling node)
이다.서브 트리
라 한다.재귀적
이다![이진 트리의 조건]
[재귀적인 성향을 담아내지 못한 완전하지 않은 표현]
→ 트리 그리고 이진 트리는 그 구조가 재귀적이다. 따라서 트리와 관련된 연산은 재귀적으로 사고하고 재귀적으로 구현할 필요가 있다.
높이
와 레벨
의 최대 값은 같다!포화 이진 트리
는 모든 레벨에 노드가 꽉 찬 트리를 의미한다.완전 이진 트리
는 위에서 아래로, 왼쪽에서 오른쪽으로 채워진 트리를 의미한다.
1. 배열 기반
2. 연결리스트 기반
// 이진 트리의 노드를 표현한 구조체
typedef struct _bTreeNode
{
BTData data;
struct _bTreeNode * left;
struct _bTreeNode * right;
} BTreeNode;
∙ 이것이 노드이자 이진 트리를 표현한 구조체의 정의이다.
∙ 양방향 연결 리스트와 구조가 동일하다. 양방향 연결 리스트는 '노드 구조체'와 '양방향 연결 리스트 구조체' 모두 구현하지만 이진트리는 '노드'만 구현한다.
∙ 이진 트리의 모든 노드는 직/간접적으로 연결되어 있다. 따라서 루트 노드의 주소 값만 기억하면, 이진 트리 전체를 가리키는 것과 다름이 없다.
∙ 논리적으로도 하나의 노드는 그 자체로 이진트리이다. 따라서 노드를 표현한 구조체는 실제로 이진 트리를 표현한 구조체가 된다.
/*** BTreeNode 관련 연산들 ****/
BTreeNode * MakeBTreeNode(void); // 노드의 생성
BTData GetData(BTreeNode * bt); // 노드에 저장된 데이터를 반환
void SetData(BTreeNode * bt, BTData data); // 노드에 데이터를 저장 (노드에 직접 접근하는 것보다, 함수를 통한 접근이 보다 칭찬받을 수 있는 구조!)
BTreeNode * GetLeftSubTree(BTreeNode * bt); // 왼쪽 서브 트리의 주소 값 반환
BTreeNode * GetRightSubTree(BTreeNode * bt); // 오른쪽 서브 트리의 주소 값 반환
/* - 루트 노드를 포함하여 어떠한 노드의 주소 값도 인자로 전달될 수 있다.
- 전달된 노드의 왼쪽, 오른쪽 '서브 트리의 루트 노드 주소 값' 또는 그냥 '노드의 주소 값'이 반환된다. */
void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub); // main의 왼쪽 서브 트리로 sub를 연결
void MakeRightSubTree(BTreeNode * main, BTreeNode * sub); // main의 오른쪽 서브 트리로 sub를 연결
이러한 형태의 노드를 동적으로 할당하여 생성한다. 유효한 데이터는 SetData 함수를 통해서 채우되 포인터 변수 left와 right는 NLUL로 자동 초기화 된다.
BTreeNode * MakeBTreeNode(void)
{
BTreeNode * nd = (BTreeNode*)malloc(sizeof(BTreeNode));
nd->left = NULL;
nd->right = NULL;
return nd;
}
BTData GetData(BTreeNode * bt)
{
return bt->data;
}
void SetData(BTreeNode * bt, BTData data)
{
bt->data = data;
}
BTreeNode * GetLeftSubTree(BTreeNode * bt)
{
return bt->left;
}
BTreeNode * GetRightSubTree(BTreeNode * bt)
{
return bt->right;
}
void MakeLeftSubTree(BTreeNode * main, BTreeNode * sub)
{
if(main->left != NULL)
free(main->left); // 기존에 연결된 노드는 삭제되게 구현
main->left = sub;
}
void MakeRightSubTree(BTreeNode * main, BTreeNode * sub)
{
if(main->right != NULL)
free(main->right); // 기존에 연결된 노드는 삭제되게 구현
main->right = sub;
}
int main(void)
{
BTreeNode * bt1 = MakeBTreeNode(); // 노드 bt1 생성
BTreeNode * bt2 = MakeBTreeNode(); // 노드 bt2 생성
BTreeNode * bt3 = MakeBTreeNode(); // 노드 bt3 생성
BTreeNode * bt4 = MakeBTreeNode(); // 노드 bt4 생성
SetData(bt1, 1); // bt1에 1 저장
SetData(bt2, 2); // bt2에 2 저장
SetData(bt3, 3); // bt3에 3 저장
SetData(bt4, 4); // bt4에 4 저장
MakeLeftSubTree(bt1, bt2); // bt2를 bt1의 왼쪽 자식 노드로
MakeRightSubTree(bt1, bt3); // bt3를 bt1의 오른쪽 자식 노드로
MakeLeftSubTree(bt2, bt4); // bt4를 bt2의 왼쪽 자식 노드로
// bt1의 왼쪽 자식 노드의 데이터 출력
printf("%d \n", GetData(GetLeftSubTree(bt1))); // 2
// bt1의 왼쪽 자식 노드의 왼쪽 자식 노드의 데이터 출력
printf("%d \n", GetData(GetLeftSubTree(GetLeftSubTree(bt1)))); // 4
return 0;
}
실행 결과
2
4
트리를 완전히 소멸시키는 방법은? 순회!
중위 순회
후위 순회
전위 순회
↓
void InorderTraverse(BTreeNode * bt) // 중위 순회
{
// 탈출 조건
if(bt == NULL) // bt가 NULL이면 재귀 탈출!
return;
InorderTraverse(bt->left); // 1단계
printf("%d \n", bt->data); // 2단계
InorderTraverse(bt->right); // 3단계
}
int main(void)
{
BTreeNode * bt1 = MakeBTreeNode();
BTreeNode * bt2 = MakeBTreeNode();
BTreeNode * bt3 = MakeBTreeNode();
BTreeNode * bt4 = MakeBTreeNode();
SetData(bt1, 1);
SetData(bt2, 2);
SetData(bt3, 3);
SetData(bt4, 4);
MakeLeftSubTree(bt1, bt2);
MakeRightSubTree(bt1, bt3);
MakeLeftSubTree(bt2, bt4);
InorderTraverse(bt1);
return 0;
}
// 중위 순회
void InorderTraverse(BTreeNode * bt)
{
if(bt == NULL)
return;
InorderTraverse(bt->left);
printf("%d \n", bt->data);
InorderTraverse(bt->right);
}
// 전위 순회
void PreorderTraverse(BTreeNode * bt)
{
if(bt == NULL)
return;
printf("%d \n", bt->data);
PreorderTraverse(bt->left);
PreorderTraverse(bt->right);
}
// 후위 순회
void PostorderTraverse(BTreeNode * bt)
{
if(bt == NULL)
return;
PostorderTraverse(bt->left);
PostorderTraverse(bt->right);
printf("%d \n", bt->data);
}
typedef void VisitFuncPtr(BTData data);
// 또는
void (*VisitFuncPtr)(BTData data);
void InorderTraverse(BTreeNode * bt, VisitFuncPtr action)
{
if(bt == NULL)
return;
InorderTraverse(bt->left,action);
action(bt->data); // 노드의 방문
InorderTraverse(bt->right, action);
}
void ShowIntData(int data)
{
printf("%d", data);
}
수식 트리
는 이진트리의 일종으로, 메모리에 수식을 표기하는 표기법의 일종이다.#include "BinaryTree2.h"
BTreeNode * MakeExpTree(char exp[]); // 수식 트리 구성
// 후위 표기법의 수식을 인자로 받아서 수식 트리를 구성하고 루트 노드의 주소값을 반환한다.
int EvaluateExpTree(BTreeNode * bt); // 수식 트리 계산
//MakeExpTree가 구성한 수식 트리의 수식을 계산하여 그 결과를 반환한다.
void ShowPrefixTypeExp(BTreeNode * bt); // 전위 표기법 기반 출력
void ShowInfixTypeExp(BTreeNode * bt); // 중위 표기법 기반 출력
void ShowPostfixTypeExp(BTreeNode * bt);// 후위 표기법 기반 출력
// 전위, 중위, 후위 순회하여 출력 시 각각 전위, 중위, 후위 표기법의 수식이 출력된다.
BTreeNode * MakeExpTree(char exp[])
{
Stack stack;
BTreeNode * pnode;
int expLen = strlen(exp);
int i;
StackInit(&stack);
for(i=0; i<expLen; i++)
{
pnode = MakeBTreeNode();
if(isdigit(exp[i])) // 피연산자라면...
{
SetData(pnode, exp[i]-'0');
}
else // 연산자라면...
{
MakeRightSubTree(pnode, SPop(&stack));
MakeLeftSubTree(pnode, SPop(&stack));
SetData(pnode, exp[i]);
}
SPush(&stack, pnode);
}
return SPop(&stack); // 완성된 수식 트리는 마지막엔 스택에 존재한다.
}
/*** VisitFuncPtr형 함수 ***/
void ShowNodeData(int data)
{
/*** 노드의 방문 목적 ***/
if(0<=data && data<=9)
printf("%d ", data); // 피연산자 출력
else
printf("%c ", data); // 연산자 출력
}
void ShowPrefixTypeExp(BTreeNode * bt)
{
PreorderTraverse(bt, ShowNodeData); // 전위 표기법 수식 출력
}
void ShowInfixTypeExp(BTreeNode * bt)
{
InorderTraverse(bt, ShowNodeData); // 중위 표기법 수식 출력
}
void ShowPostfixTypeExp(BTreeNode * bt)
{
PostorderTraverse(bt, ShowNodeData); // 후위 표기법 수식 출력
}
※ ListBaseStack.h의 type 선언 변경 필요하다.
→ typedef BTreeNode * BTData;
int main(void)
{
char exp[] = "12+7*";
BTreeNode * eTree = MakeExpTree(exp);
printf("전위 표기법의 수식: ");
ShowPrefixTypeExp(eTree); printf("\n");
printf("중위 표기법의 수식: ");
ShowInfixTypeExp(eTree); printf("\n");
printf("후위 표기법의 수식: ");
ShowPostfixTypeExp(eTree); printf("\n");
printf("연산의 결과: %d \n", EvaluateExpTree(eTree));
return 0;
}
1) 기본 구성
int EvaluateExpTree(BTreeNode * bt)
{
int op1, op2;
op1 = GetData(GetLeftSubTree(bt)); // 첫 번째 피연산자
op2 = GetData(GetRightSubTree(bt)); // 두 번째 피연산자
switch(GetData(bt)) // 연산자를 확인하여 연산을 진행
{
case '+':
return op1+op2;
case '-':
return op1-op2;
case '*':
return op1*op2;
case '/':
return op1/op2;
}
return 0;
}
2) 재귀적 구성
단말노드가 아닌 서브트리가 올 경우를 대비한다. 하지만 탈출조건이 없다.
int EvaluateExpTree(BTreeNode * bt)
{
int op1, op2;
op1 = EvaluateExpTree(GetLeftSubTree(bt)); // 첫 번째 피연산자 - 재귀
op2 = EvaluateExpTree(GetRightSubTree(bt)); // 두 번째 피연산자 - 재귀
switch(GetData(bt)) // 연산자를 확인하여 연산을 진행
{
case '+':
return op1+op2;
case '-':
return op1-op2;
case '*':
return op1*op2;
case '/':
return op1/op2;
}
return 0;
}
3) 탈출조건 추가
int EvaluateExpTree(BTreeNode * bt)
{
int op1, op2;
// 탈출 조건
if(GetLeftSubTree(bt)==NULL && GetRightSubTree(bt)==NULL) // 단말 노드라면
return GetData(bt);
op1 = EvaluateExpTree(GetLeftSubTree(bt));
op2 = EvaluateExpTree(GetRightSubTree(bt));
switch(GetData(bt))
{
case '+':
return op1+op2;
case '-':
return op1-op2;
case '*':
return op1*op2;
case '/':
return op1/op2;
}
return 0;
}