트리/힙/그래프(1) - 트리

  • 비선형 자료 구조 사용해야 하는 경우 판별
  • 데이터 표현과 문제 해결을 위해 트리 / 그래프 구조 구현하여 사용
  • 다양한 방법으로 트리 순회
  • 주어진 상황에 맞게 다양한 방법으로 그래프 표현

1. 비선형 문제

1) 계층적 문제

  • 조직도와 같이 계층적 속성을 가지는 문제
  • 트리라는 자료 구조를 사용
  • 데이터가 저장된 노드와 노드와 노드를 잇는 엣지로 구성

2) 순환 종속성

  • 친구관계도와 같이 그래프로 표현
  • 노드와 엣지로 구성

2. 트리

1) 연습 문제 7: 조직도 구조 만들기

#include <iostream>
#include <queue>

struct node
{
	std::string position;
	node* first;
	node* second;
};

struct org_tree
{
	node* root;

	static org_tree create_org_structure(const std::string& pos)
	{
		org_tree tree;
		tree.root = new node {pos, NULL, NULL};
		return tree;
	}

	static node* find(node* root, const std::string& value)
	{
		if (root == NULL)
			return NULL;

		if (root->position == value)
			return root;

		auto firstFound = org_tree::find(root->first, value);

		if (firstFound != NULL)
			return firstFound;

		return org_tree::find(root->second, value);
	}

	bool addSubordinate(const std::string& manager, const std::string& subordinate)
	{
		auto managerNode = org_tree::find(root, manager);

		if (!managerNode)
		{
			std::cout << manager << "을(를) 찾을 수 없습니다: " << std::endl;
			return false;
		}

		if (managerNode->first && managerNode->second)
		{
			std::cout << manager << " 아래에 " << subordinate << "를 추가할 수 없습니다." << std::endl;
			return false;
		}

		if (!managerNode->first)
			managerNode->first = new node {subordinate, NULL, NULL};
		else
			managerNode->second = new node {subordinate, NULL, NULL};

		std::cout << manager << " 아래에 " << subordinate << "를 추가했습니다." << std::endl;

		return true;
	}
};

int main()
{
	auto tree = org_tree::create_org_structure("CEO");

	tree.addSubordinate("CEO", "부사장");
	tree.addSubordinate("부사장", "IT부장");
	tree.addSubordinate("부사장", "마케팅부장");
	tree.addSubordinate("IT부장", "보안팀장");
	tree.addSubordinate("IT부장", "앱개발팀장");
	tree.addSubordinate("마케팅부장", "물류팀장");
	tree.addSubordinate("마케팅부장", "홍보팀장");
	tree.addSubordinate("부사장", "재무부장");
}
// 해당 연습문제에서는 노드를 동적 생성한 이후 해제하지 않아 메모리 릭이 발생

2) 트리 순회

(1) 전위 순회

  • 현재 노드를 방문하고 그다음은 현재 노드의 왼쪽 하위 노드를 마지막으로는 오른쪽 하위노드를 재귀적인 방식으로 방문
  • 루트 노드에서만 수행하는 것이 아닌 루트 노드 아래의 서브 트리에 모두 해당
static void preOrder(node* start)
{
	if(!start)
		return ;
	std::cout<<start->position<<",";
	preOrder(start->first);
	preOrder(start->second);
}

(2) 중위 순회

  • 왼쪽 노드를 제일 먼저 방문하고 그 다음에는 현재 노드 마지막으로 오른쪽 노드 방문
static void inOrder(node* start)
{
	if(!start)
		return ;
	inOrder(start->first);
	std::cout<<start->position<<",";
	inOrder(start->second);
}

(3) 후위 순회

  • 두 자식 노드를 먼저 방문한 후 현재 노드 방문
static void postOrder(node* start)
{
	if(!start)
		return ;
	postOrder(start->first);
	postOrder(start->second);
	std::cout<<start->position<<",";

}

(4) 레벨 순서 순회

  • 트리의 맨 위 레벨부터 아래 레벨까지 왼쪽 노드에서 오른쪽 노드 순서로 방문

3) 연습 문제 8: 레벨 순서 순회 구현하기

#include <iostream>
#include <queue>

struct node
{
	std::string position;
	node* first;
	node* second;
};

struct org_tree
{
	node* root;

	static org_tree create_org_structure(const std::string& pos)
	{
		org_tree tree;
		tree.root = new node {pos, NULL, NULL};
		return tree;
	}

	static node* find(node* root, const std::string& value)
	{
		if (root == NULL)
			return NULL;

		if (root->position == value)
			return root;

		auto firstFound = org_tree::find(root->first, value);

		if (firstFound != NULL)
			return firstFound;

		return org_tree::find(root->second, value);
	}

	bool addSubordinate(const std::string& manager, const std::string& subordinate)
	{
		auto managerNode = org_tree::find(root, manager);

		if (!managerNode)
		{
			std::cout << manager << "을(를) 찾을 수 없습니다: " << std::endl;
			return false;
		}

		if (managerNode->first && managerNode->second)
		{
			std::cout << manager << " 아래에 " << subordinate << "를 추가할 수 없습니다." << std::endl;
			return false;
		}

		if (!managerNode->first)
			managerNode->first = new node {subordinate, NULL, NULL};
		else
			managerNode->second = new node {subordinate, NULL, NULL};

		std::cout << manager << " 아래에 " << subordinate << "를 추가했습니다." << std::endl;

		return true;
	}
	
	static void preOrder(node* start)
	{
		if (!start)
			return;

		std::cout << start->position << ", ";
		preOrder(start->first);
		preOrder(start->second);
	}

	static void inOrder(node* start)
	{
		if (!start)
			return;

		inOrder(start->first);
		std::cout << start->position << ", ";
		inOrder(start->second);
	}

	static void postOrder(node* start)
	{
		if (!start)
			return;

		postOrder(start->first);
		postOrder(start->second);
		std::cout << start->position << ", ";
	}

	static void levelOrder(node* start)
	{
		if (!start)
			return;

		std::queue<node*> q;
		q.push(start);

		while (!q.empty())
		{
			int size = q.size();
			for (int i = 0; i < size; i++)
			{
				auto current = q.front();
				q.pop();

				std::cout << current->position << ", ";
				if (current->first)
					q.push(current->first);
				if (current->second)
					q.push(current->second);
			}

			std::cout << std::endl;
		}
	}
};

int main()
{
	auto tree = org_tree::create_org_structure("CEO");

	tree.addSubordinate("CEO", "부사장");
	tree.addSubordinate("부사장", "IT부장");
	tree.addSubordinate("부사장", "마케팅부장");
	tree.addSubordinate("IT부장", "보안팀장");
	tree.addSubordinate("IT부장", "앱개발팀장");
	tree.addSubordinate("마케팅부장", "물류팀장");
	tree.addSubordinate("마케팅부장", "홍보팀장");
	tree.addSubordinate("부사장", "재무부장");

//	org_tree::preOrder(tree.root); std::cout << std::endl;
//	org_tree::inOrder(tree.root); std::cout << std::endl;
//	org_tree::postOrder(tree.root); std::cout << std::endl;

	org_tree::levelOrder(tree.root);
}

3. 다양한 트리 구조

1) 이진 검색 트리 (Binary Search Tree)

  • 부모 노드의 값 ≥ 왼쪽 자식 노드의 값 부모 노드의 값 ≤ 오른쪽 자식 노드의 값
  • 부모 노드보다 작은 원소 무조건 왼쪽에 위치, 크거나 같은 원소는 오른쪽에 위치

(1) BST에서 원소 검색

  • 이진 검색 트리의 속성을 이용하여 검색
  • 원소 검색 시 트리의 모든 원소를 방문하지 않아도 됨

(2) BST에서 새 원소 삽입하기

  • 먼저 원소가 삽입될 위치의 부모 노드 확인 (원소 검색과 유사)
  • 부모 노드 밑에 삽입

(3) BST에서 원소 삭제하기

  • 삭제할 노드 찾기
  • 자식 노드가 없는 경우 : 단순히 해당 노드 삭제
  • 자식 노드가 하나만 있는 경우 : 노드 삭제 후, 부모 노드의 포인터가 해당 자식 노드를 가르키도록 조정
  • 자식 노드가 두 개 있는 경우 : 노드 삭제 후, 현재 노드를 후속 노드로 대체 → 후속 노드 (자식 노드 다음으로 큰 숫자를 가진 노드)

2) 트리 연산의 시간 복잡도

  • 이론적으로는 O(log N) 이지만 트리의 모양에 따라 바뀔 수 있어 정확하진 않음

3) 연습문제 9: BST 구현하기

#include <iostream>

struct node
{
	int data;
	node* left;
	node* right;
};

struct bst
{
	node* root = nullptr;

	node* find(int value)
	{
		return find_impl(root, value);
	}

private:
	node* find_impl(node* current, int value)
	{
		if (!current)
		{
			std::cout << std::endl;
			return NULL;
		}

		if (current->data == value)
		{
			std::cout << value << "을(를) 찾았습니다." << std::endl;
			return current;
		}

		if (value < current->data) // value 값이 현재 노드 왼쪽에 있는 경우
		{
			std::cout << current->data << "에서 왼쪽으로 이동: ";
			return find_impl(current->left, value);
		}

		// value 값이 현재 노드 오른쪽에 있는 경우
		std::cout << current->data << "에서 오른쪽으로 이동: ";
		return find_impl(current->right, value);
	}

public:
	void insert(int value)
	{
		if (!root)
			root = new node {value, NULL, NULL};
		else
			insert_impl(root, value);
	}

private:
	void insert_impl(node* current, int value)
	{
		if (value < current->data)
		{
			if (!current->left)
				current->left = new node {value, NULL, NULL};
			else
				insert_impl(current->left, value);
		}
		else
		{
			if (!current->right)
				current->right = new node {value, NULL, NULL};
			else
				insert_impl(current->right, value);
		}
	}

public:
	void inorder()
	{
		inorder_impl(root);
	}

private:
	void inorder_impl(node* start)
	{
		if (!start)
			return;

		inorder_impl(start->left);			// 왼쪽 하위 트리 방문
		std::cout << start->data << " ";	// 현재 노드 출력
		inorder_impl(start->right);			// 오른쪽 하위 트리 방문
	}

public:
	node* successor(node* start)
	{
		auto current = start->right;
		while (current && current->left)
			current = current->left;
		return current;
	}

	void deleteValue(int value)
	{
		root = delete_impl(root, value);
	}

private:
	node* delete_impl(node* start, int value)
	{
		if (!start)
			return NULL;

		if (value < start->data)
			start->left = delete_impl(start->left, value);
		else if (value > start->data)
			start->right = delete_impl(start->right, value);
		else
		{
			if (!start->left) // 자식 노드가 전혀 없거나, 왼쪽 자식 노드만 없는 경우
			{
				auto tmp = start->right;
				delete start;
				return tmp;
			}

			if (!start->right) // 오른쪽 자식 노드만 없는 경우
			{
				auto tmp = start->left;
				delete start;
				return tmp;
			}

			// 자식 노드가 둘 다 있는 경우
			auto succNode = successor(start);
			start->data = succNode->data;

			// 오른쪽 하위 트리에서 후계자(successor)를 찾아 삭제
			start->right = delete_impl(start->right, succNode->data);
		}

		return start;
	}
};

int main()
{
	bst tree;
	tree.insert(12);
	tree.insert(10);
	tree.insert(20);
	tree.insert(8);
	tree.insert(11);
	tree.insert(15);
	tree.insert(28);
	tree.insert(4);
	tree.insert(2);

	std::cout << "중위 순회: ";
	tree.inorder(); // BST의 모든 원소를 오름차순으로 출력합니다.
	std::cout << std::endl;

	tree.deleteValue(12);
	std::cout << "12를 삭제한 후 중위 순회: ";
	tree.inorder(); // BST의 모든 원소를 오름차순으로 출력합니다.
	std::cout << std::endl;
	
	if (tree.find(12))
		std::cout << "원소 12는 트리에 있습니다." << std::endl;
	else
		std::cout << "원소 12는 트리에 없습니다." << std::endl;
}

4) 균형 트리

  • 이진 검색 트리에서 한 쪽으로 편향될 경우 검색을 하는 데 시간이 소요가 큼
  • 따라서 트리의 높이를 최적화하여 시간 복잡도를 최적화함
  • 트리의 균형을 잡는 방법은 AVL트리, 레드-블랙 트리 등
  • AVL 트리 : BST의 속성은 유지하면서 트리 높이의 균형을 위해 약간의 회전 수행

5) N-항 트리

  • 각 노드에 N개의 자식을 가질 수 있음
struct nTree
{
	int data;
	std::vector<nTree*> children;
};
  • 컴퓨터 파일 시스템 구조 / 컴파일러 등에서 사용하고 있음

6) 실습 문제 4 : 파일 시스템 자료 구조 만들기

#include <iostream>
#include <vector>
#include <algorithm>

struct n_ary_node
{
	std::string name;
	bool is_dir;
	std::vector<n_ary_node*> children;
};

struct file_system
{
	using node = n_ary_node;
	using node_ptr = node*;

private:
	node_ptr root;
	node_ptr cwd;

public:
	file_system()
	{
		root = new node {"/", true, {}};
		cwd = root; // 처음에는 루트를 현재 디렉토리로 설정
	}

	node_ptr find(const std::string& path)
	{
		if (path[0] == '/') // 절대 경로
		{
			return find_impl(root, path.substr(1));
		}
		else // 상대 경로
		{
			return find_impl(cwd, path);
		}
	}

private:
	node_ptr find_impl(node_ptr directory, const std::string& path)
	{
		if (path.empty())
			return directory;
		auto sep = path.find('/');
		std::string current_path = sep == std::string::npos ? path : path.substr(0, sep);
		std::string rest_path = sep == std::string::npos ? "" : path.substr(sep + 1);
		auto found = std::find_if(directory->children.begin(), directory->children.end(),
			[&](const node_ptr child) {
				return child->name == current_path;
			});
		if (found != directory->children.end())
		{
			return find_impl(*found, rest_path);
		}
		return NULL;
	}

public:
	bool add(const std::string& path, bool is_dir)
	{
		if (path[0] == '/')
		{
			return add_impl(root, path.substr(1), is_dir);
		}
		else
		{
			return add_impl(cwd, path, is_dir);
		}
	}

private:
	bool add_impl(node_ptr directory, const std::string& path, bool is_dir)
	{
		if (not directory->is_dir)
		{
			std::cout << directory->name << "은(는) 파일입니다." << std::endl;
			return false;
		}

		auto sep = path.find('/');

		// path에 '/'가 없는 경우
		if (sep == std::string::npos)
		{
			auto found = std::find_if(directory->children.begin(), directory->children.end(), [&](const node_ptr child) {
				return child->name == path;
				});

			if (found != directory->children.end())
			{
				std::cout << directory->name << "에 이미 " << path << " 이름의 파일/디렉토리가 있습니다." << std::endl;
				return false;
			}

			directory->children.push_back(new node {path, is_dir, {}});
			return true;
		}

		// path에 '/'가 있는 경우, 즉, 디렉토리 이름을 포함하고 있는 경우.
		std::string next_dir = path.substr(0, sep);
		auto found = std::find_if(directory->children.begin(), directory->children.end(), [&](const node_ptr child) {
			return child->name == next_dir && child->is_dir;
			});

		if (found != directory->children.end())
		{
			return add_impl(*found, path.substr(sep + 1), is_dir);
		}

		// path에 디렉토리 이름이 포함되어 있지만, 해당 디렉토리가 없는 경우.
		std::cout << directory->name << "에 " << next_dir << " 이름의 디렉토리가 없습니다." << std::endl;
		return false;
	}

public:
	bool change_dir(const std::string& path)
	{
		auto found = find(path);
		if (found && found->is_dir)
		{
			cwd = found;
			std::cout << "현재 디렉토리를 " << cwd->name << "로 이동합니다." << std::endl;
			return true;
		}

		std::cout << path << " 경로를 찾을 수 없습니다." << std::endl;
		return false;
	}

public:
	void show_path(const std::string& path)
	{
		auto found = find(path);
		if (not found)
		{
			std::cout << path << " 경로가 존재하지 않습니다." << std::endl;
			return;
		}

		if (found->is_dir)
		{
			for (auto child : found->children)
			{
				std::cout << (child->is_dir ? "d " : "- ") << child->name << std::endl;
			}
		}
		else
		{
			std::cout << "- " << found->name << std::endl;
		}
	}
};

int main()
{
	file_system fs;
	fs.add("usr", true);       // "/"에 usr 디렉토리 추가
	fs.add("etc", true);       // "/"에 etc 디렉토리 추가
	fs.add("var", true);       // "/"에 var 디렉토리 추가
	fs.add("tmp_file", false); // "/"에 tmp_file 파일 추가

	std::cout << "\\"/\\"의 파일/디렉토리 목록:" << std::endl;
	fs.show_path("/");        // "/"의 파일/디렉토리 목록 출력

	std::cout << std::endl;
	fs.change_dir("usr");
	fs.add("gilbut", true);
	fs.add("gilbut/Downloads", true);
	fs.add("gilbut/Downloads/newFile.cpp", false);

	std::cout << "현재 디렉토리에서 usr의 파일/디렉토리 목록: " << std::endl;
	fs.show_path("usr"); // 현재 디렉토리에는 usr 디렉토리가 없으므로 정상적으로 출력하지 못합니다.

	std::cout << "\\"/usr\\"의 파일/디렉토리 목록:" << std::endl;
	fs.show_path("/usr");

	std::cout << "\\"/usr/gilbut/Downloads\\"의 파일/디렉토리 목록" << std::endl;
	fs.show_path("/usr/gilbut/Downloads");
}

 

 

출처 : 코딩 테스트를 위한 자료 구조와 알고리즘 with C++ (길벗)

'Computer Science > Data Structure & Algorithm' 카테고리의 다른 글

그리디 알고리즘  (0) 2023.07.02
분할 정복  (0) 2023.07.01
해시 테이블과 블룸 필터  (0) 2023.06.29
트리/힙/그래프(2) - 힙&그래프  (0) 2023.06.28
리스트 / 스택 /큐  (0) 2023.06.26