선형 자료구조(linear data structure): 자료들이 직선과 같이 나열되어 있는 구조(리스트, 스택, 큐)
계층적인 구조(hierarchial structure): 자료의 계층적인 관계를 표현할 수 있는 구조(트리)
트리(tree): 계층적인 자료를 표현하는데 이용되는 자료구조, 한 개 이상의 노드로 이루어진 유한 집합
ex) 결정 트리(decision tree): 인간의 의사 결정 구조를 표현하는 한 가지 방법
노드(node): 트리의 구성 요소
루트 노드(root node): 계층적인 구조에서 가장 높은 곳에 있는 노드
서브트리(subtree): 루트 노드 제외한 나머지 노드들
간선(edge): 루트 노드와 서브 트리를 연결하는 선
부모 노드(parent node), 자식 노드(children node), 형제 관계(sibling)
조상 노드(ancestor node): 루트 노드에서 임의의 노드까지의 경로를 이루고 있는 노드들
자손 노드(descendent node): 임의의 노드 하위에 연결된 모든 노들을 의미, 어떤 노드의 서브 노드에 속하는 모든 노드들은 자손 노드
단말 노드(terminal node, leaf node): 자식 노드가 없는 노드, 차수가 0인 노드
비단말 노드(nonterminal node): 자식 노드가 있는 노드
차수(degree): 어떤 노드가 가지고 있는 자식 노드의 개수
트리의 차수: 노드의 차수 중 가장 큰 차수
트리의 레벨(level): 트리의 각 층에 번호를 매기는 것(루트 노드의 레벨 1)
트리의 높이(height): 트리가 가지고 있는 최대 레벨
포리스트(forest): 트리들의 집합
트리 표현 방법
-> 각 노드가 데이터를 저장하는 데이터 필드와 자식 노드를 가리키는 링크 필드를 가지게 하는 것 -> 트리에서 각 노드들은 서로 다른 개수의 자식 노드를 가지므로 노드에 따라 링크 필드의 수가 달라짐
-> 문제점: 노드의 크기가 고정되지 않아 노드에 붙어 있는 서브 트리의 개수에 따라 노드의 크기가 커지기도 하고 작아지기도 함, 자식 노드의 개수가 일정하지 않으면 프로그램이 복잡해짐
-> 따라서, 이 책에서는 자식 노드가 2개인 이진 트리만 다룸
이진 트리(binary tree): 모든 노드가 2개의 서브 트리를 가지고 있는 트리
-서브 트리는 공집합일 수 있음
-최대 2개까지의 자식 노드가 존재할 수 있고, 모든 노드의 차수는 2이하가 됨
-서브 트리 간 순서가 존재
(1) 공집합이거나
(2) 루트와 왼쪽 서브 트리, 오른쪽 서브 트리로 구성된 노드들의 유한 집합으로 정의, 이진 트리의 서브 트리들은 모두 이진 트리여야 함
이진 트리 vs 일반 트리
typdef struct TreeNode{
int data;
struct TreeNode *left, *right;
}TreeNode;
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
typdef struct TreeNode{
int data;
struct TreeNode *left, *right;
}TreeNode;
// n1
// / |
// n2 n3
int main(){
TreeNode *n1, *n2, *n3;
n1 = (TreeNode*)malloc(sizeof(TreeNode));
n2 = (TreeNode*)malloc(sizeof(TreeNode));
n3 = (TreeNode*)malloc(sizeof(TreeNode));
// 첫 번째 노드 설정
n1->data = 10;
n1->left = n2;
n1->right = n3;
// 두 번째 노드 설정
n2->data = 20;
n2->left = NULL;
n2->right = NULL;
// 세 번째 노드 설정
n3->data = 30;
n3->left = NULL;
n3->right = NULL;
return 0;
}
링크법으로 표현된 트리는 루트 노드를 가리키는 포인터만 있으면 트리 안의 모든 노드들에 접근할 수 있음
이진 트리 순회(traversal): 이진 트리에 속하는 모든 노드를 한 번씩 방문하여 노드가 가지고 있는 데이터를 목적에 맞게 처리하는 것을 의미
이진 트리 순회 방법: 전위, 중위, 후위 3가지 방법 -> 루트와 왼쪽 서브 트리, 오른쪽 서브 트리 중에서 루트를 언제 방문하냐에 따라 구분
-> 루트 노드를 방문하는 작업을 V, 왼쪽 노드를 방문하는 작업을 L, 오른쪽 노드를 방문하는 작업을 R이라고 가정
전위 순회(preorder traversal): VLR
중위 순회(inorder traversal): LVR
후위 순회(postorder traversal): LRV
트리 순회 알고리즘은 순환 기법을 사용
-> 이진 트리에서 전체 트리나 서브 트리의 구조는 완전히 동일
-> 따라서, 전체 트리 순회에 사용된 알고리즘은 똑같이 서브 트리에 적용할 수 있음(다만 문제의 크기가 작아짐)
전위 순회: 루트를 먼저 방문하고 그 다음에 왼쪽 서브 트리를 방문하고 오른쪽 서브 트리를 마지막으로 방문
preorder(x)
if x != NULL
then print x->data; // 1. 루트 노드 방문
preorder(x->left); // 2. 왼쪽 서브 트리 방문
preorder(x->right); // 3. 오른쪽 서브 트리 방문
1. 노드 x가 NULL이면 더 이상 순환 호출을 하지 않음
2. x의 데이터를 출력
3. x의 왼쪽 서브 트리를 순환 호출하여 방문
4. x의 오른쪽 서브 트리를 순환 호출하여 방문
중위 순회: 먼저 왼쪽 서브 트리, 루트, 오른쪽 서브 트리 순으로 방문
inorder(x)
if x != NULL
then inorder(x->left); // 1. 왼쪽 서브 트리 방문
print x->data; // 2. 루트 노드 방문
inorder(x->right); // 3. 오른쪽 서브 트리 방문
1. 노드 x가 NULL이면 더 이상 순환 호출을 하지 않음
2. x의 왼쪽 서브 트리를 순환 호출하여 방문
3. x의 데이터를 출력
4. x의 오른쪽 서브 트리를 순환 호출하여 방문
후위 순회: 먼저 왼쪽 서브 트리, 오른쪽 서브 트리, 루트 순으로 방문
postorder(x)
if x != NULL
then postorder(x->left); // 1. 왼쪽 서브 트리 방문
postorder(x->right); // 2. 오른쪽 서브 트리 방문
print x->data; // 3. 루트 노드 방문
1. 노드 x가 NULL이면 더 이상 순환 호출을 하지 않음
2. x의 왼쪽 서브 트리를 순환 호출하여 방문
3. x의 오른쪽 서브 트리를 순환 호출하여 방문
4. x의 데이터를 출력
// 전위 순회
void preorder(TreeNode *root){ // 매개변수: 루트를 가리키는 포인터
if(root){
printf("%d ",root->data); // 노드 방문
preorder(root->left); // 왼쪽 서브 트리 순회
preorder(root->right); // 오른쪽 서브 트리 순회
}
}
// 중위 순회
void inorder(TreeNode *root){
if(root){
inorder(root->left); // 왼쪽 서브 트리 순회
printf("%d ",root->data); // 노드 방문
inorder(root->right); // 오른쪽 서브 트리 순회
}
}
// 후위 순회
void postorder(TreeNode *root){
if(root){
postorder(root->left);
postorder(root->right);
printf("%d ",root->data);
}
}
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
typedef struct TreeNode{
int data;
struct TreeNode *left, *right;
}TreeNode;
// 15
// 4 20
// 1 16 25
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;
// preorder, inorder, postorder 함수를 여기에 삽입
//...
// 주 함수
int main(){
printf("preorder: ");
preorder(root);
printf("\n");
printf("inorder: ");
inorder(root);
printf("\n");
printf("postorder: ");
postorder(root);
printf("\n");
return 0;
}
레벨 순회(level order): 각 노드를 레벨 순으로 검사하는 순회 방법
level_order(root)
initialize queue;
if(root = NULL) then return;
enqueue(queue, root);
while is_empty(queue) != TRUE do
x <- dequeue(queue);
print x <- data
if(x->left != NULL) then
enqueue(queue,x->left);
if(x->right != NULL) then
enqueue(queue, x->right);
큐를 초기화
트리 root가 NULL이면 종료
트리 root의 루트 노드를 큐에 삽입
큐가 공백 상태가 될 때까지 계속 다음을 반복
큐의 맨 처음에 있는 노드를 x로 가져옴
x의 데이터 값 출력
x의 왼쪽 자식이 NULL이 아니면 큐에 삽입
x의 오른쪽 자식이 NULL이 아니면 큐에 삽입
typedef TreeNode * element;
// 큐의 소스를 여기에
// ...
// 레벨 순회
void level_order(TreeNode*ptr){
QueueType q;
init(&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);
}
}
}
수식 트리(expression tree): 산술식이나 논리식의 연산자와 피연산자들로부터 만들어짐, 피연산자는 단말 노드가 되며, 연산자는 비단말 노드가 됨
-> 가장 적합한 순회 방법은 자식 노드를 먼저 방문하는 후위 순회
수식 | a+b | a-(b*c) | (a<b)or(c<d) |
---|---|---|---|
전위 순회 | +ab | -a*bc | or<ab<cd |
중위 순회 | a+b | a-(b*c) | (a<b)or(c<d) |
후위 순회 | ab+ | abc*- | ab<cd<or |
evaluate(exp)
if exp = NULL
then return 0;
if exp->left = NULL and exp->right = NULL
then return exp->data;
x <- evaluate(exp->left);
y <- evaluate(exp->right);
op <- exp.data;
return (x op y);
만약 자식 노드들이 없으면 데이터 필드 값을 반환
왼쪽 서브 트리에 대하여 evaluate를 순환 호출
오른쪽 서브 트리에 대하여 evaluate를 순환 호출
데이터 필드에서 연산자를 추출
추출된 연산자를 가지고 연산을 수행해서 결과 값을 반환
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct TreeNode {
int data;
struct TreeNode *left, *right;
} TreeNode;
// +
// * +
// 1 4 16 25
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 *exp = &n7;
// 수식 계산 함수
int evaluate(TreeNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return root->data;
else { // 후위 순회 방법 이용
int op1 = evaluate(root->left);
int op2 = evaluate(root->right);
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(exp));
}
-> 후위 순회를 사용하되 순환 호출되는 순회 함수가 용량을 반환하도록 만들어야 함
int calc_direc_size(TreeNode *root){
int left_size, right_size;
if(root !=NULL){
left_size = calc_direc_size(root->left);
right_size = calc_direc_size(root->right);
return (root->data+left_size+right_size);
}
return 0;
}
int main(){
TreeNode n4={500,NULL,NULL};
TreeNode n5={200,NULL,NULL};
TreeNode n3={100,&n4,&n5};
TreeNode n2={50,NULL,NULL};
TreeNode n1={0,&n2,&n3};
printf("Directory Size=%d\n",calc_direc_size(&n1));
return 0;
}
get_count(x)
if x != NULL then
return 1+get_count(x->left)+get_count(x->right);
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;
}
get_leaf(T)
if T != NULL then
if T->left = NULL and T->right = NULL
then return 1;
else return get_leaf(T->left) + get_leaf(T->right);
int get_leaf_count(TreeNode *node){
int count;
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;
}
get_height(T)
if T != NULL then
return 1 + max(get_height(T->left), get_height(T->right));
int get_height(TreeNode *node){
int height = 0;
if(node != NULL){
height = 1 + max(get_height(node->left), get_height(node->right));
}
return height;
}
int get_nonleaf_count(Treenode *node){
return get_node_count(node) - get_leaf_count(node);
}
int equal(TreeNode* t1, TreeNode* t2){
int ret = false;
if(t1 == NULL && t2 == NULL) ret = true;
else if(t1 != null && t2 != null && (t1->data == t2->data) && equal(t1->left, t2->left) && equal(t1->right,t2->right)){
ret = true;
}
return ret;
}
int equal(TreeNode *first, TreeNode *second){
return ((!first && !second) || (first && second &&
(first->data == second->data) &&
equal(first->left, second->left) &&
equal(first->right, second->right)));
}
트리의 노드의 개수가 n개면, 링크의 개수는 2n개(각 노드당 2개의 링크가 있으므로)
링크 중에서 n-1개의 링크들이 루트 노드를 제외한 n-1개의 노드를 가리킴. 따라서 2n개 중에서 n-1개는 NULL 링크가 아니지만 나머지 n+1개의 링크는 NULL임을 알 수 있음.
따라서, NULL 링크를 이용하여 순환 호출 없이 트리의 노드들을 순회할 수 있는지 생각해볼 수 있음
NULL 링크에 중위 순회 시에 선행 노드인 중위 선행자(inorder predecessor)나 중위 순회 시에 후속 노드인 중위 후속자(inorder successor)를 저장시켜 놓은 트리가 스레드 이진 트리(threaded binary tree) -> 스레드(thread) 즉, 실을 이용하여 노드들을 순회 순서대로 연결 시켜놓은 트리
문제를 단순화 하기위해 중위 후속자만 저장
typedef struct TreeNode{
int data;
struct TreeNode *left, *right;
int is_thread; // 만약 오른쪽 링크가 쓰레드라면 true -> 태그 필드
}TreeNode;
// 노드 p의 중위 후속자를 반환하는 함수
TreeNode *find_successor(TreeNode *p){
// q는 p의 오른쪽 포인터
TreeNode *q = p->right;
// 만약 오른쪽 포인터가 NULL이거나 스레드이면 오른쪽 포인터를 반환
if(q == NULL || p->is_thread == true){
return q;
}
// 만약 오른쪽 자식이면 다시 가장 왼쪽 노드로 이동
while(q->left != NULL) q = q->left;
return q;
}
// 스레드 이진 트리에서 중위 순회를 하는 함수
// 중위 순회는 가장 왼쪽 노드부터 시작하기 때문에, 왼쪽 자식이 NULL이 될 때까지 왼쪽 링크를 타고 이동
void thread_inorder(TreeNode *t){
TreeNode *q;
q = t;
while(q->left != NULL) q = q->left; // 가장 왼쪽 노드로 감
do{
printf("%c",q->data); // 데이터 출력
q = find_successor(q); // 후속자 함수 호출
}while(q != null); // null이 아니면
}
#include <stdio.h>
#define TRUE 1
#define FALSE 0
typedef struct TreeNode {
int data;
struct TreeNode *left, *right;
int is_thread; // 스레드이면 true
} TreeNode;
// G
// C F
// A B D E
TreeNode n1 = { 'A', NULL, NULL, 1 };
TreeNode n2 = { 'B', NULL, NULL, 1 };
TreeNode n3 = { 'C', &n1, &n2, 0 };
TreeNode n4 = { 'D', NULL, NULL, 1 };
TreeNode n5 = { 'E', NULL, NULL, 0 };
TreeNode n6 = { 'F', &n4, &n5, 0 };
TreeNode n7 = { 'G', &n3, &n6, 0 };
TreeNode* exp = &n7;
TreeNode* find_successor(TreeNode* p)
{
TreeNode* q = p->right;
if (q == NULL || p->is_thread == true) {
return q;
}
while (q->left != NULL)
q = q->left;
return q;
}
void thread_inorder(TreeNode* t)
{
TreeNode* q = t;
while (q->left != NULL)
q = q->left;
do {
printf("%c ", q->data);
q = find_successor(q);
} while (q != NULL);
}
int main()
{
// 스레드 설정
n1.right = &n3; // A -> C
n2.right = &n7; // B -> G
n4.right = &n6; // D -> F
// 중위 순회
thread_inorder(exp);
}
이진 탐색 트리(binary search tree): 이진 트리 기반의 탐색을 위한 자료 구조
탐색(search): 레코드(record)의 집합에서 특정한 레코드를 찾아내는 작업을 의미
레코드(record): 하나 이상의 필드(field)로 구성
테이블(table): 레코드(record)들의 집합
주요 키(primary key): 각각의 레코드를 구별할 수 있는 키
탐색 작업을 할 때 주요 키가 입력이 되어 특정한 키를 가진 레코드를 찾게 됨
이진 탐색 트리는 이러한 탐색 작업을 효율적으로 하기 위한 자료구조
이진 탐색 트리: 이진 탐색 트리의 성질을 만족하는 이진 트리
- 모든 노드 키는 유일함
- 왼쪽 서브 트리의 키들은 루트의 키보다 작음
- 오른쪽 서브 트리의 키들은 루트의 키보다 큼
- 왼쪽과 오른쪽 서브 트리도 이진 탐색 트리임
탐색 연산: 이진 탐색 트리에서 특정 키 값을 가진 노드를 찾는 것
search(x,k)
if x = NULL
then return NULL;
if k = x->key
then return x;
else if k < x->key
then return search(x->left,k);
else return search(x->right,k);
이진 탐색 트리가 공백 상태이면
그냥 복귀
탐색 키가 현재 트리의 루트 키 값과 같으면
루트를 반환
탐색 키가 루트 키 값보다 작으면
왼쪽 서브 트리를 순환 호출을 이용하여 계속 탐색
그렇지 않으면 오른쪽 서브 트리를 순환 호출을 이용하여 계속 탐색
typedef struct TreeNode{
int key;
struct TreeNode *left, *right;
} TreeNode;
// 순환적인 탐색 함수
TreeNode *search(TreeNode *node, int key){
if(node == NULL) return NULL;
if(key == node->key) return node;
else if(key < node->key){
return search(node->left, key); // 왼쪽 자식 노드를 매개변수로 넣어 함수 순환 호출
}else{
return search(node->right, key); // 오른쪽 자식 노드를 매개변수로 넣어 함수 순환 호출
}
}
// 반복적인 탐색 함수
TreeNode *search(TreeNode *node, int key){
while(node != NULL){
if(key == node->data) return node;
else if(key < node->data){
node = node->left;
}else{
node = node->right;
}
}
return NULL; // 탐색에 실패했을 경우 NULL 반환
}
탐색 먼저 수행 -> 이진 탐색 트리에서 같은 키 값을 가진 노드가 없어야 하기 때문 + 탐색에 실패한 위치가 바로 새로운 노드를 삽입하는 위치가 됨
insert_node(T, z)
1. 트리 T에서 z에 대한 탐색을 먼저 수행
2. 탐색이 실패하면 탐색이 띁난 지점에 노드 z를 삽입
insert_node(T, key)
p <- NULL;
t <- T;
while t != NULL do // 탐색을 수행
p <- t;
if key < p->key
then t <- p->left
else t <- p->right
z <- make_node(key);
if p = NULL
then T <- z; // 트리가 비어 있음
else if key < p->key
then p->left <- z
else p->right <- z
p는 부모 노드 포인터
t는 탐색을 위한 포인터
t가 NULL이 될 때까지 탐색을 수행함
현재 탐색 포인터의 값을 부모 노드 포인터에 복사
삽입할 키 값이 t의 키 값보다 작으면 왼쪽 자식 노드로 탐색 진행
삽입할 키 값이 t의 키 값보다 크면 오른쪽 자식 노드로 탐색 진행
key를 포함하는 트리 노드 생성
반복이 끝났고, 만약 부모 노드가 NULL이면 현재 트리가 공백 상태. 따라서, 새로운 노드를 루트로 설정
부모 노드의 키 값과 비교하여 작으면 왼쪽 자식으로 연결함. 그렇지 않으면 오른쪽 자식으로 연결
// key를 이진 탐색 트리 root에 삽입
// key가 이미 root 안에 있으면 삽입되지 않음
void insert_node(TreeNode **root,int key){
TreeNode *p, *t; // p는 부모 노드, t는 현재 노드
TreeNode *n; // n은 새로운 노드
t = *root;
p = NULL;
// 탐색을 먼저 수행
while(t != NULL){
if(key == t->data){
return; // 이미 존재
}
p = t;
if(key < p->key) t = p->left;
else t = p->right;
}
// key가 트리 안에 없으므로 삽입 가능
// 트리 노드 구성
n = (TreeNode *)malloc(sizeof(TreeNode));
if(n == NULL) error("memory allocate error");
// 데이터 복사
n->key= key;
n->left = n->right = NULL;
// 부모 노드와 연결
if( p!= NULL){
if(key < p->key){
p->left = n;
}else{
p->right = n;
}
}else{
*root = n;
}
}
탐색 먼저 수행 -> 삭제하려는 키 값이 트리 안에 어디 있는지를 알아야 삭제할 수 있음
1. 삭제하려는 노드가 단말 노드일 경우
2. 삭제하려는 노드가 하나의 왼쪽이나 오른쪽 서브 트리 중 하나만 가지고 있는 경우
3. 삭제하려는 노드가 두 개의 서브 트리 모두 가지고 있는 경우
// 삭제 함수
// 루트 노드 포인터의 포인터로 전달한 이유는 루트 노드가 삭제되거나 변경될 수 있기 때문
void delete_node(TreeNode **root, int key){
TreeNode *p, *t, *succ, *succ_p; // 부모 노드, 현재 노드, 후계자 노드, 후계자 노드의 부모
TreeNode *child;
// key를 갖는 노드 t를 탐색, p는 t의 부모
p = NULL;
t = *root;
// key를 갖는 노드 t를 탐색
while(t!=NULL && t->key != key){
p = t;
t = (key < p->key) ? p->left : p->right;
}
// 탐색이 종료된 시점에 t가 NULL이면 해당 key가 이진 트이 안에 없는 것을 의미
if(t == NULL){
printf("key is not in the tree\n");
return;
}
// 첫 번째 경우: 단말 노드의 경우
if((t->left == NULL) && (t->right == NULL)){
if(p!=NULL){
// 부모 노드의 자식 필드를 NULL로 만듦
if(p->left == t){
p->left = NULL;
}else{
p->right = NULL;
}
}else{ // 만약 부모 노드가 NULL이면 삭제되는 노드가 루트
*root = NULL;
}
}
// 두 번째 경우: 하나의 자식만 가진 경우
else if((t->left == NULL) || (t->right == NULL)){
child = (t->left != NULL) ? t->left : t->right;
if(p!=NULL){
// 부모와 자식을 연결
if(p->left == t){
p->left = child;
}else{
p->right = child;
}
}else{ // 부모 노드가 NULL이면 삭제되는 노드가 루트
*root = child;
}
}
// 세 번째 경우: 두 개의 자식을 가지는 경우
else{
// 오른쪽 서브 트리에서 후계자를 찾음
succ_p = t;
succ = t->right;
// 후계자를 찾아서 계속 왼쪽으로 이동
while(succ->left != NULL){
succ_p = succ;
succ = succ->left
}
// 후속자의 부모와 자식은 연결
if(succ_p->left == succ){
succ_p->left = succ->right;
}else{
succ_p->right = succ->right;
}
// 후속자가 가진 키 값을 현재 노드에 복사
t->key = succ->key;
// 원래의 후속자 삭제
t = succ;
}
free(t);
}
// 데이터 타입
typedef struct{
char word[MAX_WORD_SIZE]; // 키 필드
char meaning[MAX_MEANING_SIZE];
} element;
// 노드의 구조
typedef struct TreeNode{
element item;
struct TreeNode *left, *right;
}TreeNode;
// 만약 e1 < e2 -> -1 반환
// 만약 e1 = e2 -> 0 반환
// 만약 e1 > e2 -> 1 반환
int compare(element e1, element e2){
return strcmp(e1.word, e2.word);
}
// 이진 탐색 트리를 사용한 영어 사전
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <memory.h>
#define MAX_WORD_SIZE 100
#define MAX_MEANING_SIZE 200
// 데이터 형식
typedef struct{
char word[MAX_WORD_SIZE]; // 키 필드
char meaning[MAX_MEANING_SIZE];
} element;
// 노드의 구조
typedef struct TreeNode{
element key;
struct TreeNode *left, *right;
} TreeNode;
// 만약 e1 < e2 -> -1 반환
// 만약 e1 = e2 -> 0 반환
// 만약 e1 > e2 -> 1 반환
int compare(element e1, element e2){
return strcmp(e1.word, e2.word);
}
// 이진 탐색 트리 출력 함수
void display(TreeNode *p){
if(p!=NULL){
printf("(");
display(p->left);
printf("%s", p->key.word);
display(p->right);
printf(")");
}
}
// 이진 탐색 트리 함수
TreeNode *search(TreeNode *root, element key){
TreeNode* p = root;
while(p!=NULL){
switch (compare(key, p->key))
{
case -1:
p = p->left;
break;
case 0:
return p;
case 1:
p = p->right;
break;
}
}
return p; // 탐색에 실패했을 경우 NULL 반환
}
// key를 이진 탐색 트리 root에 삽입함
// key가 이미 root안에 있는 경우 삽입되지 않음
void insert_node(TreeNode **root, element key){
TreeNode* p, *t; // p는 부모 노드, t는 자식 노드
p = NULL;
t = *root;
// 탐색을 먼저 수행
while(t!=NULL){
p = t;
if(compare(key, t->key) == 0 )
return; // 이미 존재
else if(compare(key,t->key) < 0)
t = t->left;
else
t = t->right;
}
// item이 트리 앖에 없으므로 삽입 가능
TreeNode* n = (TreeNode*)malloc(sizeof(TreeNode)); // n은 새로운 노드
if(n == NULL){
return;
}
// 데이터 복사
n->key = key;
n->left = n->right = NULL;
// 부모 노드와 링크 연결
if(p != NULL){
if(compare(key,p->key) <0){
p->left = n;
}else{
p->right = n;
}
}else{
*root = n;
}
}
void delete_node(TreeNode **root, element key){
TreeNode *p, *t;
p = NULL;
t = *root;
while(t != NULL){
p = t;
t = (compare(key, t->key) < 0) ? t->left : t->right;
}
// 탐색 트리에 없는 키
if(t == NULL){
printf("key is not in the tree\n");
return;
}
// 단말 노드인 경우
if((t->left == NULL) && (t->right == NULL)){
if(p!=NULL){
if(p->left == t){
p->left = NULL;
}else{
p->right = NULL;
}
}else{ // 부모 노드가 없으면 루트
*root = NULL;
}
}
// 하나의 자식만 있는 경우
else if ((t->left == NULL) || (t->right == NULL)) {
TreeNode* child = (t->left != NULL) ? t->left : t->right;
if(p!=NULL){
if (p->left == t) { // 부모와 자식 노드를 연결
p->left = child;
} else {
p->right = child;
}
}else{
*root = child;
}
}
// 두 개의 자식을 가지는 경우
else {
// 오른쪽 서브 트리의 후속자를 찾음
TreeNode *succ, *succ_p;
succ_p = t;
succ = t->right;
// 후속자를 찾아서 계속 왼쪽으로 이동
while (t->left != NULL) {
succ_p = succ;
succ = succ->left;
}
// 후속자의 부모와 자식을 연결
if(succ_p->left == succ){
succ_p->left = succ->right;
}else{
succ_p->right = succ->right;
}
// 후속자를 현재 노드로 이동
t->key = succ->key;
t = succ;
}
free(t);
}
void help(){
printf("*****************\n");
printf("i: input\n");
printf("d: delete\n");
printf("s: search\n");
printf("p: print\n");
printf("q: quit\n");
printf("*****************\n");
}
// 이진 탐색 트리를 사용하는 영어 사전 프로그램
int main(){
char command;
element e;
TreeNode* root = NULL;
TreeNode* tmp;
do{
help();
command = getchar();
fflush(stdin);
switch (command)
{
case 'i':
printf("Word: ");
gets(e.word);
printf("Meaning: ");
gets(e.meaning);
insert_node(&root, e);
break;
case 'd':
printf("Word: ");
gets(e.word);
delete_node(&root, e);
break;
case 's':
printf("Word: ");
gets(e.word);
tmp = search(root, e);
if( tmp != NULL){
printf("Meaning: %s\n", e.meaning);
}
break;
case 'p':
display(root);
printf("\n");
break;
}
} while (command != 'q');
}
<참고자료>
천인국 · 공용해 · 하상호 지음,『C언어로 쉽게 풀어쓴 자료구조』, 생능출판(2016)