C++ 코딩테스트 강좌, 트리 알아보기

작성자: 블로그 작성자 | 날짜: 2023년 10월 1일


1. 문제 설명

이번 강좌에서는 C++ 코딩테스트에서 자주 등장하는 트리(Tree) 자료구조에 대해 알아보고, 이를 활용한 알고리즘 문제를 풀어보도록 하겠습니다.

주어진 문제는 다음과 같습니다:

문제: 이진 탐색 트리의 최솟값과 최댓값 찾기

문제 설명: 이진 탐색 트리(Binary Search Tree, BST)가 주어질 때, 그 트리의 최솟값과 최댓값을 찾아 출력하는 프로그램을 작성하세요.

이진 탐색 트리는 다음과 같은 성질을 가집니다:

  • 왼쪽 서브트리의 모든 노드는 현재 노드보다 작다.
  • 오른쪽 서브트리의 모든 노드는 현재 노드보다 크다.

입력으로 주어지는 트리는 다음과 같은 형식으로 주어집니다:

            5
           / \
          3   7
         / \   \
        1   4   8
            

출력으로는 최솟값과 최댓값을 차례로 출력해야 하며, 위 예시의 경우 출력은 다음과 같습니다:

            1
            8
            

이와 같은 문제를 해결하기 위해 이진 탐색 트리에 대한 이해가 필요합니다.

2. 이진 탐색 트리(BST)의 통찰

이진 탐색 트리(BST)는 각 노드가 데이터를 가지고 있으며, 자식 노드는 두 개(왼쪽 및 오른쪽)입니다. 왼쪽 자식 노드는 항상 그 부모 노드보다 작고, 오른쪽 자식 노드는 항상 그 부모 노드보다 큽니다.

이런 성질 덕분에 이진 탐색 트리를 사용하면 데이터의 검색, 삽입, 삭제를 O(log n) 시간 안에 수행할 수 있습니다.

  • 최솟값은 왼쪽 서브트리의 가장 왼쪽에 있는 노드입니다.
  • 최댓값은 오른쪽 서브트리의 가장 오른쪽에 있는 노드입니다.

트리의 깊이가 n인 경우 최솟값과 최댓값을 찾기 위해 단순히 트리를 탐색하면 되므로, 매우 효율적입니다.

3. 알고리즘 설계

최솟값과 최댓값을 찾기 위해 두 개의 함수를 정의할 수 있습니다. 각각의 함수는 재귀적 방법 또는 반복적 방법을 사용하여 트리를 탐색합니다.

            // 이진 탐색 트리의 노드 구조 정의
            struct Node {
                int data;
                Node* left;
                Node* right;
                
                Node(int val) : data(val), left(nullptr), right(nullptr) {}
            };

            // 최솟값을 찾는 함수
            Node* findMin(Node* root) {
                if (root == nullptr) return nullptr;
                if (root->left == nullptr) return root;
                return findMin(root->left);
            }

            // 최댓값을 찾는 함수
            Node* findMax(Node* root) {
                if (root == nullptr) return nullptr;
                if (root->right == nullptr) return root;
                return findMax(root->right);
            }
            

각 함수는 주어진 노드에서 시작하여 왼쪽 혹은 오른쪽 방향으로 계속 이동하면서 최솟값 또는 최댓값을 찾아내는 길을 제공합니다.

4. C++ 코드 구현

이진 탐색 트리에서 최솟값과 최댓값을 구하는 전체 코드는 다음과 같습니다:

            #include 
            using namespace std;

            // 이진 탐색 트리의 노드 구조 정의
            struct Node {
                int data;
                Node* left;
                Node* right;

                Node(int val) : data(val), left(nullptr), right(nullptr) {}
            };

            // 최솟값을 찾는 함수
            Node* findMin(Node* root) {
                if (root == nullptr) return nullptr;
                if (root->left == nullptr) return root;
                return findMin(root->left);
            }

            // 최댓값을 찾는 함수
            Node* findMax(Node* root) {
                if (root == nullptr) return nullptr;
                if (root->right == nullptr) return root;
                return findMax(root->right);
            }

            int main() {
                // 샘플 트리 생성
                Node* root = new Node(5);
                root->left = new Node(3);
                root->right = new Node(7);
                root->left->left = new Node(1);
                root->left->right = new Node(4);
                root->right->right = new Node(8);
                
                Node* minNode = findMin(root);
                Node* maxNode = findMax(root);

                if (minNode) cout << "최솟값: " << minNode->data << endl;
                if (maxNode) cout << "최댓값: " << maxNode->data << endl;

                // 동적 메모리 해제
                delete root->left->left;
                delete root->left->right;
                delete root->left;
                delete root->right->right;
                delete root->right;
                delete root;

                return 0;
            }
            

위 코드를 실행시키면 해당 이진 탐색 트리의 최솟값과 최댓값이 출력됩니다.

5. 코드 설명

프로그램의 주요 구성 요소에 대해 설명하겠습니다:

  • Node 구조체: 이 구조체는 이진 탐색 트리를 구성하는 각 노드를 정의합니다. 각 노드는 정수형 데이터와 두 개의 자식 포인터(왼쪽 및 오른쪽)를 가집니다.
  • findMin 함수: 주어진 노드에서 왼쪽 자식 노드를 계속 따라가면서 최솟값 노드를 찾습니다. 왼쪽 자식이 더 이상 없으면 그 노드가 최솟값이 됩니다.
  • findMax 함수: 주어진 노드에서 오른쪽 자식 노드를 계속 따라가면서 최댓값 노드를 찾습니다. 오른쪽 자식이 더 이상 없으면 그 노드가 최댓값이 됩니다.
  • main 함수: 샘플 트리를 생성하고, 최솟값과 최댓값을 찾기 위해 각각의 함수를 호출합니다. 결과를 출력한 뒤, 동적으로 할당된 메모리를 해제합니다.

6. 결론

이번 강좌에서는 C++의 이진 탐색 트리를 이용하여 최솟값과 최댓값을 찾는 알고리즘을 살펴보았습니다. 이진 탐색 트리는 매우 효율적인 데이터 구조로, 다양한 응용이 가능합니다. 트리 구조에 대한 더 깊은 이해를 통해 다른 트리 관련 문제를 해결하는 데 도움이 될 것입니다.

앞으로 보다 복잡한 트리 문제나, 트리를 응용한 알고리즘을 다루는 강좌도 준비하겠습니다. 다음 강좌를 기대해 주세요!

블로그를 방문해 주셔서 감사합니다. 질문이나 추가적인 논의가 필요하다면 댓글을 남겨주세요.


© 2023 블로그 작성자. 모든 권리 보유.

C++ 코딩테스트 강좌, 트리 순회하기

문제 설명

이 문제는 이진 트리를 구성하는 노드의 값을 특정 순서대로 출력하는 트리 순회 알고리즘을 구현하는 것입니다. 이진 트리는 각 노드가 최대 두 개의 자식 노드를 가질 수 있는 자료구조로, 다양한 방식으로 노드를 방문할 수 있습니다. 가장 일반적인 트리 순회 방식은 전위 순회, 중위 순회, 후위 순회입니다.

트리의 정의

    struct TreeNode {
        int val; // 노드의 값
        TreeNode *left; // 왼쪽 자식 노드
        TreeNode *right; // 오른쪽 자식 노드
        TreeNode(int x) : val(x), left(NULL), right(NULL) {} // 생성자
    };
    

접근 방법

각 순회의 접근 방법은 다음과 같습니다:

  • 전위 순회 (Preorder Traversal): 현재 노드를 방문한 후, 왼쪽 자식 노드를 방문하고, 오른쪽 자식 노드를 방문합니다.
  • 중위 순회 (Inorder Traversal): 왼쪽 자식 노드를 방문한 후, 현재 노드를 방문하고, 오른쪽 자식 노드를 방문합니다.
  • 후위 순회 (Postorder Traversal): 왼쪽 자식 노드를 방문한 후, 오른쪽 자식 노드를 방문하고, 마지막으로 현재 노드를 방문합니다.

전위 순회 코드 구현

아래는 C++로 전위 순회를 구현한 코드입니다:

 
#include <iostream>
#include <vector>

using namespace std;

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

void preorderTraversal(TreeNode* root, vector& result) {
    if (root == nullptr) return; // 기본 사례
    result.push_back(root->val); // 노드 방문
    preorderTraversal(root->left, result); // 왼쪽 서브트리 방문
    preorderTraversal(root->right, result); // 오른쪽 서브트리 방문
}

int main() {
    // 이진 트리 초기화
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);
    
    vector result;
    preorderTraversal(root, result);
    
    // 결과 출력
    for (int val : result) {
        cout << val << " ";
    }
    return 0;
}
    

중위 순회 코드 구현

아래는 C++로 중위 순회를 구현한 코드입니다:


void inorderTraversal(TreeNode* root, vector& result) {
    if (root == nullptr) return; // 기본 사례
    inorderTraversal(root->left, result); // 왼쪽 서브트리 방문
    result.push_back(root->val); // 노드 방문
    inorderTraversal(root->right, result); // 오른쪽 서브트리 방문
}

// main 함수는 이전과 동일
    

후위 순회 코드 구현

아래는 C++로 후위 순회를 구현한 코드입니다:


void postorderTraversal(TreeNode* root, vector& result) {
    if (root == nullptr) return; // 기본 사례
    postorderTraversal(root->left, result); // 왼쪽 서브트리 방문
    postorderTraversal(root->right, result); // 오른쪽 서브트리 방문
    result.push_back(root->val); // 노드 방문
}

// main 함수는 이전과 동일
    

시간 복잡도 및 최적화

각 트리 순회 방법의 시간 복잡도는 O(n)입니다. 이는 트리의 모든 노드를 한 번씩 방문하기 때문입니다. 공간 복잡도는 트리가 완전하지 않을 경우 최악의 경우 O(h) (h는 트리의 높이)입니다. 만약 트리가 선형적인 형태라면 O(n) 공간이 필요할 수 있습니다.

최적화 방법

트리 순회 성능을 더욱 개선하기 위해 스택을 사용하여 반복적인 방법으로 구현할 수 있습니다. 예를 들어 전위 순회를 반복적으로 구현할 때는 다음과 같이 구현할 수 있습니다:


#include <stack>

void iterativePreorderTraversal(TreeNode* root, vector& result) {
    if (root == nullptr) return; 
    stack stk;
    stk.push(root);

    while (!stk.empty()) {
        TreeNode* node = stk.top();
        stk.pop();
        result.push_back(node->val); // 노드 방문
        if (node->right != nullptr) stk.push(node->right); // 오른쪽 자식 먼저 추가
        if (node->left != nullptr) stk.push(node->left); // 왼쪽 자식 추가
    }
}

// main 함수는 이전과 동일
    

결론

이 강좌에서는 C++를 사용하여 이진 트리의 다양한 순회 알고리즘을 구현하는 방법을 설명했습니다. 각 순회의 방법론을 이해하고, 구체적인 코드를 작성하여 이를 실습하는 것이 중요합니다. 이진 트리는 다양한 분야에서 중요한 역할을 하므로 확실히 익혀두는 것이 좋습니다.

참고 자료

C++ 코딩테스트 강좌, 트라이

1. 서론

오늘의 강좌에서는 C++ 프로그래밍 언어를 사용하여 트라이(Trie) 자료구조에 대해 심도 있게 살펴보고,
이를 활용한 알고리즘 문제를 해결하겠습니다. 트라이는 문자열을 저장하고 검색하는 데 최적화된
자료구조로, 사전 찾기, 오토 컴플리션, 접두어 검색 등 다양한 문제를 해결하는 데 유용합니다.

2. 트라이 자료구조 소개

트라이는 각 노드가 문자열의 문자를 저장하며, 노드 간의 경로는 특정 문자열의 접두어를 나타냅니다.
이 자료구조를 사용하면 문자열 검색을 효율적으로 수행할 수 있는데, 기본 시간 복잡도는 O(m)입니다.
여기서 m은 검색하고자 하는 문자열의 길이입니다.

2.1 트라이의 구성

트라이의 기본 구성 요소는 다음과 같습니다:

  • Node: 각 노드는 문자와 하위 노드를 포함합니다.
  • Insert: 새로운 문자열을 트라이에 추가합니다.
  • Search: 특정 문자열을 트라이에서 검색합니다.
  • Delete: 특정 문자열을 트라이에서 삭제합니다.

2.2 트라이의 장점과 단점

트라이의 장점은 prefix(접두어) 검색이 용이하다는 것입니다. 그러나 메모리 사용량이 많고,
구현이 복잡할 수 있다는 단점이 있습니다.

3. 문제 정의

다음은 알고리즘 문제입니다.
주어진 문자열 리스트에서 특정 접두어를 가진 모든 단어를 출력하는 프로그램을 작성하세요.

문제 설명

입력:
– 단어 리스트 words: [“apple”, “app”, “application”, “bat”, “ban”, “band”]
– 특정 접두어 prefix: “ap”

출력:
– 접두어가 “ap”인 문자열: [“apple”, “app”, “application”]

4. 문제 해결 과정

4.1 트라이 클래스 정의

먼저, 트라이 노드를 정의하고, 트라이 클래스를 구현해야 합니다. 아래는 C++에서의 기본 구현입니다.


#include 
#include 
#include 
#include 

using namespace std;

class TrieNode {
public:
    unordered_map children;
    bool isEndOfWord;

    TrieNode() : isEndOfWord(false) {}
};

class Trie {
private:
    TrieNode* root;

public:
    Trie() {
        root = new TrieNode();
    }

    void insert(const string& word) {
        TrieNode* node = root;
        for (char c : word) {
            if (!node->children[c]) {
                node->children[c] = new TrieNode();
            }
            node = node->children[c];
        }
        node->isEndOfWord = true;
    }

    void searchByPrefix(const string& prefix, vector& result, string current, TrieNode* node) {
        if (node->isEndOfWord) {
            result.push_back(current);
        }

        for (const auto& pair : node->children) {
            searchByPrefix(prefix, result, current + pair.first, pair.second);
        }
    }

    vector getWordsWithPrefix(const string& prefix) {
        TrieNode* node = root;
        vector result;

        for (char c : prefix) {
            if (!node->children[c]) {
                return result; // No words with the given prefix
            }
            node = node->children[c];
        }

        searchByPrefix(prefix, result, prefix, node);
        return result;
    }
};

    

4.2 메인 함수 구현

이제 메인 함수를 구현하여 트라이에 단어를 삽입하고, 접두어로 단어를 검색해보겠습니다.


int main() {
    Trie trie;
    vector words = {"apple", "app", "application", "bat", "ban", "band"};

    // Insert words into the trie
    for (const string& word : words) {
        trie.insert(word);
    }

    // Prefix to search
    string prefix = "ap";
    vector result = trie.getWordsWithPrefix(prefix);

    // Print the result
    cout << "Words with prefix '" << prefix << "': ";
    for (const string& word : result) {
        cout << word << " ";
    }
    cout << endl;

    return 0;
}

    

5. 코드 실행 결과

위의 코드가 실행되면, 아래와 같은 결과가 출력됩니다.
주어진 접두어 “ap”에 해당하는 모든 단어가 리스트로 출력됩니다.


Words with prefix 'ap': apple app application

    

6. 결론

이번 강좌에서는 C++를 사용하여 트라이 데이터 구조를 구현하고, 특정 접두어를 가진 단어들을
검색하는 문제를 해결했습니다. 트라이는 문자열 관련 알고리즘 문제를 해결하는 데 매우 유용한
자료구조이며, 다양한 응용 분야에서 사용될 수 있습니다. 앞으로의 강좌에서도
다양한 알고리즘 문제 해결을 위해 레퍼런스로 사용할 수 있는 자료구조와 기법을 학습해 나가길
바랍니다. 감사합니다.

C++ 코딩테스트 강좌, 투 포인터

투 포인터(Two Pointers)란?

투 포인터 기법은 효율적인 알고리즘 설계를 위한 기법 중 하나입니다. 이 방법은 주로 정렬된 자료구조에서 특정 조건을 만족하는 데이터 쌍이나 서브 배열을 찾는 데 유용합니다. 기본적으로 두 개의 포인터를 사용하여 배열의 시작과 끝 또는 두 개의 다른 요소를 가리키며, 이를 통해 문제를 해결합니다.

투 포인터 기법은 여러 데이터 문제를O(n)의 시간 복잡도로 해결할 수 있게 해줍니다. 일반적으로 연속된 배열의 문제, 합 문제, 부분 배열 문제에서 많이 사용됩니다. 이 방법은 투 포인터의 위치를 조정함으로써 문제의 조건에 맞는 답을 찾도록 설계됩니다.

문제 설명: 특정 합을 갖는 부분 배열 찾기

문제: 정수 배열 nums가 주어질 때, 두 개의 수를 선택하여 그 합이 주어진 정수 target과 일치하는지 찾는 알고리즘을 작성하시오. 단, 각 요소는 단 한번만 사용할 수 있습니다. 두 수의 인덱스를 배열의 형태로 반환하시오.

예시 입력:

                nums = [2, 7, 11, 15], target = 9
            

예시 출력:

                [0, 1]
            

설명: nums[0] + nums[1] = 2 + 7 = 9.

문제 풀이 과정

1. 문제 이해

본 문제는 주어진 배열에서 두 수의 인덱스를 찾는 것을 목표로 합니다. 문제의 핵심은 각 수가 단 한 번만 사용될 수 있다는 점입니다. 따라서, 특정 합을 찾기 위해 조합을 만들어내는 과정이 필요합니다.

2. 접근 방법

투 포인터 기법을 통해 두 포인터를 이용해 문제를 해결할 예정입니다. 배열을 정렬하더라도 원래의 인덱스를 추적할 수 있도록 해야 합니다. 또한, 배열이 정렬되어 있으므로 두 포인터의 위치를 조정하여 최적의 솔루션을 찾을 수 있습니다.

3. 알고리즘 설계

  1. 배열을 정렬합니다.
  2. 두 포인터를 설정합니다: 하나는 배열의 첫 번째 요소를 가리키고, 다른 하나는 마지막 요소를 가리킵니다.
  3. 합이 target에 도달했다면 해당 인덱스를 반환합니다.
  4. 합이 target보다 작다면 왼쪽 포인터를 오른쪽으로 이동시켜 합을 증가시킵니다.
  5. 합이 target보다 크다면 오른쪽 포인터를 왼쪽으로 이동시켜 합을 감소시킵니다.
  6. 위 과정을 반복하다가 두 포인터가 겹치면 종료합니다.

4. C++ 코드 구현

                
                #include 
                #include 
                #include 

                using namespace std;

                vector twoSum(vector& nums, int target) {
                    unordered_map numMap;
                    vector result;

                    for (int i = 0; i < nums.size(); i++) {
                        int complement = target - nums[i];
                        if (numMap.find(complement) != numMap.end()) {
                            result.push_back(numMap[complement]);
                            result.push_back(i);
                            return result;
                        }
                        numMap[nums[i]] = i;
                    }
                    return result; // 결괏값을 못 찾은 경우
                }

                int main() {
                    vector nums = {2, 7, 11, 15};
                    int target = 9;
                    vector indices = twoSum(nums, target);
                    
                    cout << "[" << indices[0] << ", " << indices[1] << "]" << endl;
                    return 0;
                }
                
            

5. 코드 설명

위 C++ 코드는 주어진 nums 배열을 순회하며 target 값을 만들 수 있는 두 수의 인덱스를 찾습니다. 해시맵(unordered_map)을 사용하여 시간 복잡도를 O(n)으로 유지합니다. 첫 번째 수를 선택할 때마다 그 보완 수(target - nums[i])가 해시맵에 존재하는지 확인하여, 존재한다면 두 인덱스를 결과로 반환합니다.

해시맵은 각 수를 빠르게 조회할 수 있게 해주며, 배열을 한 번만 순회하여 효율적으로 답을 찾을 수 있도록 도와줍니다.

6. 복잡도 분석

위 알고리즘의 시간 복잡도는 O(n)이며, 공간 복잡도는 O(n)입니다. 해시맵을 통해 보완 수를 기록하고 이를 통해 효율적인 검색을 가능하게 합니다. 배열이 정렬되지 않은 경우가 많아 해시맵을 통한 접근이 유리합니다.

마무리

투 포인터 기법은 배열 문제에서 매우 유용하게 사용됩니다. 본 문제와 같이 정렬되지 않은 배열에서 특정 합을 만족하는 두 수를 찾는 것은 자주 등장하는 문제입니다. 알고리즘의 최적화를 위해 해시맵을 사용하는 것도 좋은 전략입니다.

다양한 문제를 풀어보며 투 포인터 기법을 익히고, 이를 다른 문제에 확장하여 응용할 수 있도록 연습해보시길 바랍니다.

C++ 코딩테스트 강좌, 퇴사 준비하기

여러분 안녕하세요! 이번 포스트에서는 C++로 취업을 준비하는 이들을 위한 코딩테스트에 대해 알아보겠습니다.

코딩테스트의 중요성

최근 많은 기업들이 기술 면접을 위해 알고리즘 문제를 제시합니다. 이 과정에서 우리는 알고리즘을 이해하고 이를 C++로 구현할 수 있어야 합니다. 알고리즘과 자료 구조에 대한 깊은 이해는 면접에서 좋은 결과를 가져올 수 있는 열쇠입니다.

문제 설명

문제: 두 수의 합

주어진 정수 배열과 정수 목표값이 있을 때, 배열에서 두 수를 선택하여 목표값을 만드는 모든 쌍의 인덱스를 반환하는 문제입니다. 입력으로는 정수형 배열 nums와 정수형 목표값 target이 주어집니다.

입력 예시

    nums = [2, 7, 11, 15]
    target = 9

출력 예시

    [0, 1]

문제 해결의 접근 방법

이 문제를 해결하기 위한 여러 가지 접근 방식이 있습니다. 여기서는 두 가지 접근 방식인 브루트 포스해시맵을 이용하는 방법을 설명합니다.

1. 브루트 포스 접근

브루트 포스 방법으로는 두 개의 중첩된 반복문을 사용하여 배열의 모든 가능한 두 숫자를 비교하는 것입니다. 이 방법의 시간 복잡도는 O(n^2)입니다.

    int twoSum(vector& nums, int target) {
        for (int i = 0; i < nums.size(); i++) {
            for (int j = i + 1; j < nums.size(); j++) {
                if (nums[i] + nums[j] == target) {
                    return {i, j};
                }
            }
        }
        return {};
    }

2. 해시맵 사용하기

조금 더 효율적인 방법으로 해시맵을 사용할 수 있습니다. 해시맵을 통해 배열의 각 숫자가 목표 값을 이루기 위해 필요한 숫자를 빠르게 찾을 수 있습니다. 이 방법의 시간 복잡도는 O(n)입니다.

    vector twoSum(vector& nums, int target) {
        unordered_map num_map;
        for (int i = 0; i < nums.size(); i++) {
            int complement = target - nums[i];
            if (num_map.find(complement) != num_map.end()) {
                return {num_map[complement], i}; // Found the two numbers
            }
            num_map[nums[i]] = i; // Store the number with its index
        }
        return {};
    }

문제 풀이 과정

이제 위의 두 접근 방식을 바탕으로 문제를 풀어보겠습니다.

브루트 포스 방법

먼저 브루트 포스 접근법부터 실행해보겠습니다. 간단하게 주어진 배열을 이중 반복을 통해 모든 조합을 확인하고, 두 숫자의 합이 목표값과 같은지 비교합니다.

해시맵을 활용한 방법

다음으로 해시맵을 이용한 방법으로, 배열을 한 번 스캔하면서 필요한 값을 찾고, 찾으면 인덱스를 반환합니다.

정리 및 결론

이번 포스팅에서는 간단한 두 수의 합 문제를 통해 C++에서 알고리즘 문제 해결 방법에 대해 알아보았습니다. 브루트 포스 방법은 간단하지만 비효율적이며, 해시맵을 활용한 방법은 훨씬 더 효율적입니다. 코딩 테스트 중 비슷한 문제를 만나면 다양한 접근 방법을 고려하고 시간 복잡도를 고려하여 최적의 방법을 선택해야 합니다.

퇴사 준비를 하면서 코딩테스트를 준비하는 것은 어렵지만, 꾸준한 연습과 다양한 문제 풀이를 통해 향상시킬 수 있습니다. 감사합니다!

작성자: 조광형

블로그: [당신의 블로그 링크]