본문 바로가기
코딩테스트 및 개인공부/자료구조

[자료구조] [8] 트리

by JJPearl 2021. 12. 29.
반응형

트리

-트리의 개념

선형 자료 구조: 스택, 큐, 리스트 -> 자료들이 일렬로 나열된 형태

트리는 계층적인 자료를 표현하는데 이용되는 자료구조.

예) 회사 조직도, 컴퓨터 폴더 구조, 인공지능의 결정 트리..

 

트리 구조

  • A,B,C,D,E,F,G,H,I,J는 노드(node)
  • 트리는 한 개 이상의 노드로 이루어지고, 계층적 구조에서 가장 높은 곳에 있는 노드는 루트(root) 노드
  • 나머지 노드들은 서브 트리(subtree). -> 루트의 다음 레벨에 있는 노드들은 서브 트리들의 루트가 된다.
  • 루트와 서브트리를 연결하는 선은 간선/에지
  • A는 B의 부모 노드. B는 A의 자식 노드. B,C,D는 형제.
  • 자손 노드 : 임의의 노드 하위에 연결된 모든 노드들. 즉 어떤 노드의 서브 트리에 속하는 모든 노드들은 자손노드.
  • 단말노드 : 자식 노드가 없는 노드 / 그렇지 않으면 비단말 노드
  • 차수(degree) : 어떤 노드가 갖고있는 자식의 수. -> 그림에서 루트노드는 자식 노드가 3개이므로 차수가 3. 트리의 차수는 트리가 갖고 있는 노드의 차수 중 가장 큰 값=3.
  • 레벨 : 트리의 각층에 번호 매기는 것. -> 그림에서 A의 레벨은 1, B,C,D의 레벨은 2.
  • 트리의 높이 : 트리가 갖고있는 최대 레벨.

 

 

트리를 표현하는 가장 일반적인 방법은 노드 구조를 이용하는 것.

*노드 : 데이터 필드, 링크 필드

실제로는 두 개의 자식 노드를 사용하는 이진트리(binary tree)가 많이 사용됨.

부모는 왼쪽과 오른쪽 자식에 바로 접근 가능.

- 이진트리의 노드 구조 : 데이터 필드, 링크 필드(링크1:왼쪽 자식,링크2: 오른쪽 자식)

 

 

이진 트리

모든 노드가 2개의 서브트리를 갖는 트리. 서브 트리는 공집합일 수도 있다.

최대 2개까지의 자식 노드를 가질 수 있는데, 자식들 사이에도 순서가 존재해 왼쪽 자식과 오른쪽 자식은 반드시 구별.

  1. 공집합이거나
  2. 루트와 왼쪽 서브트리, 오른쪽 서브 트리로 구성된 노드들의 유한집합으로 정의. 이진트리의 서브 트리들은 모두 이진트리여야 한다.

 

- 이진트리의 분류

  1. 포화 이진트리 : 트리의 각 레벨에 노드가 꽉 차있는 이진트리. 레벨 단위로 각 노드에 왼쪽에서 오른쪽으로 순서대로 번호를 붙일 수 있다. 이 번호는 항상 일정한데, 루트 노드의 오른쪽 자식 노드의 번호는 항상 3.

2. 완전 이진트리 : 마지막 레벨 전까지는 노드가 모두 채워져 있고, 마지막 레벨에서는 왼쪽부터 오른쪽으로 노드가 순서대로 채워져 있는 이진트리. 

마지막 레벨에서는 노드가 꽉차있지 않아도 되지만, 중간에 빈 곳이 있으면 안됨. 오른쪽 그림은 왼쪽 서브트리의 노드에 빈 곳이 있으므로 완전 이진트리 X.

 

 

링크 표현법을 이용한 이진트리 구현

링크 표현법 : 포인터를 이용해 노드와 노드를 연결.

데이터 저장 필드, 두개의 포인터 변수는 각각 왼쪽 자식 노드 / 오른쪽 자식 노드를 가리킴.

 

- 이진트리의 순회

트리 순회 : 트리에 속하는 모든 노드를 한 번씩 방문하여 노드의 데이터를 목적에 맞게 처리.

예) 트리의 모든 항목들 출력

 

이진트리의 표준 순회에는 전위, 중위, 후위 3가지 방법이 있다.

루트와 왼쪽 서브트리, 오른쪽 서브트리를 각가 어떤 순서로 방문하느냐로 구분.

V: 루트방문 / L: 왼쪽 서브트리 방문 / R: 오른쪽 서브트리 방문

  • 전위 순회(preorder traversal) : VLR
  • 중위 순회(inorder traversal) : LVR
  • 후위 순회(preorder traversal) : LRV

*루트 방문이 어디에 있느냐로 기억하면 될듯. 중위는 루트 방문이 중간 순서.

 

이진트리의 각각의 서브트리도 이진트리라서 각 서브트리의 노드들도 동일한 순회 방법이 적용. 

 

 

- 전위 순회

전위 순회에서 루트노드의 방문을 마치고, 왼쪽 서브트리를 방문할 차례일 때

왼쪽 서브트리도 하나의 이진트리이기 때문에 똑같은 방식으로 방문하면 된다.

즉, 왼쪽 서브트리의 루트를 먼저 방문하고 왼쪽 서브트리의 왼쪽 서브트리를 방문, 마지막으로 왼쪽 서브트리의 오른쪽 서브트리를 방문.

전위 순회 순서

void preorder(TNode* n) // 이진트리 전위 순회 함수 
{
	if( n!= nullptr )
    {
    	printf("[%c]",n->data); // 1. 루트 노드 처리
        preorder(n->left); // 2. 왼쪽 서브트리 방문
        preorder(n->right); // 3. 오른쪽 서브트리 방문
     }
}

 

- 중위 순회

void inorder(TNode* n) // 이진트리 중위 순회 함수 
{
	if( n!= nullptr )
    {
        inorder(n->left); // 1. 왼쪽 서브트리 방문
        printf("[%c]",n->data); // 2. 루트 노드 처리
        inorder(n->right); // 3. 오른쪽 서브트리 방문
     }
}

 

- 후위 순회

void postorder(TNode* n) // 이진트리 후위 순회 함수 
{
	if( n!= nullptr )
    {
        postorder(n->left); // 1. 왼쪽 서브트리 방문
        postorder(n->right); // 2. 오른쪽 서브트리 방문
        printf("[%c]",n->data); // 3. 루트 노드 처리
     }
}

 

 

- 순회 방법 선택

  • 순서가 중요하지 않고 노드를 전부 방문하기만 하면 된다면 -> 3가지 방법 중 어떤 것이든 상관 X

예) 트리의 모든 노드 값 순서와 상관없이 출력

  • 자식 노드 처리한 다음 부모 노드 처리해야 하는 문제 -> 후위 순회

예) 컴퓨터에서 각 폴더의 용량 계산 -> 하위 폴더의 용량이 계산되어야 이를 이용해 현재 폴더 용량 계산 가능

  • 부모 노드 처리한 다음 자식 노드 처리해야 하는 문재 -> 전위 순회

예) 모든 노드에서 레벨 계산 -> 루트 노드의 레벨이 1, 모든 노드들은 부모 노드의 레벨보다 1이 크기 때문

 

 

 

- 연산들

- 노드 개수 구하기

이진트리 전체 노드의 개수를 세기 위해서는 모든 노드들의 순회가 필요.

왼쪽 서브트리의 노드 개수와 오른쪽 서브트리의 노드 개수에 루트 노드를 더하면 된다. -> 후위 순회 방식 사용

int count_node(TNode* n) // 이진트리 전체 노드 개수 구하는 순환 함수
	{
		if (n == nullptr)
			return 0;
		return 1 + count_node(n->left) + count_node(n->right); // 루트 + 왼쪽 서브트리 개수 + 오른쪽 서브트리 개수
	}

 

- 단말 노드 개수 구하기

왼쪽 자식과 오른쪽 자식이 동시에 NULL이면 단말노드이고 1을 반환.

아니면 비단말 노드이므로 각각의 서브트리에 대하여 순환 호출해 반환되는 두 값을 더해 반환하면 된다.

int count_leaf(TNode* n) // 이진트리 단말 노드 개수 구하는 순환 함수
	{
		if (n == nullptr)
			return 0;
		if (n->left == nullptr && n->right == nullptr)
			return 1;
		else return count_leaf(n->left) + count_leaf(n->right); // 비단말 노드면 각각의 서브트리에 대해 순환호출 해 반환되는 두값을 더해 반환.
	}

 

- 트리 높이 구하기

왼쪽 서브트리와 오른쪽 서브트리 중에서 더 높은 트리를 먼저 찾고, 이 트리의 높이에 1을 더한 값. -> 후위 순회

int calc_height(TNode* n)
	{
		if (n == nullptr)
			return 0;
		int hLeft = calc_height(n->left); // 왼쪽 서브트리 높이 계산해 저장
		int hRight = calc_height(n->right); // 오른쪽 서브트리 높이 계산해 저장
		return (hLeft > hRight) ? hLeft + 1 : hRight + 1; // 두 높이 중 더 높은 값에 1 더해서 반환
	}

* C++ 삼항(조건)연산자

'조건' : 'A' ? 'B' => 조건이 참이면 A를 반환하고 조건이 거짓이면 B를 반환합니다.

 

 

- 이진 트리 구현 코드

#include <iostream>

using namespace std;

typedef char TElement;

typedef struct BinTrNode {
	TElement data;
	BinTrNode* left; // 왼쪽 자식 노드
	BinTrNode* right; // 오른쪽 자식 노드
} TNode;

class CBinTree {
public:
	void init_tree() { root = nullptr; }
	bool is_empty() { return root == nullptr; }
	TNode* get_root() { return root; }
	void set_root(TNode* n) { root = n; }
	TNode* create_tree(TElement val, TNode* l, TNode* r) 
	{
		TNode* node = new TNode;
		node->data = val;
		node->left = l;
		node->right = r;
		return node;
	}
	void preorder(TNode* n) // 이진트리 전위 순회 함수 
	{
		if (n != nullptr)
		{
			printf("[%c]", n->data); // 1. 루트 노드 처리
			preorder(n->left); // 2. 왼쪽 서브트리 방문
			preorder(n->right); // 3. 오른쪽 서브트리 방문
		}
	}
	void inorder(TNode* n) // 이진트리 중위 순회 함수 
	{
		if (n != nullptr)
		{
			inorder(n->left); // 1. 왼쪽 서브트리 방문
			printf("[%c]", n->data); // 2. 루트 노드 처리
			inorder(n->right); // 3. 오른쪽 서브트리 방문
		}
	}
	void postorder(TNode* n) // 이진트리 후위 순회 함수 
	{
		if (n != nullptr)
		{
			postorder(n->left); // 1. 왼쪽 서브트리 방문
			postorder(n->right); // 2. 오른쪽 서브트리 방문
			printf("[%c]", n->data); // 3. 루트 노드 처리
		}
	}
	int count_node(TNode* n) // 이진트리 전체 노드 개수 구하는 순환 함수
	{
		if (n == nullptr)
			return 0;
		return 1 + count_node(n->left) + count_node(n->right); // 루트 + 왼쪽 서브트리 개수 + 오른쪽 서브트리 개수
	}
	int count_leaf(TNode* n) // 이진트리 단말 노드 개수 구하는 순환 함수
	{
		if (n == nullptr)
			return 0;
		if (n->left == nullptr && n->right == nullptr)
			return 1;
		else return count_leaf(n->left) + count_leaf(n->right); // 비단말 노드면 각각의 서브트리에 대해 순환호출 해 반환되는 두값을 더해 반환.
	}
	int calc_height(TNode* n)
	{
		if (n == nullptr)
			return 0;
		int hLeft = calc_height(n->left); // 왼쪽 서브트리 높이 계산해 저장
		int hRight = calc_height(n->right); // 오른쪽 서브트리 높이 계산해 저장
		return (hLeft > hRight) ? hLeft + 1 : hRight + 1; // 두 높이 중 더 높은 값에 1 더해서 반환
	}
private:
	TNode* root;
};


void main() {
	CBinTree binTr;
	TNode *b, *c, *d, *e, *f;
	d = binTr.create_tree('D', nullptr, nullptr);
	e = binTr.create_tree('E', nullptr, nullptr);
	b = binTr.create_tree('B', d, e);
	f = binTr.create_tree('F', nullptr, nullptr);
	c = binTr.create_tree('C', f, nullptr);
	binTr.set_root(binTr.create_tree('A', b, c));

	// 트리 테스트
	printf("\n In-Order(중위순회) : "); binTr.inorder(binTr.get_root());
	printf("\n Pre-Order(전위순회) : "); binTr.preorder(binTr.get_root());
	printf("\n Post-Order(후위순회) : "); binTr.postorder(binTr.get_root());

	printf("\n 노드의 개수 = %d\n ", binTr.count_node(binTr.get_root()));
	printf("\n 단말노드의 개수 = %d\n ", binTr.count_leaf(binTr.get_root())); 
	printf("\n 트리의 높이 = %d\n ", binTr.calc_height(binTr.get_root()));


}

실행 결과

 

반응형