C 언어로 N-진 트리 구현하기: 개념부터 코드까지

C 언어에서 N-진 트리(N-ary Tree)를 구현하는 것은 복잡한 자료 구조를 다루는 중요한 프로그래밍 과제입니다. N-진 트리는 한 노드가 최대 N개의 자식을 가질 수 있는 트리 구조로, 계층적인 데이터를 표현하거나 탐색 문제를 해결하는 데 유용합니다. 본 기사에서는 N-진 트리의 기본 개념과 사용 사례를 살펴보고, C 언어로 이를 구현하기 위한 데이터 구조 설계와 코드 작성 방법을 단계별로 안내합니다. 이 과정에서 트리의 순회 방법과 노드 추가 및 삭제 알고리즘도 함께 배워 실전 프로젝트에 적용할 수 있는 기초를 다질 수 있습니다.

목차

N-진 트리란 무엇인가


N-진 트리는 컴퓨터 과학에서 사용되는 트리 자료 구조의 한 종류로, 각 노드가 최대 N개의 자식을 가질 수 있는 트리를 의미합니다.

구조와 특징

  • 노드: 각 노드는 데이터를 저장하며, 최대 N개의 자식 노드에 대한 포인터를 가집니다.
  • 루트 노드: 트리의 최상위 노드로, 트리 구조의 시작점입니다.
  • 자식 노드: 특정 노드에 연결된 하위 노드들입니다.
  • 깊이와 높이: 트리의 깊이(depth)는 루트에서 특정 노드까지의 경로 길이를, 높이(height)는 트리 전체의 최대 깊이를 나타냅니다.

N-진 트리의 예

  • 2진 트리: N=2인 트리로, 각 노드가 최대 두 자식을 가집니다.
  • 3진 트리: N=3인 트리로, 각 노드가 최대 세 자식을 가집니다.
  • N=∞ 트리: 이론적으로 모든 자식 노드를 허용하는 트리입니다.

특징 요약


N-진 트리는 노드의 자식 수가 제한되어 있다는 점에서 배열, 링크드 리스트와는 다른 독특한 데이터 표현 방식을 제공합니다. 계층적 데이터 구조를 효율적으로 표현할 수 있어 다양한 응용 프로그램에서 활용됩니다.

N-진 트리의 주요 용도

1. 파일 시스템 구조


N-진 트리는 파일 시스템에서 디렉토리와 파일의 계층적 구조를 표현하는 데 사용됩니다. 예를 들어, 루트 디렉토리는 자식 디렉토리와 파일들을 트리의 노드로 표현할 수 있습니다.

2. 게임 개발


게임 개발에서는 상태 전이(State Transition)와 같은 논리를 표현하기 위해 N-진 트리가 사용됩니다. 예를 들어, 특정 캐릭터의 행동 트리를 구성하거나 게임의 퀘스트 시스템에서 다양한 시나리오를 관리할 때 활용됩니다.

3. 데이터베이스 및 쿼리 시스템


SQL의 JOIN 연산 또는 XML/JSON 데이터를 처리할 때 N-진 트리를 사용하여 데이터의 계층적 관계를 처리합니다. 데이터 구조의 복잡성을 관리하고 빠른 탐색을 가능하게 합니다.

4. 네트워크 프로토콜 및 라우팅


네트워크 트래픽을 효율적으로 관리하기 위해 N-진 트리를 활용하여 라우팅 테이블을 구성합니다. 각 노드는 네트워크 경로의 정보를 저장하며, 자식 노드는 하위 경로를 나타냅니다.

5. 조직도 및 의사결정 트리


기업의 조직도, 의사결정 트리, 또는 학습 알고리즘에서 계층적 구조를 표현하는 데 N-진 트리가 사용됩니다. 예를 들어, 의사결정 트리는 각 노드가 조건을 평가하고 다음 조건으로 분기하는 구조를 가집니다.

6. 검색 및 탐색 문제 해결


N-진 트리는 탐색 알고리즘(예: DFS, BFS)을 사용하여 특정 데이터를 찾거나 문제를 해결하는 데 유용합니다. 다양한 탐색 방법으로 효율성을 높일 수 있습니다.

특징 요약


N-진 트리는 데이터나 문제를 계층적으로 표현하고 관리하는 데 뛰어난 장점을 가지고 있어, 컴퓨터 과학의 여러 분야에서 중요한 역할을 합니다.

C 언어로 N-진 트리 구현 준비하기

1. 데이터 구조 설계


N-진 트리 구현을 위해 노드 구조를 정의해야 합니다. 각 노드는 데이터를 저장하고, 자식 노드를 가리키는 포인터 배열을 포함합니다.

#include <stdio.h>
#include <stdlib.h>

// 트리 노드 구조 정의
typedef struct TreeNode {
    int data;                  // 노드에 저장할 데이터
    struct TreeNode** children; // 자식 노드 배열
    int childCount;            // 현재 자식 노드 수
} TreeNode;

2. 노드 생성 함수


새로운 노드를 생성하고 초기화하는 함수를 작성합니다.

TreeNode* createNode(int data, int maxChildren) {
    TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
    newNode->data = data;
    newNode->childCount = 0;
    newNode->children = (TreeNode**)malloc(maxChildren * sizeof(TreeNode*));
    return newNode;
}

3. 기본 트리 설정


트리를 구성하기 위해 루트 노드를 생성하고, 필요한 경우 최대 자식 노드 개수를 정의합니다.

int main() {
    int maxChildren = 3; // N-진 트리에서 N의 값
    TreeNode* root = createNode(1, maxChildren); // 루트 노드 생성

    printf("루트 노드가 생성되었습니다. 데이터: %d\n", root->data);

    // 필요 시 루트에 자식 노드를 추가하거나 트리를 확장
    return 0;
}

4. 메모리 관리 고려


동적 메모리를 사용하는 데이터 구조이므로, 프로그램 종료 시 모든 메모리를 적절히 해제해야 합니다.

void freeTree(TreeNode* node, int maxChildren) {
    for (int i = 0; i < node->childCount; i++) {
        freeTree(node->children[i], maxChildren);
    }
    free(node->children);
    free(node);
}

구현 준비 요약


C 언어로 N-진 트리를 구현하기 위해서는 노드 구조 정의, 노드 생성 함수, 그리고 메모리 관리 방법을 사전에 설계해야 합니다. 이를 기반으로 트리의 추가적인 기능(예: 노드 삽입, 삭제, 순회)을 구현할 수 있습니다.

노드 추가 및 삭제 구현

1. 노드 추가 함수


N-진 트리에서 특정 노드에 자식을 추가하기 위한 함수를 작성합니다. 자식 노드의 최대 개수를 초과하지 않도록 제한을 설정해야 합니다.

void addChild(TreeNode* parent, int data, int maxChildren) {
    if (parent->childCount >= maxChildren) {
        printf("자식 노드가 최대치를 초과했습니다. 추가할 수 없습니다.\n");
        return;
    }

    TreeNode* child = createNode(data, maxChildren);
    parent->children[parent->childCount] = child;
    parent->childCount++;
    printf("노드 %d의 자식으로 노드 %d가 추가되었습니다.\n", parent->data, data);
}

2. 노드 삭제 함수


특정 노드를 삭제하려면 해당 노드의 모든 자식을 먼저 삭제한 후, 자신을 삭제해야 합니다.

void deleteNode(TreeNode* parent, int childIndex, int maxChildren) {
    if (childIndex < 0 || childIndex >= parent->childCount) {
        printf("잘못된 인덱스입니다. 삭제할 수 없습니다.\n");
        return;
    }

    TreeNode* nodeToDelete = parent->children[childIndex];
    freeTree(nodeToDelete, maxChildren);

    // 자식 노드 배열을 한 칸씩 당겨 재정렬
    for (int i = childIndex; i < parent->childCount - 1; i++) {
        parent->children[i] = parent->children[i + 1];
    }

    parent->childCount--;
    printf("노드 %d의 자식 노드 %d가 삭제되었습니다.\n", parent->data, childIndex);
}

3. 예제 코드


노드 추가와 삭제를 시뮬레이션하는 코드입니다.

int main() {
    int maxChildren = 3; // 최대 자식 노드 개수
    TreeNode* root = createNode(1, maxChildren);

    // 자식 노드 추가
    addChild(root, 2, maxChildren);
    addChild(root, 3, maxChildren);
    addChild(root, 4, maxChildren);

    // 자식 노드 초과 시도
    addChild(root, 5, maxChildren);

    // 자식 노드 삭제
    deleteNode(root, 1, maxChildren); // 인덱스 1(노드 3) 삭제

    // 메모리 해제
    freeTree(root, maxChildren);
    return 0;
}

4. 실행 결과


코드를 실행하면 다음과 같은 출력이 예상됩니다:

루트 노드가 생성되었습니다. 데이터: 1  
노드 1의 자식으로 노드 2가 추가되었습니다.  
노드 1의 자식으로 노드 3이 추가되었습니다.  
노드 1의 자식으로 노드 4가 추가되었습니다.  
자식 노드가 최대치를 초과했습니다. 추가할 수 없습니다.  
노드 1의 자식 노드 1이 삭제되었습니다.  

추가 및 삭제 구현 요약

  • 노드 추가: 부모 노드에 자식을 추가하며, 자식 노드의 개수를 확인하여 제한합니다.
  • 노드 삭제: 자식 노드를 재귀적으로 삭제하고 배열을 재정렬합니다.
    이 두 함수를 통해 N-진 트리의 동적 구조를 관리할 수 있습니다.

트리 순회 알고리즘 구현

1. 트리 순회란?


트리 순회는 트리의 모든 노드를 특정 순서에 따라 방문하는 알고리즘입니다. N-진 트리에서도 이 원칙은 동일하게 적용됩니다. 대표적인 순회 방식은 다음과 같습니다:

  • 전위 순회(Preorder): 부모 노드를 방문한 후 자식 노드를 순서대로 방문.
  • 후위 순회(Postorder): 모든 자식 노드를 방문한 후 부모 노드를 방문.
  • 레벨 순회(Level-order): 트리의 깊이에 따라 노드를 순서대로 방문.

2. 전위 순회 구현


전위 순회에서는 현재 노드를 방문한 후 모든 자식 노드를 재귀적으로 방문합니다.

void preorderTraversal(TreeNode* node) {
    if (node == NULL) return;

    // 현재 노드 처리
    printf("%d ", node->data);

    // 모든 자식 노드를 재귀적으로 방문
    for (int i = 0; i < node->childCount; i++) {
        preorderTraversal(node->children[i]);
    }
}

3. 후위 순회 구현


후위 순회에서는 자식 노드를 모두 방문한 후 현재 노드를 처리합니다.

void postorderTraversal(TreeNode* node) {
    if (node == NULL) return;

    // 모든 자식 노드를 재귀적으로 방문
    for (int i = 0; i < node->childCount; i++) {
        postorderTraversal(node->children[i]);
    }

    // 현재 노드 처리
    printf("%d ", node->data);
}

4. 레벨 순회 구현


레벨 순회에서는 큐를 사용해 트리의 각 레벨을 차례대로 탐색합니다.

#include <queue>

void levelOrderTraversal(TreeNode* root) {
    if (root == NULL) return;

    // 큐 초기화
    std::queue<TreeNode*> q;
    q.push(root);

    while (!q.empty()) {
        TreeNode* current = q.front();
        q.pop();

        // 현재 노드 처리
        printf("%d ", current->data);

        // 자식 노드 큐에 추가
        for (int i = 0; i < current->childCount; i++) {
            q.push(current->children[i]);
        }
    }
}

5. 예제 코드


각 순회 방식의 예제를 실행합니다.

int main() {
    int maxChildren = 3;
    TreeNode* root = createNode(1, maxChildren);

    addChild(root, 2, maxChildren);
    addChild(root, 3, maxChildren);
    addChild(root, 4, maxChildren);

    addChild(root->children[0], 5, maxChildren);
    addChild(root->children[0], 6, maxChildren);

    printf("전위 순회: ");
    preorderTraversal(root);
    printf("\n");

    printf("후위 순회: ");
    postorderTraversal(root);
    printf("\n");

    printf("레벨 순회: ");
    levelOrderTraversal(root);
    printf("\n");

    freeTree(root, maxChildren);
    return 0;
}

6. 실행 결과


위 코드 실행 시 다음과 같은 결과가 나옵니다:

전위 순회: 1 2 5 6 3 4  
후위 순회: 5 6 2 3 4 1  
레벨 순회: 1 2 3 4 5 6  

트리 순회 구현 요약

  • 전위 순회: 부모 -> 자식 순서.
  • 후위 순회: 자식 -> 부모 순서.
  • 레벨 순회: 깊이 순서대로 노드 방문.
    트리 순회 알고리즘은 트리의 구조와 데이터를 탐색하는 기본 도구로, 다양한 문제 해결에 활용됩니다.

구현 코드와 동작 예시

1. 전체 구현 코드


N-진 트리를 정의하고 노드 추가, 삭제, 순회 기능을 포함한 코드입니다.

#include <stdio.h>
#include <stdlib.h>
#include <queue>

// 트리 노드 정의
typedef struct TreeNode {
    int data;
    struct TreeNode** children;
    int childCount;
} TreeNode;

// 노드 생성 함수
TreeNode* createNode(int data, int maxChildren) {
    TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
    newNode->data = data;
    newNode->childCount = 0;
    newNode->children = (TreeNode**)malloc(maxChildren * sizeof(TreeNode*));
    return newNode;
}

// 노드 추가 함수
void addChild(TreeNode* parent, int data, int maxChildren) {
    if (parent->childCount >= maxChildren) {
        printf("자식 노드가 최대치를 초과했습니다. 추가할 수 없습니다.\n");
        return;
    }
    TreeNode* child = createNode(data, maxChildren);
    parent->children[parent->childCount] = child;
    parent->childCount++;
    printf("노드 %d의 자식으로 노드 %d가 추가되었습니다.\n", parent->data, data);
}

// 노드 삭제 함수
void deleteNode(TreeNode* parent, int childIndex, int maxChildren) {
    if (childIndex < 0 || childIndex >= parent->childCount) {
        printf("잘못된 인덱스입니다. 삭제할 수 없습니다.\n");
        return;
    }
    TreeNode* nodeToDelete = parent->children[childIndex];
    for (int i = 0; i < nodeToDelete->childCount; i++) {
        deleteNode(nodeToDelete, i, maxChildren);
    }
    free(nodeToDelete->children);
    free(nodeToDelete);
    for (int i = childIndex; i < parent->childCount - 1; i++) {
        parent->children[i] = parent->children[i + 1];
    }
    parent->childCount--;
    printf("노드 %d의 자식 노드 %d가 삭제되었습니다.\n", parent->data, childIndex);
}

// 전위 순회 함수
void preorderTraversal(TreeNode* node) {
    if (node == NULL) return;
    printf("%d ", node->data);
    for (int i = 0; i < node->childCount; i++) {
        preorderTraversal(node->children[i]);
    }
}

// 후위 순회 함수
void postorderTraversal(TreeNode* node) {
    if (node == NULL) return;
    for (int i = 0; i < node->childCount; i++) {
        postorderTraversal(node->children[i]);
    }
    printf("%d ", node->data);
}

// 레벨 순회 함수
void levelOrderTraversal(TreeNode* root) {
    if (root == NULL) return;
    std::queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        TreeNode* current = q.front();
        q.pop();
        printf("%d ", current->data);
        for (int i = 0; i < current->childCount; i++) {
            q.push(current->children[i]);
        }
    }
}

// 메모리 해제 함수
void freeTree(TreeNode* node, int maxChildren) {
    for (int i = 0; i < node->childCount; i++) {
        freeTree(node->children[i], maxChildren);
    }
    free(node->children);
    free(node);
}

// 메인 함수
int main() {
    int maxChildren = 3; // 최대 자식 노드 개수
    TreeNode* root = createNode(1, maxChildren);

    addChild(root, 2, maxChildren);
    addChild(root, 3, maxChildren);
    addChild(root, 4, maxChildren);

    addChild(root->children[0], 5, maxChildren);
    addChild(root->children[0], 6, maxChildren);

    printf("전위 순회: ");
    preorderTraversal(root);
    printf("\n");

    printf("후위 순회: ");
    postorderTraversal(root);
    printf("\n");

    printf("레벨 순회: ");
    levelOrderTraversal(root);
    printf("\n");

    freeTree(root, maxChildren);
    return 0;
}

2. 실행 결과


코드 실행 시 예상 출력:

노드 1의 자식으로 노드 2가 추가되었습니다.
노드 1의 자식으로 노드 3이 추가되었습니다.
노드 1의 자식으로 노드 4가 추가되었습니다.
노드 2의 자식으로 노드 5가 추가되었습니다.
노드 2의 자식으로 노드 6가 추가되었습니다.
전위 순회: 1 2 5 6 3 4 
후위 순회: 5 6 2 3 4 1 
레벨 순회: 1 2 3 4 5 6 

3. 구현 요약

  • 전위, 후위, 레벨 순회 방식으로 데이터를 탐색합니다.
  • 노드 추가와 삭제를 동적으로 처리하며, 트리의 구조를 유지합니다.
  • 메모리 관리를 포함해 전체 구현을 통합하여 실행 가능한 N-진 트리 코드를 제공합니다.

요약


C 언어로 N-진 트리를 구현하는 방법을 기본 개념부터 구현 코드와 응용 사례까지 자세히 살펴보았습니다. 트리 노드의 구조 설계, 노드 추가 및 삭제 함수 작성, 전위/후위/레벨 순회 알고리즘 구현을 통해 계층적 데이터를 효율적으로 처리할 수 있음을 확인했습니다. 또한, 동적 메모리 관리를 통해 프로그램 안정성을 보장하는 방법도 다뤘습니다. 본 기사를 바탕으로 N-진 트리를 활용한 다양한 프로젝트에서 구조적 데이터를 효과적으로 처리할 수 있을 것입니다.

목차