C++ 코딩테스트 강좌, 배열과 리스트

코딩테스트는 현대의 많은 기업들이 구직자를 평가하는 데 사용하는 중요한 도구입니다. 오늘은 배열과 리스트를 주제로 하는 문제를 다뤄보겠습니다. 배열(array)과 리스트(list)는 자료 구조의 가장 기초적인 형태로, 많은 알고리즘 문제에서 기본적으로 사용됩니다. 배열은 고정 크기의 연속 메모리 공간에 데이터를 저장하는 반면, 리스트는 동적 크기로 요소를 저장할 수 있습니다. 이 두 가지의 차이점을 이해하고 이를 이용한 문제 해결 능력을 키우는 것이 중요합니다.

문제: 배열에서 두 숫자의 합 찾기

주어진 정수 배열에서 특정 합을 만들 수 있는 두 숫자의 인덱스를 찾아라. 배열의 요소는 중복될 수 있으며, 두 숫자는 반드시 서로 다른 인덱스에서 선택되어야 한다.

입력 형식

  • 첫 번째 줄에는 배열의 크기 n (1 ≤ n ≤ 10^4)이 주어진다.
  • 두 번째 줄에는 배열의 n개의 정수 a[0], a[1], ..., a[n-1] (1 ≤ a[i] ≤ 10^9)이 주어진다.
  • 세 번째 줄에는 찾고자 하는 정수 target가 주어진다.

출력 형식

찾은 두 숫자의 인덱스를 공백으로 구분하여 출력한다. 만약 존재하지 않는 경우 -1을 출력한다.

예제

    입력:
    5
    2 7 11 15
    9

    출력:
    0 1
    

문제 해결 전략

이 문제는 배열에서 두 숫자의 합을 찾는 대표적인 문제로, 다음과 같은 과정을 통해 해결할 수 있습니다:

  1. 해시맵 사용:
  2. 해시맵을 사용하여 배열 요소와 그 인덱스를 저장합니다. 각 요소에 대해, target - current_number를 계산하여 해시맵에서 해당 값이 있는지 확인합니다. 만약 존재한다면 해당 인덱스와 현재 인덱스를 반환합니다. 이 접근법은 평균적으로 O(n) 시간 복잡도를 가집니다.

C++ 코드 구현


#include <iostream>
#include <unordered_map>
#include <vector>

using namespace std;

vector twoSum(vector& nums, int target) {
    unordered_map hashMap; // 해시맵 선언
    for (int i = 0; i < nums.size(); ++i) {
        int complement = target - nums[i]; // 필요한 숫자 찾기
        if (hashMap.find(complement) != hashMap.end()) {
            return {hashMap[complement], i}; // 인덱스 반환
        }
        hashMap[nums[i]] = i; // 숫자와 그 인덱스 저장
    }
    return {-1}; // 찾지 못한 경우
}

int main() {
    int n, target;
    cin >> n;
    vector nums(n);
    for (int i = 0; i < n; ++i) {
        cin >> nums[i]; // 배열 입력
    }
    cin >> target; // 타겟 입력

    vector result = twoSum(nums, target);
    for (int idx : result) {
        cout << idx << " "; // 결과 출력
    }
    return 0;
}

코드 설명

위 코드는 주어진 배열과 타겟 값을 입력받아 두 숫자의 인덱스를 찾는 방식으로 작동합니다. unordered_map을 사용해 각 숫자와 그 인덱스를 저장함으로써, 배열을 한 번만 순회하여 해결할 수 있습니다. 이렇게 함으로써 최악의 경우에도 O(n)의 시간 복잡도를 가지게 됩니다.

결과 검증

작성한 코드가 정확하게 작동하는지 검증하기 위해, 다양한 테스트 케이스를 준비합니다. 예를 들어:

  • n=5, 입력 배열: [2, 7, 11, 15], 타겟: 9 → 결과: 0 1
  • n=4, 입력 배열: [3, 2, 4], 타겟: 6 → 결과: 1 2
  • n=3, 입력 배열: [3, 3], 타겟: 6 → 결과: 0 1
  • n=2, 입력 배열: [1, 2], 타겟: 3 → 결과: 0 1
  • n=1, 입력 배열: [2], 타겟: 2 → 결과: -1 (원소가 없으므로)

마무리

오늘은 배열과 리스트를 이용한 코딩테스트 문제를 한 가지 다루어 보았습니다. 두 숫자의 합을 찾는 문제는 어려운 문제는 아니지만, 다양한 방법으로 접근 가능하다는 점에서 연습할 가치가 있습니다. 해시맵을 사용하면 시간 복잡도를 효율적으로 줄일 수 있는 좋은 예입니다. 다음 강좌에서는 다른 자료 구조나 알고리즘 접근 방식을 다뤄보겠습니다. 계속해서 코딩 연습을 해보세요!

C++ 코딩테스트 강좌, 미로 탐색하기

문제 설명

주어진 미로를 탐색하여 출구까지 가는 경로를 찾는 문제입니다. 미로는 2차원 배열로 주어지며,
0은 이동할 수 있는 경로, 1은 벽을 의미합니다. 시작점은 (0, 0)이고, 출구는 (n-1, m-1)입니다.
미로의 크기는 n x m이며, 미로를 탐색하는 알고리즘을 구현하시오.

입력 형식

첫째 줄에 미로의 크기 nm이 주어집니다. (1 ≤ n, m ≤ 100)
다음 n개의 줄에는 0과 1로 이루어진 미로의 정보가 주어집니다.

출력 형식

출구까지 도달할 수 있다면 그 경로의 길이를 출력하고, 불가능하다면 -1을 출력합니다.

예제 입력

    5 5
    0 1 0 0 0
    0 1 0 1 0
    0 0 0 1 0
    1 1 0 1 0
    0 0 0 0 0
    

예제 출력

    9
    

문제 해결 접근 방법

이 문제는 BFS(너비 우선 탐색) 또는 DFS(깊이 우선 탐색)를 활용하여 미로를 탐색할 수 있습니다.
BFS는 가장 먼저 발견된 경로가 최단 경로이므로, 그리드 탐색 시 BFS를 추천합니다.

BFS를 사용할 경우, 큐를 활용하여 현재 위치에서 이동 가능한 노드를 탐색하고,
방문 여부를 체크한 후, 새로운 위치를 큐에 추가합니다.
이러한 방법으로 출구에 도달하는 가장 짧은 경로를 찾아낼 수 있습니다.

C++ 코드 구현

아래는 미로 탐색 문제를 해결하기 위한 C++ 코드입니다.


#include <iostream>
#include <vector>
#include <queue>
#include <utility>

using namespace std;

// 이동 방향: 상, 하, 좌, 우
const int dx[] = {-1, 1, 0, 0};
const int dy[] = {0, 0, -1, 1};

int bfs(const vector>& maze, int n, int m) {
    queue> q; // 큐 선언
    q.push({0, 0}); // 시작 위치 (0, 0)
    
    vector> distance(n, vector(m, -1)); // 거리 배열
    distance[0][0] = 1; // 시작점 거리 1로 초기화

    while (!q.empty()) {
        auto current = q.front(); q.pop();
        int x = current.first;
        int y = current.second;

        // 목표 지점에 도달한 경우
        if (x == n - 1 && y == m - 1) {
            return distance[x][y]; // 경로 길이 반환
        }

        // 인접 노드 탐색
        for (int i = 0; i < 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];

            // 범위 체크 및 이동 가능한지 확인
            if (nx >= 0 && nx < n && ny >= 0 && ny < m && 
                maze[nx][ny] == 0 && distance[nx][ny] == -1) {
                q.push({nx, ny}); // 큐에 추가
                distance[nx][ny] = distance[x][y] + 1; // 거리 업데이트
            }
        }
    }
    return -1; // 출구에 도달하지 못한 경우
}

int main() {
    int n, m;
    cin >> n >> m; // 미로 크기 입력
    vector> maze(n, vector(m));
    
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> maze[i][j]; // 미로 입력
        }
    }

    cout << bfs(maze, n, m) << endl; // 결과 출력
    return 0;
}
    

코드 설명

위 코드는 2차원 벡터를 사용하여 미로를 표현하고, BFS를 통해 출구까지의 거리를 계산합니다.
주요 구성 요소로는:

  • 큐(Queue): 현재 탐색 중인 노드를 저장하여 다음 이동할 노드를 결정하는 데 사용됩니다.
  • 거리 배열(Distance Array): 각 노드까지의 거리를 기록하여 이미 방문한 노드를 다시 방문하지 않도록 합니다.
  • 이동 방향 배열: 상, 하, 좌, 우 방향으로 이동할 수 있도록 dx와 dy 배열을 설정했습니다.

프로그램이 수행될 때, 시작점에서 BFS를 수행하며 모든 인접 노드를 탐색하게 됩니다.
출구에 도달하면 해당 지점까지의 길이를 반환하며, 도달하지 못한 경우 -1을 반환합니다.

시간 복잡도

시간 복잡도는 O(n * m)입니다. 모든 노드를 한 번씩 방문하기 때문입니다.
공간 복잡도 또한 역시 O(n * m)으로, 거리 배열과 큐를 사용하기 때문입니다.

마치며

이번 강좌에서는 C++를 이용한 미로 탐색 문제를 해결하기 위해 BFS 알고리즘을 구현해보았습니다.
알고리즘의 기본 개념을 이해하고, 실습을 통해 실제 코드를 작성하는 데에 도움을 줄 수 있기를 바랍니다.
그러므로, 다양한 형태의 미로 탐색 문제를 통해 경험치를 쌓아가길 추천합니다.

자신의 코드를 더욱 개선하기 위해 다양한 테스트 케이스를 만들어보는 것도 좋은 학습 방법입니다.

C++ 코딩테스트 강좌, 문자열 찾기

안녕하세요! 이번 글에서는 C++를 활용한 문자열 찾기 문제에 대해 알아보겠습니다. 알고리즘 문제를 해결하는 것은 프로그래밍 능력을 발전시키고, 코딩 테스트에서 좋은 성적을 얻는 데 도움이 됩니다. 특히 최근 몇 년 동안 코딩 테스트에서 문자열 처리 문제는 매우 자주 출제되고 있습니다. 따라서 이 주제에 대한 충분한 이해와 연습이 필요합니다.

문제 설명

이번 강의에서는 주어진 텍스트에서 특정 문자열(패턴)을 찾아서 해당 문자열이 텍스트에 몇 번 등장하는지 세는 문제를 다루어 보겠습니다. 이 문제는 문자열 검색 알고리즘의 기초적인 예로, 다음과 같은 문제를 해결할 것입니다.

문제:

주어진 문자열 textpattern이 있을 때, patterntext에 몇 번 나타나는지를 구하는 프로그램을 작성하시오.

입력 형식

  • 첫 번째 줄: 문자열 text (1 ≤ |text| ≤ 100,000)
  • 두 번째 줄: 문자열 pattern (1 ≤ |pattern| ≤ 100,000)

출력 형식

  • 일치하는 패턴의 개수를 출력한다.

접근 방식

문제 해결을 위해 사용할 수 있는 여러 알고리즘이 있지만, 여기서는 간단한 브루트 포스 방법을 살펴보겠습니다. 이 방법은 텍스트의 각 위치에서 패턴을 비교하여 그 일치 여부를 판단합니다. 성능은 O(n * m)으로, n은 텍스트의 길이, m은 패턴의 길이입니다. 이 방법은 간단하고 직관적이지만, 더 효율적인 방법인 KMP 알고리즘이나 Rabin-Karp 알고리즘 등을 고려할 수도 있습니다.

코드 구현

이제 코드를 구현해보겠습니다. 아래는 C++로 작성된 문자열 찾기 프로그램입니다:


#include <iostream>
#include <string>

int countOccurrences(const std::string &text, const std::string &pattern) {
    int count = 0;
    int textLength = text.length();
    int patternLength = pattern.length();

    for (int i = 0; i <= textLength - patternLength; i++) {
        // substring를 통해 부분 문자열 비교
        if (text.substr(i, patternLength) == pattern) {
            count++;
        }
    }
    return count;
}

int main() {
    std::string text, pattern;

    std::cout << "텍스트를 입력하세요: ";
    std::getline(std::cin, text);
    std::cout << "패턴을 입력하세요: ";
    std::getline(std::cin, pattern);

    int occurrences = countOccurrences(text, pattern);
    std::cout << "패턴은 " << occurrences << " 번 나타납니다." << std::endl;

    return 0;
}

코드 설명

위의 C++ 코드는 문자열 찾기 문제를 해결합니다. 사용자가 제공한 textpattern을 기반으로 기능을 수행합니다.

  • countOccurrences 함수: 이 함수는 주어진 text에서 pattern의 발생 횟수를 세는 역할을 합니다. 이 함수는 텍스트의 각 위치에서 서브스트링을 추출하고 주어진 패턴과 비교하여 일치할 때마다 카운트를 증가시킵니다.
  • main 함수: 기본적인 입출력을 처리하는 부분입니다. 사용자로부터 문자열을 입력받고, countOccurrences 함수를 호출하여 결과를 출력합니다.

성능 분석

위의 브루트 포스 방법은 최악의 경우 O(n * m)의 시간 복잡도를 가지게 됩니다. 텍스트의 길이가 길어질수록 성능이 떨어질 수 있습니다. 그러나 코드의 간결성과 직관성을 중시한다면 이 방법은 유용할 수 있습니다.

다음으로, 효율적인 해결을 원한다면 KMP(Knuth-Morris-Pratt) 알고리즘을 고려해 볼 수 있습니다. KMP 알고리즘은 패턴의 부분 반복을 활용하여 문자열 검색을 최적화합니다. 이 알고리즘은 O(n + m)의 시간 복잡도를 갖습니다.

KMP 알고리즘 소개

KMP 알고리즘은 패턴 검색 문제를 해결하기 위해 만들어진 알고리즘으로, 이전에 일치했던 부분을 이용하여 반복 검사를 줄이는 방식입니다. 이 알고리즘은 두 가지 주요 단계로 구성됩니다. 첫 번째 단계는패턴에 대한 ‘LPS(Longest Prefix Suffix)’ 배열을 구축하는 것이고, 두 번째 단계는 이 LPS 배열을 활용하여 텍스트에서 패턴을 검색하는 것입니다.

LPS 배열 구축

LPS 배열은 패턴의 각 접두사(prefix) 중에서 가장 긴 접두사와 접미사(suffix)의 길이를 저장하고 있습니다. 예를 들어, 패턴 “ABAB”의 경우 LPS 배열은 [0, 0, 1, 2]가 됩니다. 이는 다음의 의미를 갖습니다:

  • 인덱스 0에 해당하는 A까지는 접두사와 접미사가 없으므로 0
  • 인덱스 1에서 B는 접두사와 접미사가 없으므로 0
  • 인덱스 2에서 AB는 A가 접두사이자 접미사이므로 1
  • 인덱스 3에서 ABA는 AB가 접두사이자 접미사이므로 2

KMP 알고리즘 구현

아래는 KMP 알고리즘을 구현한 C++ 코드입니다:


#include <iostream>
#include <string>
#include <vector>

std::vector computeLPS(const std::string &pattern) {
    int m = pattern.length();
    std::vector lps(m);
    int len = 0; 
    lps[0] = 0; 
    int i = 1;

    while (i < m) {
        if (pattern[i] == pattern[len]) {
            len++;
            lps[i] = len;
            i++;
        } else {
            if (len != 0) {
                len = lps[len - 1];
            } else {
                lps[i] = 0;
                i++;
            }
        }
    }
    return lps;
}

int KMP(const std::string &text, const std::string &pattern) {
    std::vector lps = computeLPS(pattern);
    int n = text.length();
    int m = pattern.length();
    int i = 0; 
    int j = 0; 
    int count = 0;

    while (i < n) {
        if (pattern[j] == text[i]) {
            i++;
            j++;
        }
        if (j == m) {
            count++;
            j = lps[j - 1];
        } else if (i < n && pattern[j] != text[i]) {
            if (j != 0)
                j = lps[j - 1];
            else
                i++;
        }
    }
    return count;
}

int main() {
    std::string text, pattern;

    std::cout << "텍스트를 입력하세요: ";
    std::getline(std::cin, text);
    std::cout << "패턴을 입력하세요: ";
    std::getline(std::cin, pattern);

    int occurrences = KMP(text, pattern);
    std::cout << "패턴은 " << occurrences << " 번 나타납니다." << std::endl;

    return 0;
}

마무리

이번 글에서는 C++로 문자열 찾기 문제를 해결하는 방법에 대해 알아보았습니다. 브루트 포스 방법을 소개한 후, 효율적인 KMP 알고리즘이 어떻게 작동하는지 살펴보았습니다. 문자열 관련 문제는 기본적인 알고리즘 지식을 토대로 더욱 발전할 수 있습니다. 따라서 이러한 문제를 꾸준히 연습하여 프로그래밍 능력을 향상시키는 것이 중요합니다.

여러분도 다양한 문자열 문제를 풀면서 자신만의 알고리즘을 개발해보세요! 코딩 테스트에 대한 자신감을 가질 수 있을 것입니다. 감사합니다!

C++ 코딩테스트 강좌, 물의 양 구하기

여러분 안녕하세요! 이번 강좌에서는 물의 양을 구하는 문제를 통해 C++ 프로그래밍 언어의 기초와 알고리즘 문제 해결 과정을 자세히 살펴보겠습니다. 알고리즘 문제는 단순한 코드 작성을 넘어서, 문제를 어떻게 이해하고 해석할지에 대한 고민이 필요합니다. 따라서 이번에는 문제에 대한 접근 과정, 코드 작성, 성능 분석, 그리고 최적화 전략에 대해 자세히 다루도록 하겠습니다.

문제 설명

문제의 전반적 정의는 다음과 같습니다. 주어진 높이의 배열이 있을 때, 비가 올 경우 어떤 지역에 얼마나 많은 물이 고일 수 있는지를 계산하는 문제입니다. 이는 “Trapping Rain Water” 문제로 널리 알려져 있으며, 다양하고 흥미로운 해결 방법이 존재합니다.

문제 예시

높이 배열이 주어졌다고 가정해 보겠습니다. 아래의 배열을 예로 들어 볼까요:

{0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1}

위와 같은 배열이 주어질 때, 비가 내리면 다음과 같이 물이 고일 수 있습니다:

  • 인덱스 0: 물이 고이지 않음
  • 인덱스 1: 물이 고이지 않음
  • 인덱스 2: 1 유닛의 물
  • 인덱스 3: 물이 고이지 않음
  • 인덱스 4: 1 유닛의 물
  • 인덱스 5: 2 유닛의 물
  • 인덱스 6: 1 유닛의 물
  • 인덱스 7: 물이 고이지 않음
  • 인덱스 8: 1 유닛의 물
  • 인덱스 9: 1 유닛의 물
  • 인덱스 10: 1 유닛의 물
  • 인덱스 11: 물이 고이지 않음

따라서 총 고인 물의 양은 6 유닛입니다.

문제 접근 방법

문제를 해결하기 위해서는 먼저 어떻게 물이 고이는지를 이해해야 합니다. 물이 고일 수 있는 양은 특정 인덱스 주변의 높은 경계에 따라 결정됩니다. 예를 들어, 어떤 위치에서 물이 고이기 위해서는 그 위치의 양쪽에 더 높은 경계가 존재해야 하며, 이 두 경계의 낮은 부분이 물이 고일 수 있는 높이입니다.

이를 계산하는 방법으로는 여러 가지가 있으며, 여기서는 두 가지 방법을 소개할 것입니다:

  1. 투 포인터 방식
  2. DP(동적 프로그래밍) 방식

투 포인터 방식

첫 번째 접근 방식은 투 포인터를 사용하는 방법입니다. 왼쪽과 오른쪽에서 각각 포인터를 두고, 높은 벽을 기준으로 물의 양을 계산해 나가는 방식입니다. 이 방법은 O(n) 시간 복잡도로 보통 배열을 한 번만 순회하여 문제를 해결할 수 있습니다.

알고리즘 설명

  1. 배열의 왼쪽과 오른쪽 끝에 각각 포인터를 두고, 왼쪽과 오른쪽의 최대 높이를 저장할 변수를 초기화합니다.
  2. 두 포인터가 서로 교차할 때까지 반복합니다.
  3. 왼쪽 포인터의 현재 높이가 오른쪽 포인터의 높이보다 낮은 경우, 왼쪽 포인터를 한 칸 오른쪽으로 이동시키고, 해당 위치에 저장된 최대 높이와의 차이를 이용하여 고인 물의 양을 계산합니다.
  4. 그렇지 않은 경우, 오른쪽 포인터를 한 칸 왼쪽으로 이동시키고, 유사한 계산을 합니다.

코드 구현

#include 
#include 
using namespace std;

int trap(vector& height) {
    if (height.size() == 0) return 0;

    int left = 0, right = height.size() - 1;
    int left_max = 0, right_max = 0;
    int water_trapped = 0;

    while (left < right) {
        if (height[left] < height[right]) {
            if (height[left] >= left_max) {
                left_max = height[left];
            } else {
                water_trapped += left_max - height[left];
            }
            left++;
        } else {
            if (height[right] >= right_max) {
                right_max = height[right];
            } else {
                water_trapped += right_max - height[right];
            }
            right--;
        }
    }
    return water_trapped;
}

int main() {
    vector height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};
    cout << "Total water trapped: " << trap(height) << endl;
    return 0;
}

동적 프로그래밍 방식

동적 프로그래밍을 사용하는 방법도 있습니다. 이 방법은 각 인덱스에서 물이 고일 수 있는 양을 사전에 계산해 놓은 배열을 바탕으로 진행됩니다. 이 또한 O(n) 시간 복잡도를 가집니다.

알고리즘 설명

  1. 정수 배열의 길이를 구하고, 두 개의 배열을 생성합니다. 하나는 왼쪽에서부터의 최대 높이 배열, 다른 하나는 오른쪽에서부터의 최대 높이 배열입니다.
  2. 왼쪽 최대 높이 배열을 채웁니다. 각 인덱스에서의 최대 높이는 이전 인덱스의 최대 높이와 현재 높이 중 더 큰 값을 선택하여 설정합니다.
  3. 오른쪽 최대 높이 배열을 채우는 과정도 유사하게 진행합니다.
  4. 마지막으로 각 인덱스의 고인 물의 양은 두 최대 높이 배열을 바탕으로 결정됩니다: min(left_max[i], right_max[i]) – height[i] 입니다.

코드 구현

#include 
#include 
using namespace std;

int trap(vector& height) {
    if (height.size() == 0) return 0;
    int n = height.size();
    vector left_max(n);
    vector right_max(n);
    int water_trapped = 0;

    // 왼쪽 최대 높이 계산
    left_max[0] = height[0];
    for (int i = 1; i < n; i++) {
        left_max[i] = max(left_max[i - 1], height[i]);
    }

    // 오른쪽 최대 높이 계산
    right_max[n - 1] = height[n - 1];
    for (int i = n - 2; i >= 0; i--) {
        right_max[i] = max(right_max[i + 1], height[i]);
    }

    // 고인 물의 양 계산
    for (int i = 0; i < n; i++) {
        water_trapped += min(left_max[i], right_max[i]) - height[i];
    }
    return water_trapped;
}

int main() {
    vector height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};
    cout << "Total water trapped: " << trap(height) << endl;
    return 0;
}

성능 분석

위의 두 가지 접근 방식 모두 O(n)의 시간 복잡도를 가집니다. 하지만 공간 복잡도가 중요한 경우, 투 포인터 방식이 더 효율적입니다. 반면 동적 프로그래밍 방식은 추가적인 O(n)의 공간을 필요로 하므로, 메모리 사용량이 중요한 경우 주의해야 합니다.

최적화 전략

C++ 코딩 테스트에서 성능을 극대화하기 위해 모집단, 배열 크기, 알고리즘 복잡도 등을 항상 고려해야 합니다. 또한 컴파일러 최적화에도 유의해야 하며, 불필요한 변수를 줄이고 메모리 관리에 신경을 기울이는 것이 좋습니다.

결론

이번 강좌에서는 물의 양 구하기 문제를 통해 C++의 기본적인 배열 처리 및 알고리즘 작성 방법을 알아보았습니다. 다양한 접근 방법과 각 방법의 장단점을 분석하면서, 어떻게 최적화할 수 있는지에 대해서도 살펴보았습니다. 앞으로의 코딩테스트에서 얻은 지식과 경험이 매우 유용하게 활용되길 바랍니다.

항상 연습하고 개선하는 여러분이 되길 바랍니다! 감사합니다.

C++ 코딩테스트 강좌, 리프 노드의 개수 구하기

이 글에서는 C++ 프로그래밍 언어를 사용하여 리프 노드의 개수를 구하는 방법에 대해 자세히 설명하겠습니다. 알고리즘 문제를 해결하는 과정과 코드를 모두 포함하여 처음부터 끝까지 쉽게 이해할 수 있도록 구성하였습니다.

문제 설명

리프 노드는 자식 노드가 없는 노드를 의미합니다. 주어진 이진 트리에서 리프 노드의 개수를 구하는 프로그램을 작성하십시오. 이 문제의 입력으로는 이진 트리의 루트 노드를 받을 것입니다.

입력 형식:

  • 이진 트리의 루트 노드 (루트 노드는 nullptr일 수 있습니다.)

출력 형식:

  • 리프 노드의 개수

알고리즘 접근 방식

이 문제를 해결하기 위해 우리는 재귀적으로 이진 트리를 탐색할 것입니다. 각 노드를 방문할 때마다 그 노드가 리프 노드인지 확인합니다. 리프 노드는 자식 노드가 없기 때문에, 왼쪽 및 오른쪽 자식 노드가 모두 nullptr인지 확인하면 됩니다.

구체적인 알고리즘:

  1. 루트 노드가 nullptr인 경우, 0을 반환합니다.
  2. 노드가 리프 노드일 경우, 1을 반환합니다.
  3. 왼쪽과 오른쪽 서브트리를 각각 재귀적으로 탐색하고, 그 결과를 합산하여 반환합니다.

C++ 코드 구현

이제 위의 알고리즘을 C++로 구현한 코드를 살펴보겠습니다.


#include <iostream>

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

int countLeafNodes(TreeNode* root) {
    // 1. 루트가 nullptr인 경우
    if (root == nullptr) {
        return 0;
    }
    
    // 2. 리프 노드인 경우
    if (root->left == nullptr && root->right == nullptr) {
        return 1;
    }
    
    // 3. 왼쪽과 오른쪽 서브트리 탐색
    return countLeafNodes(root->left) + countLeafNodes(root->right);
}

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);
    
    // 리프 노드의 개수 출력
    int leafCount = countLeafNodes(root);
    std::cout << "리프 노드의 개수: " << leafCount << std::endl;
    
    // 메모리 해제
    delete root->left->left;
    delete root->left->right;
    delete root->left;
    delete root->right;
    delete root;

    return 0;
}

코드 설명

위의 코드는 이진 트리의 루트 노드를 받아서 리프 노드의 개수를 반환하는 countLeafNodes 함수를 구현하였습니다.

구조체 선언:

TreeNode 구조체는 이진 트리의 각 노드를 표현합니다. 각 노드는 정수 값(val)과 왼쪽 자식(left) 및 오른쪽 자식(right) 포인터를 가지고 있습니다.

리프 노드 개수 세기:

countLeafNodes 함수는 먼저 루트가 nullptr인지 확인합니다. nullptr이면 0을 반환하고, 리프 노드인 경우 1을 반환합니다. 그렇지 않으면 왼쪽과 오른쪽 서브트리를 재귀적으로 호출하여 리프 노드 개수를 세어 합산합니다.

메인 함수:

메인 함수에서는 테스트용 이진 트리를 생성하고 그에 대한 리프 노드 개수를 출력합니다. 마지막으로 할당한 메모리를 해제하는 것도 잊지 말아야 합니다.

테스트 및 결과

이 코드를 실행하면 다음과 같은 결과를 얻게 됩니다:

리프 노드의 개수: 3

위의 이진 트리에서 리프 노드는 4와 5, 3 노드이므로 총 3개의 리프 노드가 존재합니다.

마무리

이 글에서는 C++로 이진 트리의 리프 노드 개수를 구하는 방법을 설명했습니다. 알고리즘을 이해하고 구현하는 과정의 예를 통해 C++의 기본적인 재귀 호출과 구조체 사용법을 배울 수 있습니다. 알고리즘 문제를 풀기 위해서는 문제를 쪼개고, 단계별로 접근하는 것이 중요합니다. 앞으로도 다양한 문제를 연습하여 알고리즘 실력을 쌓아가시기 바랍니다.

참고 자료