본문 바로가기

Software Engineering/Algorithm

[Programmers Lv. 1] C++: 로또의 최고 순위와 최저 순위

프로그래머스 Lv.1에 있는 "로또의 최고 순위와 최저 순위" 문제에 대한 풀이 및 탐구에 대한 기록.

목   차

     

    로또의 최고 순위와 최저 순위

    간만에 C++, 그리고 vector를 사용하게 된 문제다. 이에 따라 문제풀이를 시작하기 전에 가볍게 vector의 각종 키워드에 대한 복습부터 하고 문제풀이에 들어갔다. 

    vector 주요 키워드

    //vector의 맨 뒤에 요소 추가.
    lottos.push_back(44);
    
    //vector의 size(실제 요소의 개수) 구하기.
    std::cout << lottos.size(); 
    
    //vector의 empty여부를 bool로 반환.
    if (lottos.empty()) { /* 벡터가 비어있음 */ } 
    
    //인자의 위치에 있는 요소 반환.
    int num = lottos.at(2);
    
    //인자의 위치에 있는 요소 반환.
    int num = lottos[2];
    
    //vector의 처음/끝의 iterator 반환.
    for (auto it = lottos.begin(); it != lottos.end(); ++it) {
        std::cout << *it << " ";
    }
    
    //vector의 특정 위치에 새 요소 삽입.
    lottos.insert(lottos.begin() + 2, 99);
    
    //vector의 특정 위치(범위 가능)에 요소 제거.
    lottos.erase(lottos.begin() + 2);
    lottos.erase(lottos.begin() + 1, lottos.begin() + 3);
    
    //vector 요소 전체 삭제.
    lottos.clear();
    
    //vector의 size 재설정.
    lottos.resize(10);
    
    //vector의 capacity 재설정.
    lottos.reserve(20);
    
    //vector의 첫/마지막 요소 반환.
    int first = lottos.front();
    int last = lottos.back();
    
    //두 vector의 모든 요소 교환.
    lottos.swap(win_nums);
    
    //vector를 새로운 값으로 초기화.
    lottos.assign(5, 10);
    
    //vector의 끝에 요소를 직접 추가. push와의 차이는 이미 생성된 객체인지 여부에 있음.
    lottos.emplace_back(44);

     

     

    문제 풀이

    #1차 답안

    이 코드는 정답이긴 하지만, switch - case 문을 사용할 때 하드코딩된 숫자로 사용하는 것은 그 의미를 알기 힘들고 유지보수성을 떨어트리므로, 개선이 필요해보인다.

    #2차 답안

    위의 코드에서 하드코딩된 부분을 가독성과 유지보수성을 고려하여 아래와 같은 새로운 답안을 만들었다.

     

    1. for문 개선안

    for (int num : lottos) {
        if (num == 0) { missing_num++; }
        else if (find(win_nums.begin(), win_nums.end(), num) != win_nums.end()) { correct_num++; }
    }

     우선, 반복문을 vector 범위 기반 for 루프로 바꾸었다. 이로 인해 가독성을 높이면서 vector의 모든 범위를 순환할 수 있다. 

    다음으로 각기 따로 있던 if문을 else if문을 통해 합쳤다. 이로써 continue의 사용이 불필요해졌다. 이때 if문의 순서는 더 비용이 적고 빠르게 계산 가능한 순으로 구성했다.

    이 과정에서 기존에 lotto_it 에 find 메서드의 결과를 저장한 후 find의 결과를 end와 비교하던 로직을 한 줄로 바꾸었다. 어느 쪽이 더 가독성이 좋은지는 어느 정도 취향의 문제이고 나는 주로 줄바꿈을 함으로써 가독성이 높아진다고 생각하지만, 이 코드에서는 else if를 사용하기 위해 불가피한 선택이었다.

     

    2. 하드코딩된 switch문의 대안

    int min_rank;
    if (correct_num < 2) { min_rank = 6; }
    else { min_rank = aux_rank - correct_num; }

    가장 낮은 등수는 알아볼 수 없는 숫자들이 모두 틀렸다 가정했을 때이다. 6개 모두 맞추었을 때 1등부터 2개 맞추었을 때 5등으로 반비례하므로, 맞은 숫자의 개수를 7로부터 뺄셈하면 최소 등수를 구할 수 있다. 이때, 이 7의 의미를 알 수 있게 "보조 등수" 라는 상수로 대체했다.

    또한, 기존에 answer Vector를 사용하던 것과 다르게 최소 등수라는 것을 변수로 명시했다.

     

    3. max_rank 조건 명료화

    int max_rank;
    if (missing_num == 6) { max_rank = 1; }
    else { max_rank = min_rank - missing_num; }

     

    최종 답안

    vector<int> solution(vector<int> lottos, vector<int> win_nums) {
        int correct_num = 0;
        int missing_num = 0;
    
        for (int num : lottos) {
            if (num == 0) { missing_num++; }
            else if (find(win_nums.begin(), win_nums.end(), num) != win_nums.end()) { correct_num++; }
        }
    
        const int aux_rank = 7;
        int min_rank;
        if (correct_num < 2) { min_rank = 6; }
        else { min_rank = aux_rank - correct_num; }
    
        int max_rank;
        if (missing_num == 6) { max_rank = 1; }
        else { max_rank = min_rank - missing_num; }
        
        return { max_rank, min_rank };
    }

     

    vector의 순회 방법

    위의 for문 리팩토링을 할 때, vector를 순회하는 더 간단한 방법을 찾았다. 또 다른 순회 방법들은 뭐가 있는지, 그리고 그 중 어떤 방법이 가장 효율적인지 궁금증이 생겨 찾아보고 정리를 해보았다. 너무 과한 시간소모를 하고 싶지 않아 이 단락은 챗 GPT의 힘을 빌렸다.

    # 1. 범위 기반 for 루프 (Range-based for loop)

    #include <vector>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    for (int num : v) {
        // num을 사용
    }
    • 선호되는 경우: 가장 간단하고 가독성이 좋기 때문에, 요소를 단순히 순회하며 읽기 전용 작업을 할 때 매우 유용합니다.
    • 비선호되는 경우: 인덱스가 필요한 경우 또는 요소를 수정해야 하는 경우에는 적합하지 않을 수 있습니다. 또한, 요소를 참조로 받지 않는다면 복사가 발생할 수 있어 비효율적입니다.
    • 결론: 대부분의 경우 선호됩니다. 특히 요소의 값을 변경하지 않는 순회 작업에서 자주 사용됩니다.

    # 2. 전통적인 for 루프 (Indexed for loop)

    #include <vector>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    for (size_t i = 0; i < v.size(); ++i) {
        int num = v[i];
        // num을 사용
    }
    • 선호되는 경우: 인덱스가 필요한 작업이나, 순회 중에 벡터의 크기를 변경해야 하는 경우에 유용합니다. 요소를 수정하거나 참조로 접근할 수도 있습니다.
    • 비선호되는 경우: 단순히 순회만 할 경우에는 번거롭고 코드가 길어져 가독성이 떨어질 수 있습니다.
    • 결론: 인덱스가 필요한 경우에 사용되지만, 그렇지 않은 경우에는 범위 기반 for 루프나 다른 방법을 사용하는 것이 좋습니다.

    # 3. 반복자(Iterator)를 사용한 for 루프

    #include <vector>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
        int num = *it;
        // num을 사용
    }
    • 선호되는 경우: 벡터의 요소를 수정해야 하거나, 반복자 관련 기능(예: 요소 삭제)을 활용해야 할 때 유용합니다. 복잡한 컨테이너 조작을 할 때도 선호됩니다.
    • 비선호되는 경우: 단순 순회를 위한 경우에는 가독성이 떨어질 수 있습니다.
    • 결론: 요소를 수정하거나, 반복자에 특화된 작업이 필요할 때 사용됩니다. 일반적으로는 범위 기반 for 루프가 더 나은 선택입니다.

    # 4. 상수 반복자(Constant iterator)를 사용한 for 루프

    #include <vector>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    for (std::vector<int>::const_iterator it = v.cbegin(); it != v.cend(); ++it) {
        int num = *it;
        // num을 사용
    }
    • 선호되는 경우: 벡터 요소를 변경하지 않고 순회해야 하는 경우, 특히 반복자를 명시적으로 사용해야 할 때 유용합니다.
    • 비선호되는 경우: 단순히 읽기 전용 순회를 할 때는 범위 기반 for 루프가 더 적합합니다.
    • 결론: 반복자를 사용해야 하며, 요소를 변경하지 않는 경우에 사용됩니다. 그러나, 범위 기반 for 루프가 일반적으로 더 가독성이 좋습니다.

    # 5. while 루프와 반복자 사용

    #include <vector>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    std::vector<int>::iterator it = v.begin();
    while (it != v.end()) {
        int num = *it;
        // num을 사용
        ++it;
    }
    • 선호되는 경우: 특정 조건에서 루프를 중간에 종료하거나, 반복자의 증감이 일반적인 순회와 다를 때 유용할 수 있습니다.
    • 비선호되는 경우: 단순 순회에는 비효율적이며, 가독성이 떨어집니다.
    • 결론: 조건부 순회가 필요할 때 사용할 수 있으나, 대다수의 경우 다른 방법이 더 적합합니다. 잘 사용되지 않는 방법입니다.

    # 6. for_each 알고리즘 사용

    #include <vector>
    #include <algorithm>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    std::for_each(v.begin(), v.end(), [](int num) {
        // num을 사용
    });
    • 선호되는 경우: C++ 표준 라이브러리의 알고리즘 함수와 일관성을 유지하고, 람다를 통해 간결하게 요소를 처리할 때 유용합니다.
    • 비선호되는 경우: 인덱스가 필요하거나, 복잡한 로직이 포함된 순회를 하는 경우에는 비효율적일 수 있습니다.
    • 결론: 간단한 처리를 위한 함수형 접근법으로, 순수한 순회에는 좋지만 복잡한 로직이 필요할 때는 적합하지 않습니다.

     

    # 7. 반복자를 사용한 역방향 순회

    #include <vector>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    for (std::vector<int>::reverse_iterator it = v.rbegin(); it != v.rend(); ++it) {
        int num = *it;
        // num을 사용
    }
    • 선호되는 경우: 벡터의 요소를 역순으로 순회해야 할 때 사용됩니다.
    • 비선호되는 경우: 순방향 순회가 필요한 경우에는 비효율적입니다.
    • 결론: 역순으로 처리해야 하는 특수한 경우에 사용되며, 그 외에는 일반적인 순회 방법이 선호됩니다.

    # 8. 상수 반복자를 사용한 역방향 순회

    #include <vector>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    for (std::vector<int>::const_reverse_iterator it = v.crbegin(); it != v.crend(); ++it) {
        int num = *it;
        // num을 사용
    }

     

    • 선호되는 경우: 역순으로 순회하면서, 요소를 수정하지 않을 때 사용됩니다.
    • 비선호되는 경우: 역방향 순회 자체가 필요하지 않으면 사용되지 않습니다.
    • 결론: 역방향 읽기 전용 순회가 필요할 때 적합합니다. 역시 역순 처리가 필요한 특수한 경우에 사용됩니다.

    # 9. C++20의 std::ranges::for_each 사용

    #include <vector>
    #include <ranges>
    #include <algorithm>
    
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    std::ranges::for_each(v, [](int num) {
        // num을 사용
    });

     

    • 선호되는 경우: 최신 C++20 표준을 활용하며, 코드의 간결성과 가독성을 높이고자 할 때 사용됩니다.
    • 비선호되는 경우: C++20을 사용할 수 없는 환경에서는 사용이 불가능하며, 이전 표준에 비해 호환성 문제도 있을 수 있습니다.
    • 결론: 최신 C++ 표준을 지원하는 환경에서 함수형 프로그래밍 스타일을 선호할 때 사용됩니다. 그러나 C++20을 사용하지 않는 경우에는 사용하지 않습니다.

    최종 결론:

    • 범위 기반 for 루프는 대부분의 경우 가장 선호되는 방법입니다. 간결하고 직관적이며, 읽기 전용 작업에 적합합니다.
    • 전통적인 for 루프는 인덱스가 필요한 경우 또는 크기 조작이 필요한 경우에 유용합니다.
    • 반복자 기반 루프는 벡터의 요소를 수정하거나 복잡한 컨테이너 조작이 필요할 때 적합합니다.
    • 역방향 반복자는 특별히 역순으로 순회해야 하는 경우에만 사용됩니다.
    • std::for_each와 같은 알고리즘은 함수형 프로그래밍 스타일을 선호하거나 간단한 처리를 일관성 있게 처리할 때 유용합니다.

    다만, while 루프와 반복자를 사용하는 방식은 거의 사용되지 않으며, 대부분의 경우 다른 더 간결하고 가독성 높은 방법을 선택하는 것이 좋습니다.

    후기

    마지막으로 C++ 공부를 했던게 3월 중순인지라 정말 오랜만에 C++을 봐서 반갑기도 하고 어색하기도 하다. 그동안 Unity 개발 공부를 하면서 알고리즘 공부도 C#으로 풀고, 프로젝트도 C#으로만 진행했기 때문이다. 하지만 딱 VS Community로 새 프로젝트를 생성하자마자 #include... 부터 나오는걸 보니 습관이 무섭다는 생각이 들었다. 비록 using문을 한 번 빠트리기는 했지만 말이다...

     

    C#은 자동완성을 방지하기 위해 일단 Programmers 웹사이트에서 문제를 풀고 VS로 한 번 옮겨와 오타 검수를 한 후 제출을 해왔는데 이번에는 C++이 너무 오래간만인데다 원래도 잘 안써봤던 vector를 사용해 문제를 풀어야 하다보니 이번 문제는 처음부터 vs community로 풀었고, 자동완성 덕분인지는 몰라도 테스트할 때 맨 위에 함수 선언하는 것을 빠트린 것과 프로그래머스 사이트에 복붙할 때 #include문의 복사를 빼먹은 것을 제외하고는 한 번에 문제를 맞출 수 있었다.