C++을 이용한 이진 트리 알고리즘 : 탐색과 정렬에 대한 실질적 이해

1. C++과 이진 트리 알고리즘 소개

이진 트리(Binary Tree)는 데이터의 저장과 탐색을 빠르고 효율적으로 할 수 있게 해주는 유용한 자료구조입니다. 이진 트리는 각 노드가 두 개의 서브트리를 가지는 특히 효율적인 트리 구조이며, 데이터를 구조적으로 저장함으로써 검색, 삽입, 삭제 등이 빠르게 이루어집니다.

이제 C++를 이용하여 이진 트리를 구현해보도록 하겠습니다. 먼저 노드(Node)라는 구조체를 정의하는 것으로 시작합니다. 이진 트리에서 각 노드는 데이터 필드와 두 개의 자식 노드에 대한 링크(왼쪽 및 오른쪽)가 있습니다.


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

위의 코드는 C++에서 노드를 표현하는 방법을 보여줍니다. struct 키워드를 사용하여 노드를 정의하였으며, 이 노드는 정수형 데이터를 저장하고, 왼쪽과 오른쪽 자식 노드에 대한 포인터를 가지고 있습니다.

이진 트리는 이러한 노드들이 서로 연결된 형태로 구성됩니다. 어떤 노드는 루트 노드가 될 수도 있고, 다른 노드의 왼쪽 또는 오른쪽 자식 노드가 될 수도 있습니다. 이러한 구조는 데이터의 상대적 위치를 저장하는 데 효과적이며, 이를 바탕으로 다양한 알고리즘을 구현할 수 있습니다.


2. 이진 트리의 기본 개념

2.1 정의와 특징

이진 트리는 노드가 최대 두 개의 자식을 가질 수 있는 트리 자료구조입니다. 이 때 자식 노드는 왼쪽과 오른쪽, 두 방향을 가지고 있습니다. 이진 트리에서 노드의 레벨은 루트에서 해당 노드에 이르는 간선의 수를 의미하며, 루트 노드의 레벨은 0입니다.

이진 트리의 몇 가지 중요한 특징은 다음과 같습니다:

  • 각 레벨의 최대 노드 수는 2^n개입니다(n은 레벨 번호).
  • 높이가 h인 이진 트리가 가질 수 있는 최대 노드 수는 2^(h+1) – 1개입니다.
  • 높이가 h인 이진 트리가 가질 수 있는 최소 노드 수는 h + 1개입니다.

2.2 이진 트리의 유형

이진 트리는 트리의 불균형에 따라 여러 유형으로 나눌 수 있습니다:

  • 완전 이진 트리(Complete Binary Tree): 모든 레벨이 완전히 채워져 있고, 마지막 레벨은 왼쪽부터 차례대로 채워져 있는 이진 트리를 말합니다.
  • 완전 이진 트리(Full Binary Tree): 모든 노드가 0개 또는 2개의 자식 노드를 가지는 이진 트리를 말합니다.
  • 균형 이진 트리(Balanced Binary Tree): 모든 노드의 왼쪽 서브 트리와 오른쪽 서브 트리의 높이 차이가 최대 1인 이진 트리를 말합니다.
  • 편향 이진 트리(Skewed Binary Tree): 각각의 노드가 하나의 자식 노드만을 가지는 이진 트리로, 왼쪽 편향 이진 트리와 오른쪽 편향 이진 트리로 나뉩니다.

3. C++을 이용한 이진 트리 구현

3.1 노드 생성

이진 트리를 구현하기 위한 첫 걸음은 노드를 생성하는 것입니다. 각 노드는 데이터를 저장할 공간과 왼쪽 자식 노드 및 오른쪽 자식 노드에 대한 포인터를 가지고 있어야 합니다. 이를 C++에서 구현하는 방식은 다음과 같습니다:


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

Node* createNode(int data) {
    Node* newNode = new Node();
    if (!newNode) {
        cout << "Memory error\n";
        return NULL;
    }
    newNode->data = data;
    newNode->left = newNode->right = NULL;
    return newNode;
}

여기서, createNode라는 함수는 데이터를 파라미터로 받아서 새로운 노드를 생성하고 노드의 data 필드에 그 값을 저장합니다. 그리고 이 노드의 왼쪽 및 오른쪽 포인터는 아직 아무 노드도 가리키지 않으므로 NULL로 설정됩니다.

3.2 트리 생성

노드를 생성하는 함수를 갖추었다면, 이제 이 노드들을 이용해서 이진 트리를 생성할 수 있습니다.
이진 트리는 노드들이 특정한 방식으로 연결되어 있는 구조이므로, 이진 트리를 생성한다는 것은 이러한 연결 구조를 구현하는 것을 의미합니다. 루트 노드부터 시작해서 서브트리를 생성하고, 이를 재귀적으로 반복하여 트리를 완성할 수 있습니다.


Node* insertNode(Node* root, int data) {
    // If the tree is empty, assign the new node address to root
    if (root == NULL) {
        root = createNode(data);
        return root;
    }
   
    // Else, do level order traversal until we find an empty
    // place, i.e. either left child or right child of some
    // node is pointing to NULL.
    queue q;
    q.push(root);
   
    while (!q.empty()) {
        Node* temp = q.front();
        q.pop();
   
        if (temp->left != NULL)
            q.push(temp->left);
        else {
            temp->left = createNode(data);
            return root;
        }
   
        if (temp->right != NULL)
            q.push(temp->right);
        else {
            temp->right = createNode(data);
            return root;
        }
    }
    return root;
}

위의 코드는 데이터를 이진 트리에 삽입하는 함수를 보여줍니다. 여기서는 레벨 순회(level order traversal) 방식을 사용하여 노드를 삽입합니다.


4. 탐색 알고리즘의 개념과 종류

탐색 알고리즘은 특정 조건에 해당하는 요소를 찾기 위한 알고리즘입니다. 이진 트리에서는 주로 깊이 우선 탐색(depth-first-search)과 너비 우선 탐색(breadth-first-search) 알고리즘이 사용됩니다.

4.1 깊이 우선 탐색(DFS)

깊이 우선 탐색은 트리의 루트 노드에서 시작하여 가장 멀리 있는 노드를 우선적으로 탐색하며, 더 이상 방문할 노드가 없으면 바로 이전 노드로 돌아가는 방식으로 탐색합니다. 이 방식을 사용하여 전위 순회(pre-order), 중위 순회(in-order), 후위 순회(post-order)를 하게 됩니다.


// 전위 순회
void preOrder(Node* temp) {
    if (temp == NULL)
        return;
 
    cout << temp->data << " ";
    preOrder(temp->left);
    preOrder(temp->right);
}

// 중위 순회
void inOrder(Node* temp) {
    if (temp == NULL)
        return;
 
    inOrder(temp->left);
    cout << temp->data << " ";
    inOrder(temp->right);
}

// 후위 순회
void postOrder(Node* temp) {
    if (temp == NULL)
        return;
 
    postOrder(temp->left);
    postOrder(temp->right);
    cout << temp->data << " ";
}

위의 코드는 이진 트리의 전위, 중위, 후위 순회를 보여줍니다.

4.2 너비 우선 탐색(BFS)

너비 우선 탐색(BFS)는 루트 노드에서 시작하여 가장 가까운 노드부터 차례대로 탐색하는 방법입니다. 이 방법은 주로 큐를 사용하여 구현하게 됩니다.


void levelOrder(Node* temp) {
    if (temp == NULL)
        return;
 
    queue q;
    q.push(temp);
 
    while (!q.empty()) {
        Node* node = q.front();
        cout << node->data << " ";
        q.pop();
        if (node->left != NULL)
            q.push(node->left);
        if (node->right != NULL)
            q.push(node->right);
    }
}

위 코드는 이진 트리의 너비 우선 탐색(BFS), 레벨 순회 예를 보여줍니다. 이진 트리의 각 레벨을 차례대로 출력하는 것을 볼 수 있습니다.


5. C++를 이용한 이진 트리 탐색 알고리즘 구현

이진 트리 탐색 알고리즘은 트리의 노드들을 일정한 순서에 따라 방문하는 알고리즘입니다. 가장 기본적인 이진 트리 탐색 알고리즘은 전위(pre-order), 중위(in-order), 후위(post-order) 탐색입니다.

5.1 전위(pre-order) 탐색

전위 탐색은 현재 노드, 왼쪽 하위 트리, 오른쪽 하위 트리 순서로 탐색하는 방식입니다. 따라서 루트 노드가 제일 먼저 방문되게 됩니다.


void preOrder(Node* temp) {
    if (temp == NULL)
        return;
 
    cout << temp->data << " ";
    preOrder(temp->left);
    preOrder(temp->right);
}

5.2 중위(in-order) 탐색

중위 탐색은 왼쪽 하위 트리, 현재 노드, 오른쪽 하위 트리 순서로 탐색하는 방식입니다. 이러한 방식은 이진 탐색 트리에서 중요한 역할을 하는데, 왜냐하면 중위 탐색을 수행하면 노드들의 값이 오름차순으로 방문되기 때문입니다.


void inOrder(Node* temp) {
    if (temp == NULL)
        return;
 
    inOrder(temp->left);
    cout << temp->data << " ";
    inOrder(temp->right);
}

5.3 후위(post-order) 탐색

후위 탐색은 왼쪽 하위 트리, 오른쪽 하위 트리, 현재 노드 순서로 탐색하는 방식입니다. 트리의 모든 노드를 한 번씩 방문하며, 리프 노드부터 루트 노드 순으로 방문합니다.


void postOrder(Node* temp) {
    if (temp == NULL)
        return;
 
    postOrder(temp->left);
    postOrder(temp->right);
    cout << temp->data << " ";
}

위의 코드에서, 'temp' 파라미터는 각 재귀 호출에서 탐색을 시작할 노드를 나타냅니다. 재귀적으로 왼쪽과 오른쪽 하위 트리를 방문하며, 이들 중 하나라도 NULL일 때에는 탐색을 멈춥니다.


6. 정렐 알고리즘의 개념과 종류

정렬 알고리즘은 데이터를 정해진 순서에 따라 재배열하는 알고리즘입니다. 정렬 알고리즘들은 각각 다른 특징과 성능을 가지고 있습니다.

6.1 버블 정렬(Bubble Sort)

버블 정렬은 서로 인접한 두 원소를 비교하며 정렬하는 알고리즘입니다.


void bubbleSort(int arr[], int n) {
    for(int i = 0; i < n-1; i++) {
        for(int j = 0; j < n-i-1; j++) {
            if(arr[j] > arr[j+1]) {
                swap(&arr[j], &arr[j+1]);
            }
        }
    }
}

6.2 선택 정렬(Selection Sort)

선택 정렬은 가장 작은 원소를 선택하여 앞으로 이동시키는 알고리즘입니다.


void selectionSort(int arr[], int n) {
    for(int i = 0; i < n-1; i++) {
        int minIndex = i;
        for(int j = i+1; j < n; j++) {
            if(arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        swap(&arr[minIndex], &arr[i]);
    }
}

6.3 삽입 정렬(Insertion Sort)

삽입 정렬은 정렬된 부분 배열에 현재 원소를 적절한 위치에 삽입하는 방법으로 정렬하는 알고리즘입니다.


void insertionSort(int arr[], int n) {
    for(int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while(j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key;
    }
}

위의 코드에서 각 함수는 배열과 그 길이를 인수로 받고 있습니다. 배열은 'arr'를 나타내며, 길이는 'n'을 나타냅니다. 매 반복마다, 필요한 만큼 특정 값을 배열에서 옮깁니다.


7. C++을 이용한 이진 트리 정렬 알고리즘 구현

이진 트리 탐색을 통하여 이진 트리 정렬은 수행됩니다. 데이터를 정렬하는 메소드는 중위 탐색인데, 이는 이진 탐색 트리에서 중위 탐색을 수행하면 노드들의 값이 오름차순으로 방문되기 때문입니다.

7.1 이진 트리 정렬 과정

이진 트리 정렬은 다음과 같은 과정을 거칩니다.

1. 새로운 노드를 생성합니다.
2. 생성된 노드를 현재 노드로 설정합니다.
3. 만약 현재 노드의 값이 새로운 노드의 값보다 크면 새로운 노드는 현재 노드의 왼쪽 자식이 됩니다.
4. 만약 현재 노드의 값이 새로운 노드의 값보다 작거나 같으면 새로운 노드는 현재 노드의 오른쪽 자식이 됩니다.
5. 왼쪽 또는 오른쪽 노드로 이동하려면 해당 노드가 비어 있지 않은지 확인해야 합니다. 그렇지 않은 경우 이진 트리는 정렬됩니다.

7.2 이진 트리 정렬 코드 실습


#include
using namespace std;

struct Node {
    int key;
    Node* left, *right;
};

// 노드를 생성하는 함수
Node* newNode(int item) {
    Node* temp = new Node;
    temp->key = item;
    temp->left = temp->right = NULL;
    return temp;
};

// 중위 탐색 구현
void inorder(Node* root) {
    if (root != NULL) {
        inorder(root->left);
        cout << root->key << " ";
        inorder(root->right);
    }
}

// 새 노드를 삽입하는 함수
Node* insert(Node* node, int key) {
    // 트리가 비어 있으면 새 노드를 루트 노드로 만듭니다.
    if (node == NULL) return newNode(key);

    // 그렇지 않으면 재귀적으로 삽입됩니다. 
    if (key < node->key)
        node->left  = insert(node->left, key);
    else
        node->right = insert(node->right, key);

    // 새로운 부모 노드 포인터를 반환합니다.
    return node;
}

int main() {
    Node *root = NULL;
    root = insert(root, 50);
    insert(root, 30);
    insert(root, 20);
    insert(root, 40);
    insert(root, 70);
    insert(root, 60);
    insert(root, 80);

    // print inoder traversal of the BST
    inorder(root);

    return 0;
}

위 코드에서 실행을 마치면 기대 출력값은 '20 30 40 50 60 70 80'이 됩니다. 이는 중위 탐색의 결과이며 중위 탐색은 이진 탐색 트리에서 노드들의 값을 오름차순으로 방문하기 때문입니다.


8. 이진 트리 알고리즘의 활용 사례

이진 트리는 이진 검색, 정렬, 빠른 검색 등 다양한 알고리즘에서 사용됩니다. 이번 섹션에서는 이러한 사용 사례 중 하나인 이진 검색 알고리즘의 사용 예를 살펴보겠습니다.

8.1 이진 탐색(Binary Search)

이진 탐색 알고리즘은 오름차순으로 정렬된 배열에서 효율적으로 원하는 값을 찾는 기법입니다. 이 알고리즘은 재귀적으로 중간 값과 찾고자 하는 값을 비교하여 효율적으로 검색합니다. 이진 트리를 사용하면 검색 시간이 상당히 단축됩니다.


#include 
using namespace std;

int binarySearch(int arr[], int l, int r, int x) {
    if (r >= l) {
        int mid = l + (r - l) / 2;

        // 중간값이 찾는 값일 경우
        if (arr[mid] == x)
            return mid;

        // 중간값이 찾는 값보다 큰 경우, 왼쪽 부분 탐색
        if (arr[mid] > x)
            return binarySearch(arr, l, mid - 1, x);

        // 중간값이 찾는 값보다 작은 경우, 오른쪽 부분 탐색
        return binarySearch(arr, mid + 1, r, x);
    }

    // 원하는 값이 배열에 없는 경우
    return -1;
}

int main(void) {
    int arr[] = {2, 3, 4, 10, 40};
    int n = sizeof(arr) / sizeof(arr[0]);
    int x = 10;
    int result = binarySearch(arr, 0, n-1, x);
    if(result == -1) cout << "Element is not present in array";
    else cout << "Element found at index " << result;
    return 0;
} 

위 코드를 실행하면 "Element found at index 3"이라는 결과를 출력합니다. 이것은 이진 탐색 알고리즘이 원하는 값을 찾아 인덱스를 반환하는 것을 보여줍니다.


9. 이진 트리 알고리즘의 장단점

9.1 장점

  • 데이터를 개체의 계층적 관계를 표현하는 데 이용할 수 있습니다.
  • 이진 탐색 트리를 사용하면 데이터를 빠르게 검색하거나 소팅할 수 있습니다.
  • 이진 트리를 이용하면 쉽게 새로운 데이터를 트리에 삽입하거나 기존 데이터를 제거할 수 있습니다.
  • 트리는 탐색, 삽입, 삭제 연산을 로그 시간 복잡도로 처리할 수 있는 가장 좋은 방법 중 하나입니다.

9.2 단점

  • 이진 트리를 구현하고 유지 관리하는 것은 복잡할 수 있으며, 알고리즘은 특별한 주의를 요구합니다.
  • 트리가 균형을 잃어버릴 경우(한쪽으로 치우친 경우) 탐색/삽입/삭제의 성능은 비효율적으로 저하됩니다.
  • 데이터의 삽입과 삭제에 따라 트리를 재조정해야 하는 경우가 있습니다.

9.3 활용 코드 예시

이진 트리 알고리즘의 장단점을 이해하는 데 도움이 되는 코드 예제를 제공합니다. 이진 검색 트리를 사용하는 코드를 다시 살펴봅니다.


#include 
using namespace std;

struct Node {
    int key;
    struct Node *left, *right;
};

// 새로운 노드 생성
struct Node *newNode(int item) {
    struct Node *temp = new Node;
    temp->key = item;
    temp->left = temp->right = NULL;
    return temp;
}

// 중위 탐색 (Inorder)
void inorder(struct Node *root) {
    if (root != NULL) {
        inorder(root->left);
        cout << root->key << " ";
        inorder(root->right);
    }
}

// 새 노드 삽입
struct Node* insert(struct Node* node, int key) {
    if (node == NULL) return newNode(key);
    if (key < node->key)
        node->left = insert(node->left, key);
    else if (key > node->key)
        node->right = insert(node->right, key);
    return node;
}

int main() {
    /* Let us create following BST
                50
             /     \
            30      70
           / \    /  \
         20  40  60   80
    */
    struct Node *root = NULL;
    root = insert(root, 50);
    insert(root, 30);
    insert(root, 20);
    insert(root, 40);
    insert(root, 70);
    insert(root, 60);
    insert(root, 80);

    // print inoder traversal of the BST
    inorder(root);
    return 0;
}

위 코드는 이진 탐색 트리를 구현하고 중위 순회를 통해 정렬된 순서로 데이터를 출력하는 알고리즘입니다. 코드를 실행하면 "20 30 40 50 60 70 80"이 출력됩니다.


10. 마무리 및 후기

이번 포스팅에서는 이진 트리 알고리즘의 기본 개념, 작동 방식, 이진 트리의 활용 예시 및 장단점에 대해 배웠습니다. 근본적으로 이진 트리는 우리가 데이터를 계층적이고 효율적인 방식으로 조직하고 검색할 수 있는 데이터 구조를 제공합니다.

10.1 이진 트리의 중요성

이진 트리는 컴퓨터 과학의 핵심 구성 요소 중 하나입니다. 효율적인 검색, 삽입 및 삭제를 가능하게 하며, 또한 이해하고 사용하기가 상대적으로 쉽습니다. 따라서 프로그래머로서 이진 트리 알고리즘에 대한 충분한 이해는 필수적입니다.

10.2 코드에 대한 후기

이번에 제시한 코드는 모두 C++로 작성되었습니다. 간결하면서도 이진 트리 알고리즘의 기본 개념과 이해를 돕기 위한 것이었습니다. 이러한 예제 코드를 통해 이진 트리의 동작 방식을 직접 보고 이해하는 것은 매우 중요합니다.


// 코드 예시
// 이진 트리 노드
class Node {
    int key;
    Node *left, *right;

public:
    Node(int item) {
        key = item;
        left = right = nullptr;
    }
};

위의 간단한 C++ 코드는 이진 트리를 생성하는 기본적인 예를 보여줍니다. 'Node' 클래스는 키 값(key) 그리고 왼쪽과 오른쪽 자식 노드에 대한 포인터를 갖습니다.

10.3 마치면서

이진 트리에 대해서 더 깊게 배우고 싶다면 다양한 트리 순회 방법(전위 순회, 중위 순회, 후위 순회 등)을 조사해보거나, 이진 탐색 트리, AVL 트리 등과 같은 다양한 이진 트리의 변형에 대해 알아보는 것도 추천합니다.


Leave a Comment