내배캠/C++

온라인 학습 관리 시스템 구현

동그래님 2025. 2. 5. 21:43

 

 

✅구현 요건

더보기

📌개요

  • 최소 2개 이상의 STL 컨테이너를 조합하여 프로그램을 구현할 것.
  • 실용적인 데이터 처리를 수행하는 프로그램을 작성할 것.
  • 코드의 설계 의도를 발표하고, 컨테이너 선택 이유를 설명할 것.

📌문제

어떤 온라인 학습 플랫폼에서 학생들의 성적을 관리하는 시스템을 만든다고 가정한다. 이 시스템은 다음과 같은 기능을 포함해야 한다.
  1. 학생 성적 추가 기능
    • 학생 ID(int)와 과목 이름(string), 점수(int)를 저장한다.
    • 한 학생은 여러 과목을 수강할 수 있다.
      • (예: "1001번 학생이 'C++' 과목에서 85점, '알고리즘'에서 90점을 받음")
    • 동일 학생의 동일 과목을 입력하는 경우엔 최신 점수로 갱신을 한다.
    • 점수는 0 ~ 100점까지만 유효한 범위
  2. 학생의 전체 성적 조회 기능
    • 특정 학생의 모든 과목 성적을 출력한다.
      • (예: "1001번 학생: C++(85점), 알고리즘(90점)")
    • 과목명은 알파벳 순으로 정렬하여 출력한다.
    • 또한, 존재하지 않는 학생 ID 입력 시 예외 처리를 하도록 한다.
  3. 전체 학생의 평균 점수 출력 기능
    • 전체 학생들의 각 과목별 평균 점수를 출력한다.
    • 평균 점수는 소수점 둘째 자리까지 표시한다.
      • (예: "C++ 과목 평균 점수: 87.5, 알고리즘 과목 평균 점수: 92.3")
  4. 과목별 최고 점수 학생 조회 기능
    • 특정 과목에서 가장 높은 점수를 받은 학생들을 찾는다.
    • 동점자가 있을 경우 학생 ID를 오름차순으로 정렬해서 모두 출력한다.
      • (예: "알고리즘 최고 점수: 95점 (학생 ID: 1003, 1005)")

이외에 여유가 되면 다음과 같은 기능들을 구현해보도록 한다.

  1. 성적 구간 검색 기능
    • 특정 과목에서 지정된 점수 구간(이상, 이하)에 해당하는 학생 목록을 출력한다.
  2. 과목별 성적 통계 기능
    • 특정 과목의 최고/최저 평균 점수 및 수강 인원을 출력한다.

 

📌구현 요구 사항

  • 학생 ID를 기준으로 데이터를 효율적으로 관리할 것.
  • 과목별 성적을 효율적으로 조회할 수 있도록 설계할 것.
  • map, multimap, vector, set, multiset 등의 컨테이너를 조합하여 사용할 것.
  • 적절한 STL 알고리즘(sort, find_if, accumulate 등)을 활용할 것.

 

 

 

 

✅구현 코드 및 결과

더보기
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <numeric> // accumulate
#include <algorithm> // max_element
#include <iomanip> // fixed, setprecision

using namespace std;

class ScoreManager
{
private:
    // Id를 기반으로 과목과 점수 저장 리스트
    multimap<int, pair<string, int>> ScoreList;
    // 과목을 기반으로 점수와 Id 저장 리스트
    map<string, vector<pair<int, int>>> SubjectScores;
public:
    // 성적 입력 기능
    void InitScore(int Id, string Subject, int Score);

    // 학생의 전체 성적 조회 기능
    void DisplayStudentScores(int Id);

    // 전체 학생의 평균 점수 출력 기능
    void DisplayAverageScore();

    // 과목별 최고 점수 학생 조회 기능
    void DisplayTopScoreStudent(string Subject);

    // 성적 구간 검색 기능
    void DisplayStudentsInScoreRange(const string& Subject, const int& MinScore, const int& MaxScore);
};

void ScoreManager::InitScore(int Id, string Subject, int Score)
{
    ScoreList.insert({ Id, {Subject, Score} });
    SubjectScores[Subject].push_back({ Id, Score });
}

void ScoreManager::DisplayStudentScores(int Id)
{
    if (ScoreList.empty())
    {
        cout << "저장되어 있는 데이터가 없습니다." << endl;
        return;
    }

    cout << "학생 ID " << to_string(Id) << "의 성적:" << endl;
    
    map<string, int> SortedList;
    auto Range = ScoreList.equal_range(Id);
    for (auto It = Range.first; It != Range.second; ++It)
    {
        SortedList[It->second.first] = It->second.second;
    }

    for (auto It = SortedList.begin(); It != SortedList.end(); ++It)
    {
        cout << "- " << It->first << ": " << It->second << "점" << endl;
    }

    cout << endl;
}

void ScoreManager::DisplayAverageScore()
{
    if (SubjectScores.empty())
    {
        cout << "저장되어 있는 데이터가 없습니다." << endl;
        return;
    }

    cout << "전체 과목 평균 점수:" << endl;

    for (auto It = SubjectScores.begin(); It != SubjectScores.end(); ++It)
    {
        string Subject = It->first;
        vector<pair<int,int>> ScoreList = It->second;
        int TotalScore = accumulate(ScoreList.begin(), ScoreList.end(), 0, [](int acc, const auto& p)
            {
                return acc + p.second;
            });
        
        float AverageScore = static_cast<float>(TotalScore) / ScoreList.size();
        cout << "- " << Subject << ": " << fixed << setprecision(2) << AverageScore << "점" << endl;
    }

    cout << endl;
}

void ScoreManager::DisplayTopScoreStudent(string Subject)
{
    if (SubjectScores.find(Subject) == SubjectScores.end())
    {
        cout << Subject + " 과목에 대한 데이터가 없습니다." << endl;
        return;
    }
    
    auto Top = max_element(SubjectScores[Subject].begin(), SubjectScores[Subject].end(), [](const auto& a, const auto& b)
        {
            return a.second < b.second;
        });
 
    cout << Subject << " 최고 점수: " << Top->second << "점" << endl;
    cout << "- 학생 ID: " << Top->first << endl << endl;
}

void ScoreManager::DisplayStudentsInScoreRange(const string& Subject, const int& MinScore, const int& MaxScore)
{
    vector<pair<int,int>> ScoreList = SubjectScores[Subject];
    set<int> IdList;
    
    for (auto& Score : ScoreList)
    {
        if (MinScore <= Score.second && Score.second <= MaxScore)
        {
            IdList.insert(Score.first);
        }
    }

    cout << Subject << "과목 " << to_string(MinScore) << "점 ~ " << to_string(MaxScore) << "점의 점수를 가진 학생:" << endl;

    for (int Id : IdList)
    {
        cout << "- 학생 ID: " << Id << endl;
    }
}

int main()
{
    ScoreManager SM;
    
    SM.InitScore(1001, "C++", 85);
    SM.InitScore(1001, "알고리즘", 90);
    SM.InitScore(1002, "C++", 92);
    SM.InitScore(1002, "알고리즘", 78);
    SM.InitScore(1003, "C++", 95);
    SM.InitScore(1003, "알고리즘", 95);

    SM.DisplayStudentScores(1001);
    SM.DisplayAverageScore();
    SM.DisplayTopScoreStudent("알고리즘");
    SM.DisplayStudentsInScoreRange("C++", 90, 100);

    return 0;
}

 

 

 

📍 multimap과 map의 사용

한 학생이 여러 과목을 수강할 수 있기에 multimap 컨테이너를 선택해 데이터를 저장하였고, 전체 과목 평균 점수를 구한다던가 한 과목에서 최고 점수를 기록한 학생을 찾는 등의 검색이 빠를 수 있도록 Subject를 key 값으로 데이터를 저장하는 map 컨테이너를 추가적으로 사용해서 전체 데이터를 관리하였다.

 // Id를 기반으로 과목과 점수 저장 리스트
 multimap<int, pair<string, int>> ScoreList;
 // 과목을 기반으로 점수와 Id 저장 리스트
 map<string, vector<pair<int, int>>> SubjectScores;

 

 

📍 accumulate의 사용

전체 과목 평균을 구해 출력하는 DisplayAverageScore 함수에서 과목의 점수 합산을 할 때 사용하여, 코드가 간결해지도록 하였다.

 string Subject = It->first;
 vector<pair<int,int>> ScoreList = It->second;
 int TotalScore = accumulate(ScoreList.begin(), ScoreList.end(), 0, [](int acc, const auto& p)
     {
         return acc + p.second;
     });

 

 

📍 max_element의 사용

DisplayTopScoreStudent 함수에서 과목 점수 중 가장 높은 점수를 찾는데에 max_element 메서드를 사용했다.
vector<pair<int, int>>형태에서 최대 값을 찾는 것이기에 람다 함수를 사용해 두 번째 값을 기준으로 최대 값을 찾도록 하였다.

auto Top = max_element(SubjectScores[Subject].begin(), SubjectScores[Subject].end(), [](const auto& a, const auto& b)
        {
            return a.second < b.second;
        });
람다 함수의 비교연산자를 작성할 때, 최대 값이 나오지 않는 결과가 계속 생겨서 헷갈렸던 부분이 있었다.

return a > b를 해야 최대 값을 갱신한다고 생각했는데, 오히려 반대였다. 결론적으로 return a < b를 해야했고 이는 min_element에서도 같은 조건을 사용해야된다.

max_element: 비교 결과가 true일 때, 현재 값(b)이 더 크다고 간주하고 갱신한다.
min_element: 비교 결과가 true일 때, 현재 값 (a)이 더 작다고 간주하고 그대로 유지한다.

둘 다 같은 비교연산자를 사용하지만, 기준이 되는 값이 다르므로 해석 방식의 차이로 인해 다른 결과를 도출하게 된다.

 

 

📍 fixed와 setprecision의 사용

fixed는 소수점을 기준으로 소수점 이하의 자릿수를 고정하는 역할을 한다.
setprecision은 숫자를 출력할 때 n개의 유효 자릿수(정수부 + 소수부)를 출력도록 설정한다.

fixed와 setprecision을 함께 사용하여 소수점 아래 자릿수를 고정하였다.
이 둘은 iomanip 헤더에 포함된 조정자이다.

cout << "- " << Subject << ": " << fixed << setprecision(2) << AverageScore << "점" << endl;