C++ STL 알고리즘 기초 완벽 가이드 | sort·find

C++ STL 알고리즘 기초 완벽 가이드 | sort·find

이 글의 핵심

C++ STL 알고리즘 기초 입니다. sort, find, transform, accumulate 등 핵심 알고리즘의 사용법과 성능 특성을 실전 예제로 설명합니다. 데이터 처리 코드를 작성할 때마다 직접 for문을 돌리다 보면 이런 일이 반복됩니다: "인덱스 범위를 잘못 써서 Segmentation fault가 나요." "정렬할 때 같은 값 처리 로직을 빼먹었어요.

들어가며: “for문 작성하다 보니 버그가 자꾸 생겨요”

실제 겪는 문제 시나리오

데이터 처리 코드를 작성할 때마다 직접 for문을 돌리다 보면 이런 일이 반복됩니다:

"인덱스 범위를 잘못 써서 Segmentation fault가 나요."
"정렬할 때 같은 값 처리 로직을 빼먹었어요."
"값을 찾을 때 존재하지 않으면 end() 체크를 깜빡했어요."
"조건에 맞는 원소를 제거하려다 반복자 무효화로 크래시가 났어요."
"합계·평균 계산할 때 초기값 타입을 잘못 넣어서 부동소수점 오차가 나요."

STL 알고리즘으로 해결:

문제STL 해결
인덱스 오류[begin, end) 반복자로 범위 명시, 경계 검증된 구현 사용
정렬 실수std::sort, std::stable_sort — 검증된 O(n log n) 구현
검색 실수std::find, std::find_if — end 체크 패턴 통일
개수 세기std::count, std::count_if — 한 줄로 집계
변환 실수std::transform — 선언적 변환, 출력 범위만 확보
복사 실수std::copy, std::copy_if — 범위 기반 복사
제거 실수std::remove + erase — erase-remove idiom

요구 환경: C++17 이상 권장

이 글을 읽으면:

  • sort, find, count, transform, accumulate, copy, remove를 완전히 이해할 수 있습니다.
  • 문제 상황별 적절한 알고리즘을 선택할 수 있습니다.
  • 흔한 에러를 피하고 베스트 프랙티스를 적용할 수 있습니다.
  • 프로덕션 수준의 패턴을 사용할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 문제 시나리오 상세
  2. 정렬: sort
  3. 검색: find
  4. 개수: count
  5. 변환: transform
  6. 집계: accumulate
  7. 복사: copy
  8. 제거: remove
  9. 완전한 예제 모음
  10. 자주 발생하는 에러와 해결법
  11. 베스트 프랙티스
  12. 프로덕션 패턴
  13. 구현 체크리스트

1. 문제 시나리오 상세

시나리오 1: 대량 데이터 정렬 시 성능·정확성 문제

상황: 수십만 건의 로그를 타임스탬프 기준으로 정렬해야 합니다. 직접 버블 정렬을 구현하면 O(n²)로 시간 초과가 발생하고, 같은 값의 순서를 잘못 처리하기 쉽습니다.

해결: std::sort는 인트로소트(퀵소트 + 힙소트 + 삽입정렬)로 O(n log n)에 정렬합니다. 같은 값의 순서가 중요하면 std::stable_sort를 사용합니다.

시나리오 2: 컨테이너에서 값 검색 시 end 체크 누락

상황: ID 목록에서 특정 ID가 있는지 확인하고, 있으면 인덱스를 사용합니다. for문으로 돌리다 vec[i] == target 체크 후 i를 사용하는데, 없을 때 -1이나 size()를 반환하는 로직을 빼먹기 쉽습니다.

해결: std::find는 “찾으면 반복자, 없으면 end”를 반환합니다. it != vec.end()로 존재 여부를 일관되게 확인할 수 있습니다.

시나리오 3: 조건 만족 개수 세기

상황: “에러 로그가 몇 개인지”, “80점 이상이 몇 명인지” 등을 세어야 합니다. 수동 카운터를 쓰면 초기화를 빼먹거나, 다른 변수와 섞이기 쉽습니다.

해결: std::count, std::count_if로 한 줄에 집계합니다.

시나리오 4: 데이터 변환 파이프라인

상황: JSON 파싱 결과를 DTO로 변환하거나, 센서 값을 정규화하는 등 “각 원소에 함수 적용”이 반복됩니다. for문은 가독성이 떨어지고 인덱스 실수가 납니다.

해결: std::transform으로 변환 로직을 선언적으로 표현합니다.

시나리오 5: 합계·평균·곱셈 등 집계

상황: 벡터의 합, 곱, 또는 문자열 리스트를 하나로 연결하는 “접기(fold)” 연산이 필요합니다. 수동 루프는 초기값 처리나 타입 변환에서 실수하기 쉽습니다.

해결: std::accumulate로 초기값부터 순차적으로 이항 함수를 적용해 집계합니다.

시나리오 6: 조건에 맞는 원소만 복사

상황: “양수만”, “에러 레벨만” 등 조건을 만족하는 원소만 새 컨테이너로 복사해야 합니다. for문으로 push_back하다 범위 오류가 나기 쉽습니다.

해결: std::copy_if + std::back_inserter로 한 줄에 처리합니다.

시나리오 7: 조건에 맞는 원소 제거

상황: “짝수 제거”, “만료된 항목 제거” 등입니다. 반복문 안에서 erase를 호출하면 반복자 무효화로 크래시가 납니다.

해결: std::remove_if + vec.erase 조합(erase-remove idiom)으로 안전하게 제거합니다.

STL 알고리즘 분류 다이어그램

flowchart TB
    subgraph sort[정렬]
        S1[sort]
        S2[stable_sort]
        S3[partial_sort]
    end
    subgraph search[검색]
        F1[find]
        F2[find_if]
        F3[find_if_not]
    end
    subgraph count[개수]
        C1[count]
        C2[count_if]
    end
    subgraph transform[변환]
        T1[transform]
    end
    subgraph agg[집계]
        A1[accumulate]
    end
    subgraph copy[복사]
        CP1[copy]
        CP2[copy_if]
    end
    subgraph remove[제거]
        R1[remove]
        R2[remove_if]
    end
    input["[begin, end) 반복자"] --> sort
    input --> search
    input --> count
    input --> transform
    input --> agg
    input --> copy
    input --> remove

2. 정렬: sort

std::sort: 기본 정렬

std::sort반개구간 [begin, end) 에 있는 원소를 기본적으로 오름차순으로 정렬합니다. 내부적으로 인트로소트를 사용하며, 비교 연산만 정의되어 있으면 어떤 타입이든 정렬할 수 있습니다.

// g++ -std=c++17 -o sort_basic sort_basic.cpp && ./sort_basic
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
    std::sort(vec.begin(), vec.end());
    for (int x : vec) std::cout << x << " ";  // 1 2 3 5 7 8 9
    std::cout << "\n";

    // 내림차순
    std::sort(vec.begin(), vec.end(), std::greater<int>());
    for (int x : vec) std::cout << x << " ";  // 9 8 7 5 3 2 1
    std::cout << "\n";
    return 0;
}

주의: std::sort같은 값끼리의 원래 순서를 보장하지 않습니다. 같은 값의 순서가 중요하면 std::stable_sort를 사용하세요.

커스텀 비교자

구조체나 클래스를 정렬할 때는 비교 함수로 기준을 넘깁니다. 람다 { return a.x < b.x; }는 “a가 b보다 앞에 오려면 a.x < b.x”를 의미합니다.

#include <algorithm>
#include <vector>
#include <string>

struct LogEntry {
    std::string timestamp;
    int level;
    std::string message;
};

int main() {
    std::vector<LogEntry> logs = {
        {"2026-04-23 10:00:00", 2, "Warning"},
        {"2026-04-23 09:00:00", 1, "Info"},
        {"2026-04-23 10:00:00", 1, "Error"}
    };

    // 타임스탬프 오름차순, 같으면 level 오름차순
    std::sort(logs.begin(), logs.end(),
         {
            if (a.timestamp != b.timestamp)
                return a.timestamp < b.timestamp;
            return a.level < b.level;
        });
    return 0;
}

stable_sort: 안정 정렬

같은 값끼리 원래 입력 순서를 유지해야 할 때 사용합니다.

#include <algorithm>
#include <vector>

struct Student {
    std::string name;
    int score;
};

int main() {
    std::vector<Student> students = {
        {"Alice", 90}, {"Bob", 85}, {"Charlie", 90}, {"David", 85}
    };

    std::stable_sort(students.begin(), students.end(),
         { return a.score > b.score; });
    // Alice(90), Charlie(90), Bob(85), David(85) - 같은 점수 내 순서 유지
    return 0;
}

partial_sort: 상위 k개만 정렬

전체를 정렬할 필요 없이 “상위 N개만 올바른 순서”가 필요할 때 사용합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};

    // 상위 3개만 정렬 (가장 큰 3개)
    std::partial_sort(vec.begin(), vec.begin() + 3, vec.end(),
                      std::greater<int>());

    for (int i = 0; i < 3; ++i)
        std::cout << vec[i] << " ";  // 9 8 7
    std::cout << "\n";
    return 0;
}

3. 검색: find

std::find: 값으로 검색

std::find선형 검색으로 범위 내에서 value와 같은 첫 번째 원소의 반복자를 반환합니다. 없으면 end를 반환합니다. O(n)이지만 정렬이 필요 없습니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};

    auto it = std::find(vec.begin(), vec.end(), 8);
    if (it != vec.end()) {
        std::cout << "Found at index: " << std::distance(vec.begin(), it) << "\n";  // 2
    } else {
        std::cout << "Not found\n";
    }

    // 없는 값
    auto it2 = std::find(vec.begin(), vec.end(), 100);
    if (it2 == vec.end()) {
        std::cout << "100 not found\n";
    }
    return 0;
}

std::find_if: 조건으로 검색

조건(술어)을 만족하는 첫 번째 원소의 반복자를 반환합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};

    // 5보다 큰 첫 번째 원소
    auto it = std::find_if(vec.begin(), vec.end(),
         { return x > 5; });
    if (it != vec.end()) {
        std::cout << "First > 5: " << *it << "\n";  // 8
    }

    // 짝수인 첫 번째 원소
    auto it2 = std::find_if(vec.begin(), vec.end(),
         { return x % 2 == 0; });
    if (it2 != vec.end()) {
        std::cout << "First even: " << *it2 << "\n";  // 2
    }
    return 0;
}

std::find_if_not: 조건을 만족하지 않는 첫 원소

조건을 만족하지 않는 첫 번째 원소의 반복자를 반환합니다.

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> vec = {2, 4, 6, 7, 8, 10};

    // 홀수인 첫 번째 원소
    auto it = std::find_if_not(vec.begin(), vec.end(),
         { return x % 2 == 0; });
    if (it != vec.end()) {
        // *it == 7
    }
    return 0;
}

4. 개수: count

std::count: 값과 같은 원소 개수

범위 내에서 value와 같은 원소의 개수를 반환합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 2, 2, 3, 4, 2, 5};

    size_t n = std::count(vec.begin(), vec.end(), 2);
    std::cout << "Count of 2: " << n << "\n";  // 4

    n = std::count(vec.begin(), vec.end(), 10);
    std::cout << "Count of 10: " << n << "\n";  // 0
    return 0;
}

std::count_if: 조건 만족 개수

조건(술어)을 만족하는 원소의 개수를 반환합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};

    // 5보다 큰 원소 개수
    size_t n = std::count_if(vec.begin(), vec.end(),
         { return x > 5; });
    std::cout << "Count > 5: " << n << "\n";  // 3

    // 짝수 개수
    n = std::count_if(vec.begin(), vec.end(),
         { return x % 2 == 0; });
    std::cout << "Even count: " << n << "\n";  // 2
    return 0;
}

5. 변환: transform

std::transform: 단항 변환

범위의 각 원소에 함수(람다)를 적용한 결과를 출력 범위에 씁니다. 출력을 입력과 같은 범위로 주면 제자리(in-place) 변환이 됩니다.

#include <algorithm>
#include <vector>
#include <cmath>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::vector<int> result(vec.size());

    // 각 원소 2배
    std::transform(vec.begin(), vec.end(), result.begin(),
                   { return x * 2; });
    // result: {2, 4, 6, 8, 10}

    // 제자리 변환 (원본 수정)
    std::transform(vec.begin(), vec.end(), vec.begin(),
                   { return x * 2; });
    // vec: {2, 4, 6, 8, 10}

    // 실수 변환 (sqrt)
    std::vector<double> values = {1.0, 4.0, 9.0, 16.0};
    std::vector<double> roots(values.size());
    std::transform(values.begin(), values.end(), roots.begin(),
                   { return std::sqrt(x); });
    // roots: {1, 2, 3, 4}
    return 0;
}

std::transform: 두 범위 결합 (이항 변환)

두 시퀀스의 같은 위치 원소를 한 번에 받아 결과를 출력합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> a = {1, 2, 3, 4, 5};
    std::vector<int> b = {10, 20, 30, 40, 50};
    std::vector<int> sum(a.size());

    std::transform(a.begin(), a.end(), b.begin(), sum.begin(),
                   { return x + y; });
    // sum: {11, 22, 33, 44, 55}
    return 0;
}

6. 집계: accumulate

std::accumulate: 기본 동작

std::accumulate왼쪽부터 차례로 접어 나가는(fold) 연산입니다. 세 번째 인자는 초기값이고, 기본 동작은 초기값 + vec[0] + vec[1] + ...입니다. <numeric> 헤더가 필요합니다.

flowchart LR
    I["초기값 0"] --> A1["+ vec[0]"]
    A1 --> A2["+ vec[1]"]
    A2 --> A3["+ vec[2]"]
    A3 --> R["최종 결과"]
#include <numeric>
#include <vector>
#include <string>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    int sum = std::accumulate(vec.begin(), vec.end(), 0);
    std::cout << "Sum: " << sum << "\n";  // 15

    int product = std::accumulate(vec.begin(), vec.end(), 1,
         { return a * b; });
    std::cout << "Product: " << product << "\n";  // 120

    std::vector<std::string> words = {"Hello", " ", "World"};
    std::string concat = std::accumulate(words.begin(), words.end(), std::string(),
         { return a + b; });
    std::cout << concat << "\n";  // "Hello World"
    return 0;
}

accumulate로 평균, 분산 계산

#include <numeric>
#include <vector>
#include <cmath>

double mean(const std::vector<double>& vec) {
    if (vec.empty()) return 0;
    double sum = std::accumulate(vec.begin(), vec.end(), 0.0);
    return sum / vec.size();
}

double variance(const std::vector<double>& vec) {
    if (vec.size() < 2) return 0;
    double m = mean(vec);
    double sq_diff = std::accumulate(vec.begin(), vec.end(), 0.0,
        [m](double a, double x) { return a + (x - m) * (x - m); });
    return sq_diff / (vec.size() - 1);
}

7. 복사: copy

std::copy: 전체 복사

[first, last) 범위의 원소를 출력 반복자로 복사합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst(src.size());

    std::copy(src.begin(), src.end(), dst.begin());
    // dst: {1, 2, 3, 4, 5}

    // back_inserter로 동적 추가
    std::vector<int> dst2;
    dst2.reserve(src.size());
    std::copy(src.begin(), src.end(), std::back_inserter(dst2));
    return 0;
}

std::copy_if: 조건 만족 원소만 복사

조건(술어)을 만족하는 원소만 출력 범위로 복사합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> even;

    std::copy_if(src.begin(), src.end(), std::back_inserter(even),
         { return x % 2 == 0; });
    // even: {2, 4, 6, 8, 10}

    // reserve로 재할당 최소화
    std::vector<int> result;
    result.reserve(src.size());
    std::copy_if(src.begin(), src.end(), std::back_inserter(result),
         { return x > 5; });
    // result: {6, 7, 8, 9, 10}
    return 0;
}

std::copy_n: n개만 복사

처음 n개 원소만 복사합니다.

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst(3);

    std::copy_n(src.begin(), 3, dst.begin());
    // dst: {1, 2, 3}
    return 0;
}

8. 제거: remove

remove의 동작 방식 (주의)

std::removestd::remove_if실제로 원소를 삭제하지 않습니다. 제거할 값들을 뒤로 밀어내고, “새 논리적 끝” 반복자를 반환합니다. 물리적 크기는 그대로이므로, 반드시 erase와 함께 사용해야 합니다.

flowchart LR
    subgraph before[remove 전]
        B1[1] --> B2[2] --> B3[2] --> B4[3] --> B5[2]
    end
    subgraph after["remove 후 (erase 전)"]
        A1[1] --> A2[3] --> A3[?] --> A4[?] --> A5[?]
    end

erase-remove idiom

조건에 맞는 원소를 실제로 제거하려면 remove_if의 반환값을 erase에 넘깁니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};

    // 값 2 제거
    vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
    // vec: {1, 3, 4, 5}

    // 짝수 제거
    std::vector<int> vec2 = {1, 2, 3, 4, 5, 6};
    vec2.erase(std::remove_if(vec2.begin(), vec2.end(),
         { return x % 2 == 0; }), vec2.end());
    // vec2: {1, 3, 5}
    return 0;
}

std::remove: 값으로 제거

지정한 값과 같은 모든 원소를 “제거”하고, 새 끝 반복자를 반환합니다.

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 2, 3, 2, 4};
    auto new_end = std::remove(vec.begin(), vec.end(), 2);
    vec.erase(new_end, vec.end());
    // vec: {1, 3, 4}
    return 0;
}

std::remove_if: 조건으로 제거

조건을 만족하는 모든 원소를 제거합니다.

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 5보다 큰 원소 제거
    vec.erase(std::remove_if(vec.begin(), vec.end(),
         { return x > 5; }), vec.end());
    // vec: {1, 2, 3, 4, 5}
    return 0;
}

9. 완전한 예제 모음

예제 1: 로그 분석기 (sort + find + count + copy + accumulate)

// g++ -std=c++17 -o log_analyzer log_analyzer.cpp && ./log_analyzer
#include <algorithm>
#include <numeric>
#include <vector>
#include <string>
#include <iostream>

struct LogEntry {
    std::string timestamp;
    int level;  // 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR
    std::string message;
};

int main() {
    std::vector<LogEntry> logs = {
        {"2026-04-23 10:00:00", 2, "Connection timeout"},
        {"2026-04-23 09:55:00", 1, "User login"},
        {"2026-04-23 10:01:00", 3, "Disk full"},
        {"2026-04-23 09:58:00", 1, "Request received"},
        {"2026-04-23 10:00:30", 2, "Retry attempt"}
    };

    // 1. sort: 타임스탬프 오름차순 정렬
    std::sort(logs.begin(), logs.end(),
         {
            return a.timestamp < b.timestamp;
        });

    // 2. count_if: ERROR(3) 개수
    int error_count = std::count_if(logs.begin(), logs.end(),
         { return e.level == 3; });
    std::cout << "Errors: " << error_count << "\n";

    // 3. copy_if: WARN 이상만 필터링
    std::vector<LogEntry> critical;
    std::copy_if(logs.begin(), logs.end(), std::back_inserter(critical),
         { return e.level >= 2; });

    // 4. find_if: 첫 ERROR 찾기
    auto it = std::find_if(logs.begin(), logs.end(),
         { return e.level == 3; });
    if (it != logs.end()) {
        std::cout << "First error: " << it->message << "\n";
    }

    // 5. accumulate: level 합계 (평균 레벨 계산용)
    int level_sum = std::accumulate(logs.begin(), logs.end(), 0,
         { return a + e.level; });
    double avg_level = static_cast<double>(level_sum) / logs.size();
    std::cout << "Avg level: " << avg_level << "\n";

    return 0;
}

실행 결과:

Errors: 1
First error: Disk full
Avg level: 1.8

예제 2: 점수 처리 파이프라인 (transform + sort + find + remove)

#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> scores = {72, 85, 91, 68, 78, 88, 95, 62, -1, 0};

    // 1. remove_if: 유효하지 않은 점수(-1, 0) 제거
    scores.erase(std::remove_if(scores.begin(), scores.end(),
         { return s <= 0; }), scores.end());

    // 2. transform: 정규화 (0-100 -> 0.0-1.0)
    std::vector<double> normalized(scores.size());
    std::transform(scores.begin(), scores.end(), normalized.begin(),
         { return s / 100.0; });

    // 3. sort: 상위 5명 (partial_sort)
    std::partial_sort(scores.begin(), scores.begin() + 5, scores.end(),
                     std::greater<int>());

    // 4. find: 80점인 사람이 있는지
    auto it = std::find(scores.begin(), scores.end(), 80);
    bool has_80 = (it != scores.end());

    // 5. accumulate: 평균
    double avg = std::accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
    std::cout << "Average: " << avg << "\n";

    return 0;
}

예제 3: 두 벡터 결합 및 집계 (transform + accumulate)

#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> prices = {100, 200, 150, 300};
    std::vector<int> quantities = {2, 1, 3, 2};

    // transform: 각 품목별 금액 = price * quantity
    std::vector<int> amounts(prices.size());
    std::transform(prices.begin(), prices.end(), quantities.begin(),
                  amounts.begin(),  { return p * q; });
    // amounts: {200, 200, 450, 600}

    // accumulate: 총 금액
    int total = std::accumulate(amounts.begin(), amounts.end(), 0);
    std::cout << "Total: " << total << "\n";  // 1450

    return 0;
}

10. 자주 발생하는 에러와 해결법

에러 1: find 결과를 end 체크 없이 사용

증상: 찾지 못했을 때 *it 접근으로 Segmentation fault 또는 undefined behavior.

원인: std::findend를 반환했는데, 존재한다고 가정하고 역참조함.

// ❌ 잘못된 사용
auto it = std::find(vec.begin(), vec.end(), 100);
int value = *it;  // 100이 없으면 UB!

// ✅ 올바른 사용
auto it = std::find(vec.begin(), vec.end(), 100);
if (it != vec.end()) {
    int value = *it;
} else {
    // 없을 때 처리
}

에러 2: transform 출력 범위 크기 부족

증상: Segmentation fault 또는 메모리 손상.

원인: 출력 범위가 입력보다 작으면 범위를 벗어나 씁니다.

// ❌ 잘못된 사용
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int> result(2);  // 크기 부족!
std::transform(vec.begin(), vec.end(), result.begin(),  { return x * 2; });

// ✅ 올바른 사용
std::vector<int> result(vec.size());
std::transform(vec.begin(), vec.end(), result.begin(),  { return x * 2; });

// ✅ 또는 back_inserter + reserve
std::vector<int> result;
result.reserve(vec.size());
std::transform(vec.begin(), vec.end(), std::back_inserter(result),
               { return x * 2; });

에러 3: accumulate 초기값 타입 불일치

증상: 정수 합계는 괜찮은데, 부동소수점 합계가 부정확해짐.

원인: std::accumulate(vec.begin(), vec.end(), 0)에서 0은 int이므로, double 벡터를 합할 때 매 단계 int로 변환됩니다.

// ❌ 잘못된 사용 (double 벡터에 int 초기값)
std::vector<double> vec = {0.1, 0.2, 0.3};
double sum = std::accumulate(vec.begin(), vec.end(), 0);  // int 0 -> 부정확

// ✅ 올바른 사용
double sum = std::accumulate(vec.begin(), vec.end(), 0.0);  // 0.0

에러 4: remove만 하고 erase 안 함

증상: 벡터 크기는 그대로인데, 뒤쪽에 “쓰레기” 값이 남아 있음.

원인: remove/remove_if는 원소를 삭제하지 않고, 새 논리적 끝만 반환합니다.

// ❌ 잘못된 사용
std::vector<int> vec = {1, 2, 2, 3};
std::remove(vec.begin(), vec.end(), 2);  // vec 크기 그대로, 뒤에 쓰레기

// ✅ 올바른 사용 (erase-remove idiom)
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());

에러 5: 비교자에서 strict weak ordering 위반

증상: std::sort 호출 시 크래시 또는 무한 루프.

원인: 비교 함수가 a < a가 false, a <= b처럼 <=를 쓰면 a == b일 때 true가 되어 위반.

// ❌ 잘못된 비교자
std::sort(vec.begin(), vec.end(),
     { return a <= b; });  // a <= a -> true (위반)

// ✅ 올바른 비교자 (< 만 사용)
std::sort(vec.begin(), vec.end(),
     { return a < b; });

에러 6: copy_if에서 출력 범위 reserve 누락

증상: 대량 데이터에서 copy_if + back_inserter 사용 시 재할당이 반복되어 느려짐.

해결: 결과 크기를 대략 알 수 있으면 reserve로 재할당을 줄입니다.

// ✅ reserve로 재할당 최소화
std::vector<int> result;
result.reserve(src.size());  // 최대 src.size()개
std::copy_if(src.begin(), src.end(), std::back_inserter(result),
              { return x % 2 == 0; });

에러 7: empty 범위에서 accumulate 후 나눗셈

증상: vec.empty()일 때 accumulate 결과가 초기값인데, vec.size()로 나누면 0으로 나누기.

// ✅ empty 체크
double avg = vec.empty()
    ? 0.0
    : std::accumulate(vec.begin(), vec.end(), 0.0) / vec.size();

11. 베스트 프랙티스

1. for문 대신 알고리즘 우선

반복문으로 “찾기, 세기, 복사, 제거”를 직접 구현하기 전에, STL에 해당 알고리즘이 있는지 확인합니다. 가독성과 안정성이 좋아집니다.

// ❌ 수동 루프
int count = 0;
for (size_t i = 0; i < vec.size(); ++i) {
    if (vec[i] > 5) ++count;
}

// ✅ count_if
int count = std::count_if(vec.begin(), vec.end(),  { return x > 5; });

2. 람다 비교자 사용 (std::function 피하기)

std::function을 넘기면 간접 호출 비용이 생깁니다. 람다는 컴파일러가 인라인하기 쉽습니다.

// ✅ 람다 (인라인 가능)
std::sort(vec.begin(), vec.end(),  { return a < b; });

// ❌ std::function (간접 호출)
std::function<bool(int,int)> cmp =  { return a < b; };
std::sort(vec.begin(), vec.end(), cmp);

3. reserve로 재할당 최소화

back_inserter와 함께 쓸 때, 결과 크기를 대략 알 수 있으면 reserve를 호출합니다.

std::vector<int> result;
result.reserve(vec.size());
std::copy_if(vec.begin(), vec.end(), std::back_inserter(result),
              { return x > 0; });

4. 정렬이 필요할 때만 정렬

한 번만 검색할 거면 std::find(O(n))가 나을 수 있습니다. 여러 번 검색할 때만 정렬 후 binary_search를 고려합니다.

5. partial_sort로 상위 k개만

전체 정렬이 필요 없으면 partial_sortsort보다 빠릅니다.

// 상위 10개만 필요
std::partial_sort(vec.begin(), vec.begin() + 10, vec.end(), std::greater<int>());

12. 프로덕션 패턴

패턴 1: erase-remove idiom

조건에 맞는 원소를 제거할 때 remove_if + erase 조합을 사용합니다.

// 짝수 제거
vec.erase(std::remove_if(vec.begin(), vec.end(),
                          { return x % 2 == 0; }),
          vec.end());

패턴 2: 정렬 + unique로 중복 제거

std::sort(vec.begin(), vec.end());
vec.erase(std::unique(vec.begin(), vec.end()), vec.end());

패턴 3: 파이프라인 체이닝

여러 알고리즘을 순차 적용해 데이터 파이프라인을 구성합니다.

// 1. copy_if: 필터링
std::vector<int> filtered;
std::copy_if(vec.begin(), vec.end(), std::back_inserter(filtered),
              { return x > 0; });

// 2. transform: 변환
std::vector<double> transformed(filtered.size());
std::transform(filtered.begin(), filtered.end(), transformed.begin(),
                { return std::sqrt(static_cast<double>(x)); });

// 3. accumulate: 집계
double sum = std::accumulate(transformed.begin(), transformed.end(), 0.0);

패턴 4: find + distance로 인덱스 얻기

auto it = std::find(vec.begin(), vec.end(), target);
if (it != vec.end()) {
    size_t index = std::distance(vec.begin(), it);
    // index 사용
}

패턴 5: count_if로 조건 필터 개수

size_t error_count = std::count_if(logs.begin(), logs.end(),
     { return e.level >= 3; });

패턴 6: transform + accumulate로 가중 합

std::vector<int> values = {1, 2, 3, 4, 5};
std::vector<int> weights = {1, 2, 1, 2, 1};
std::vector<int> weighted(values.size());
std::transform(values.begin(), values.end(), weights.begin(),
               weighted.begin(),  { return v * w; });
int total = std::accumulate(weighted.begin(), weighted.end(), 0);

성능 비교 요약

연산알고리즘복잡도비고
정렬sortO(n log n)일반적
정렬stable_sortO(n log n)순서 유지 필요 시
부분 정렬partial_sortO(n log k)상위 k개만
선형 검색findO(n)정렬 불필요
개수count/count_ifO(n)한 번 순회
변환transformO(n)한 번 순회
집계accumulateO(n)순차
복사copy/copy_ifO(n)한 번 순회
제거remove + eraseO(n)erase-remove

13. 구현 체크리스트

프로덕션에 STL 알고리즘을 적용할 때 확인할 항목입니다.

  • find/find_if 결과 사용 전 it != end 체크
  • transform 출력 범위 크기가 입력 이상인지 확인 (또는 back_inserter + reserve)
  • accumulate 초기값 타입이 집계 결과와 일치하는지 확인 (double이면 0.0)
  • 커스텀 비교자가 strict weak ordering을 만족하는지 확인
  • remove/remove_if 후 반드시 erase로 논리적 끝 구간 제거
  • copy_if + back_inserter 사용 시 reserve로 재할당 최소화
  • empty 범위에서 나눗셈 시 0으로 나누기 방지

정리

항목설명
정렬sort, stable_sort, partial_sort — 상황에 맞게 선택
검색find, find_if, find_if_not — end 체크 필수
개수count, count_if — 한 줄 집계
변환transform — 단항/이항, 출력 범위 크기 확인
집계accumulate — 초기값 타입 일치
복사copy, copy_if — reserve로 재할당 최소화
제거remove + erase — erase-remove idiom
에러end 체크, 출력 범위 부족, 비교자 위반, erase 누락
프로덕션erase-remove, sort+unique, 파이프라인 체이닝

핵심 원칙:

  1. find 결과 사용 전 반드시 end 체크
  2. transform 출력 범위 크기 확보
  3. accumulate 초기값 타입 일치 (0.0 for double)
  4. remove/remove_if 후 반드시 erase
  5. reserve로 불필요한 재할당 방지

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 데이터 정렬, 검색, 집계, 변환, 복사, 제거 등 일상적인 컨테이너 처리에 필수입니다. for문 대신 STL 알고리즘을 쓰면 버그가 줄고 가독성이 올라갑니다.

Q. 선행으로 읽으면 좋은 글은?

A. STL 컨테이너 기초람다 표현식을 먼저 익히면 좋습니다.

Q. 더 깊이 공부하려면?

A. cppreference의 algorithm, numeric 헤더 문서와 C++20 Ranges를 참고하세요.

한 줄 요약: sort·find·count·transform·accumulate·copy·remove를 마스터하고, 흔한 에러를 피하며 프로덕션 패턴을 적용할 수 있습니다.


관련 글

  • C++ 알고리즘 선택 완벽 가이드 | 상황별 STL·커스텀 알고리즘 선택법 [#54-1]
  • C++ STL 알고리즘 완벽 가이드 | sort·transform·accumulate [#54-1]
  • C++ 알고리즘 |
  • C++ Algorithm Sort |
  • C++ Algorithm Search |
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3