호우동의 개발일지

Today :

article thumbnail

1. 이진트리

  • 정의 : 공집합 또는 루트와 왼쪽/오른쪽 서브트리로 구성된 유한집합(서브트리는 이진트리)
  • 모든 노드가 2개의 서브트리를 가지고 있는 트리
  • 서브트리는 공집할 수 있음

 


1.1. 특징

  • 각 노드에는 최대 2개까지의 자식 노드가 존재
    • 각 노드의 차수 ≤ 2 → 구현하기가 편리
  • 서브트리 간 순서 존재 ( L → R )

 


1.2. 성질

  • 노드의 개수가 n개이면 간선의 개수는 n-1
  • 높이가 h인 이진트리인 경우
    • 최소 h개의 노드
    • 최대 2^h-1개의 노드

  • n개의 노드를 가지는 이진트리의 높이
    • 최대 n
    • 최소 log_2(n+1)(올림)

 


1.3. 이진트리의 분류

  • 포화 이진트리

포화이진트리
포화이진트리

  • 트리의 각 레벨에 노드가 꽉 차있는 이진트리

  • 완전 이진트리

완전 이진트리 종류

  • 높이가 h일 때, 레벨 1부터 h-1까지는 노드가 모두 채워져 있음
  • 마지막 레벨 h는 왼쪽부터 오른쪽으로 노드가 순서대로 채워짐

 


1.4. 이진트리 표현

  • 배열 표현법

배열 표현법과 테이블
배열 표현법과 테이블

  • 모든 이진트리를 포화 이진트리라고 가정
  • 각 노드에 번호를 붙여서 그 번호를 배열의 인덱스로 삼아 노드의 데이터를 배열에 저장
  • 부모와 자식의 인덱스 관계
    • 배열의 인덱스가 1로 시작할 때
      • 노드 i의 부모 노드 인덱스 = |i/2|
      • 노드 i의 왼쪽 자식 노드 인덱스 = 2*i
      • 노드 i의 오른쪽 자식 노드 인덱스 = 2*i+1

    • 배열의 인덱스가 0으로 시작할 때
      • 노드 i의 부모 노드 인덱스 = |i-1/2|
      • 노드 i의 왼쪽 자식 노드 인덱스 = 2*i+1
      • 노드 i의 오른쪽 자식 노드 인덱스 = 2*i+2

  • 링크 표현법
    링크 표현법
    • 포인터를 이용하여 부모노드가 자식노드를 가리키게 하는 방법
    • 왼쪽 완전 이진트리를 링크 표현법으로 나타내면 오른쪽 처럼 된다.
    • 노드의 구조
      • 왼쪽 자식노드 + data + 오른쪽 자식 노드
      • 트리 노드의 구조체 자료형
<code />
typedef struct TreeNode{ int data; struct TreeNode *left; struct TreeNode *right; }TreeNode;

 


2. 이진트리의 순회

  • 순회 : 트리의 노드들을 체계적으로 방문
  • 기본적으로 3가지 순회방법이 존재
    • 전위 순회 : 자손노드보다 루트노드를 먼저 방문(V → L → R)
    • 중위 순회 : 왼쪽 자손, 루트, 오른쪽 자손 순으로 방문( L → V → R)
    • 후위 순회 : 루트노드보다 자손을 먼저 방문한다.( L → R → V)
    • 왼쪽 서브트리 L, 오른쪽 서브트리 R, 루트 V

 


2.1. 전위순회

  • 루트를 먼저 방문하는 순회방법

전위순회

<code />
preorder(TreeNode* root){ if(root){ printf("%d", root -> data); // 루트 노드 방문 출력 preorder(root -> left); // 왼쪽 노드 방문 preorder(root -> right); // 오른쪽 노드 방문 } }

 


2.2. 중위순회

  • 왼쪽서브트리 → 루트 → 오른쪽 서브 트리 순으로 방문

중위순회

<code />
inorder(TreeNode* root){ if(root){ preorder(root -> left); // 왼쪽 노드 방문 printf("%d", root -> data); // 루트 노드 방문 출력 preorder(root -> right); // 오른쪽 노드 방문 } }

 


2.3. 후위순회

  • 왼쪽서브트리 → 오른쪽 서브트리 → 루트 순으로 방문

후위순회

<code />
postorder(TreeNode* root){ if(root){ preorder(root -> left); // 왼쪽 노드 방문 preorder(root -> right); // 오른쪽 노드 방문 printf("%d", root -> data); // 루트 노드 방문 출력 } }

 


2.4. 반복적 순회

  • 앞선 재귀적인 순회 방식 말고 스택을 사용한 반복적 방법으로 순회 구현
<code />
#include <stdio.h> #include <stdlib.h> #include <memory.h> typedef struct TreeNode{ int data; struct TreeNode *left,*right; }TreeNode; // 스택 기능 함수 #define SIZE 100 int top = -1; TreeNode *stack[SIZE]; void push(TreeNode* p){ if(top < SIZE-1) stack[++top] = p; } TreeNode* pop(){ TreeNode *p = NULL; if(top>= 0) p = stack[top--]; return p; } // 전위 순회 void preorder_iter(TreeNode* root){ while(1){ for(;root;root = root -> left) { push(root); printf("%d -> ",root->data); } root = pop(); if(!root) break; root = root -> right; } } // 중위 순회 void inorder_iter(TreeNode* root){ while(1){ for(;root;root = root -> left) push(root); root = pop(); if(!root) break; printf("%d -> ",root->data); root = root -> right; } } //후위 순회 void postorder_iter(TreeNode* root){ while(1){ do{ //이미 방문한 노드의 데이터 값을 NULL로 만듦 //스택에 맨 위의 노드를 꺼내서 연결된 왼쪽 노드를 없을때까지 스택에 넣음 if(root -> data != NULL && root != NULL){ push(root); root = root -> left; } else break; }while(root != NULL); root = pop(); if(!root) break; // 연결된 오른쪽 노드가 있을 경우 if(root -> right != NULL){ // 연결된 오른쪽 노드를 이미 방문했을 경우 if(root -> right -> data == NULL){ printf("%d -> ",root -> data); root -> data = NULL; }else{ push(root); root = root -> right; } } else{ //연결된 노드가 왼쪽밖에 없는 경우 printf("%d -> ", root -> data); root -> data = NULL; } } } TreeNode n1 = {1,NULL,NULL}; TreeNode n2 = {4,&n1,NULL}; TreeNode n3 = {16,NULL,NULL}; TreeNode n4 = {25,NULL,NULL}; TreeNode n5 = {20,&n3,&n4}; TreeNode n6 = {15,&n2,&n5}; TreeNode *root = &n6; int main(void){ printf("전위 순회 = "); preorder_iter(root); printf("\n"); printf("중위 순회 = "); inorder_iter(root); printf("\n"); printf("후위 순회 = "); postorder_iter(root); printf("\n"); return 0; }

 

<출력>

<code />
전위 순회 = 15 -> 4 -> 1 -> 20 -> 16 -> 25 -> 중위 순회 = 1 -> 4 -> 15 -> 16 -> 20 -> 25 -> 후위 순회 = 1 -> 4 -> 16 -> 25 -> 20 -> 15 ->

 


2.5. 레벨 순회

  • 노드를 레벨 순으로 검사하는 순회 방법 → 큐를 사용해 구현
    • 아래와 같은 순서로 진행됨

레벨 순회 순서
레벨 순회 순서

<code />
/* ----- 큐 구현 함수 ---- */ void level_order(TreeNode *ptr){ QueueType q; init_queue(&q); if(ptr == NULL) return; enqueue(&q,ptr); while(!is_empty(&q)){ ptr = dequeue(&q); printf(" [%d] ", ptr -> data); if(ptr -> left) enqueue(&q,ptr -> left); if(ptr -> right) enqueue(&q,ptr -> right); } } TreeNode n1 = {1,NULL,NULL}; TreeNode n2 = {4,&n1,NULL}; TreeNode n3 = {16,NULL,NULL}; TreeNode n4 = {25,NULL,NULL}; TreeNode n5 = {20,&n3,&n4}; TreeNode n6 = {15,&n2,&n5}; TreeNode* root = &n6; int main(){ printf("레벨 순회 = "); level_order(root); }

<출력>

<code />
레벨 순회 = [15] [4] [20] [1] [16] [25]

 


2.6. 후위 순회 활용 - 수식트리계산

  • 자식 노드 먼저 처리 후 부모 노드 처리
<code />
#include <stdio.h> #include <stdlib.h> #include <memory.h> typedef struct TreeNode{ int data; struct TreeNode *left, *right; }TreeNode; TreeNode n1 = {1,NULL,NULL}; TreeNode n2 = {4,NULL,NULL}; TreeNode n3 = {'*',&n1,&n2}; TreeNode n4 = {16,NULL,NULL}; TreeNode n5 = {25,NULL,NULL}; TreeNode n6 = {'+',&n4,&n5}; TreeNode n7 = {'+',&n3,&n6}; TreeNode *test = &n7; int evaluate(TreeNode *root){ if(root == NULL) return 0; if(root -> left == NULL && root -> right == NULL) return root -> data; int op1 = evaluate(root -> left); int op2 = evaluate(root -> right); printf("%d %c %d을 계산합니다.\n",op1,root->data,op2); switch(root->data){ case '+': return op1 + op2; case '-': return op1 - op2; case '*': return op1 * op2; case '/': return op1 /op2; } return 0; } int main(){ printf("수식의 값 = %d",evaluate(test)); }

<출력>

<code />
1 * 4을 계산합니다. 16 + 25을 계산합니다. 4 + 41을 계산합니다. 수식의 값 = 45

 


2.7. 후위 순회 활용 - 폴더 용량 계산

<code />
/* --- TreeNode 구조체 ---- */ int calc_dir_size(TreeNode* root){ if(root == NULL) return 0; int left_size = calc_dir_size(root->left); int right_size = calc_dir_size(root->right); return (root -> data + left_size + right_size); } int main() { TreeNode n4 = { 200, NULL, NULL }; TreeNode n5 = { 500, NULL, NULL }; TreeNode n3 = { 100, &n4, &n5 }; TreeNode n2 = { 50, NULL, NULL }; TreeNode n1 = { 0, &n2, &n3 }; printf("디렉토리 크기 =%d\n",calc_dir_size(&n1)); }

<출력>

<code />
디렉토리 크기 =850

 


2.8. 이진트리 연산

  • 노드 개수 계산
    • 각 서브트리에 대해 순환 호출 → 반환되는 값에 +1

  • 높이
    • 서브트리에 대해 순환호출 → 반환값 중 최댓값을 구하여 반환

  • 단말노드 개수 계산
    • 트리 노드 전체 순환 → 어떤 노드의 왼쪽 자식과 오른쪽 자식이 동시에 0이면 1 반환
      → 그렇지 않으면 비단말노드이므로 각각의 서브트리에 대해 순환호출 → 반환값을 서로 더함
<code />
#include <stdio.h> #include <stdlib.h> typedef struct TreeNode{ int data; struct TreeNode *left, *right; }TreeNode; TreeNode n1 = {1,NULL,NULL}; TreeNode n2 = {4,&n1,NULL}; TreeNode n3 = {16,NULL,NULL}; TreeNode n4 = {25,NULL,NULL}; TreeNode n5 = {20,&n3,&n4}; TreeNode n6 = {15,&n2,&n5}; TreeNode* root = &n6; // 노드 개수 세기 함수 int get_node_count(TreeNode *node){ int count = 0; if(node != NULL){ count = 1+ get_node_count(node -> left) + get_node_count(node -> right); } return count; } // 단말 노드 개수 세기 함수 int get_leaf_count(TreeNode *node){ int count = 0; if(node != NULL){ if(node -> left == NULL && node -> right == NULL) return 1; else count = get_leaf_count(node -> left) + get_leaf_count(node -> right); } return count; } // 노드 높이 측정 int get_height(TreeNode *node){ int height = 0; if(node != NULL){ int lHeight = get_height(node -> left); int rHeight = get_height(node -> right); height = (lHeight >= rHeight)? 1+lHeight : 1+rHeight; } return height; } int main() { printf("노드 개수 : %d\n", get_node_count(root)); printf("단말노드 개수 : %d\n", get_leaf_count(root)); printf("높이 : %d\n", get_height(root)); }

 

<출력>

<code />
노드 개수 : 6 단말노드 개수 : 3 높이 : 3