STL 알고리즘 기본기: sort·find·count·transform·accumulate·copy·remove와 Ranges

들어가며: “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를 완전히 이해할 수 있습니다.
  • 문제 상황별 적절한 알고리즘을 선택할 수 있습니다.
  • 흔한 에러를 피하고 베스트 프랙티스를 적용할 수 있습니다.
  • 프로덕션 수준의 패턴을 사용할 수 있습니다.

for문 대신 알고리즘이 필요한 일곱 가지 상황

대량 데이터 정렬 시 성능·정확성 문제

상황: 수십만 건의 로그를 타임스탬프 기준으로 정렬해야 합니다. 직접 버블 정렬을 구현하면 O(n²)로 시간 초과가 발생하며, 같은 값의 순서를 잘못 처리하기 쉽습니다. 해결: std::sort는 인트로소트(퀵소트 + 힙소트 + 삽입정렬)로 O(n log n)에 정렬합니다. 같은 값의 순서가 중요하면 std::stable_sort를 사용합니다.

컨테이너에서 값 검색 시 end 체크 누락

상황: ID 목록에서 특정 ID가 있는지 확인하며, 있으면 인덱스를 사용합니다. for문으로 돌리다 vec[i] == target 체크 후 i를 사용하는데, 없을 때 -1이나 size()를 반환하는 로직을 빼먹기 쉽습니다. 해결: std::find는 “찾으면 반복자, 없으면 end”를 반환합니다. it != vec.end()로 존재 여부를 일관되게 확인할 수 있습니다.

조건 만족 개수 세기

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

데이터 변환 파이프라인

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

합계·평균·곱셈 등 집계

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

조건에 맞는 원소만 복사

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

조건에 맞는 원소 제거

상황: “짝수 제거”, “만료된 항목 제거” 등입니다. 반복문 안에서 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

정렬: 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를 사용하세요.

커스텀 비교자

구조체나 클래스를 정렬할 때는 비교 함수로 기준을 넘깁니다. 람다 [](const auto& a, const auto& b) { 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(),
         [](const LogEntry& a, const LogEntry& b) {
            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(),
         [](const Student& a, const Student& b) { 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;
}

검색: 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(),
         [](int x) { return x > 5; });
    if (it != vec.end()) {
        std::cout << "First > 5: " << *it << "\n";  // 8
    }
    // 짝수인 첫 번째 원소
    auto it2 = std::find_if(vec.begin(), vec.end(),
         [](int x) { 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(),
         [](int x) { return x % 2 == 0; });
    if (it != vec.end()) {
        // *it == 7
    }
    return 0;
}

정렬된 범위 검색: binary_search·lower_bound·upper_bound

전제 조건: 범위가 정렬되어 있어야 함

std::binary_search, std::lower_bound, std::upper_bound는 정렬된 범위에서만 올바르게 동작합니다. 정렬되지 않은 범위에 사용하면 undefined behavior입니다. 실제로는 크래시가 나는 경우보다 있는 값을 없다고 답하는 경우가 대부분이라 더 찾기 어렵습니다. 또 정렬에 쓴 비교자와 검색에 쓰는 비교자가 같아야 합니다. 내림차순으로 정렬했다면 std::lower_bound(v.begin(), v.end(), x, std::greater<>{})처럼 같은 비교자를 넘겨야 합니다. std::find가 O(n)인 것과 달리 이 함수들은 O(log n)이므로, 같은 벡터를 여러 번 검색한다면 한 번 정렬해 두고 이진 탐색하는 편이 빠릅니다.

std::binary_search: 존재 여부만 확인

O(log n)에 “값이 존재하는지” true/false만 반환합니다. 위치(반복자)가 필요하면 lower_bound/upper_bound를 사용하세요.

#include <algorithm>
#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    if (std::binary_search(vec.begin(), vec.end(), 5)) {
        std::cout << "5 exists\n";
    }
    if (!std::binary_search(vec.begin(), vec.end(), 10)) {
        std::cout << "10 not found\n";
    }
    return 0;
}

std::lower_bound: “이 값 이상”인 첫 위치

정렬된 범위에서 value 이상인 첫 번째 원소의 반복자를 반환합니다. 같은 값이 여러 개일 때 그 구간의 시작을 알 수 있습니다.

#include <algorithm>
#include <vector>
#include <iostream>
int main() {
    std::vector<int> vec = {1, 2, 2, 2, 3, 4, 5};
    auto lower = std::lower_bound(vec.begin(), vec.end(), 2);
    std::cout << "Index: " << std::distance(vec.begin(), lower) << "\n";  // 1
    // 2의 개수
    auto upper = std::upper_bound(vec.begin(), vec.end(), 2);
    std::cout << "Count of 2: " << std::distance(lower, upper) << "\n";  // 3
    return 0;
}

std::upper_bound: “이 값 초과”인 첫 위치

value보다 큰 첫 번째 원소의 반복자를 반환합니다. [lower_bound, upper_bound) 구간이 value와 같은 모든 원소입니다.

#include <algorithm>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 2, 2, 3, 4, 5};
    auto lower = std::lower_bound(vec.begin(), vec.end(), 2);
    auto upper = std::upper_bound(vec.begin(), vec.end(), 2);
    // [lower, upper) 구간이 값 2인 모든 원소
    for (auto it = lower; it != upper; ++it) {
        // *it == 2
    }
}

equal_range: lower_bound + upper_bound 한 번에

std::equal_range는 lower_bound와 upper_bound를 한 번에 호출한 것과 동일한 std::pair를 반환합니다. 같은 값의 구간을 한 번에 얻을 때 유용합니다.

#include <algorithm>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 2, 2, 3, 4, 5};
    auto [lower, upper] = std::equal_range(vec.begin(), vec.end(), 2);
    size_t count = std::distance(lower, upper);  // 3
}

이진 탐색이 조용히 틀리거나 느려지는 경우

정렬 전제가 깨지면 크래시도 진단 메시지도 없이 “있는 값을 없다”고 답하므로, 문제는 대개 리팩터링에서 생깁니다. id 오름차순으로 정렬해 lower_bound하던 벡터를 “최근 항목 먼저”로 바꾸면서 정렬만 std::greater<>로 고치고 검색 호출은 기본 비교자로 남겨 두는 식입니다. 테스트 데이터가 작으면 우연히 맞는 답이 나와 초기 검증도 통과합니다. 외부에서 받아 뒤에 덧붙이기만 하고 재정렬을 빠뜨린 “거의 정렬된” 데이터도 같은 증상을 간헐적으로 냅니다. 정렬 기준을 바꿀 때는 그 컨테이너를 대상으로 하는 모든 이진 탐색 호출을 함께 점검하고, 디버그 빌드에서는 검색 직전에 assert(std::is_sorted(v.begin(), v.end(), cmp))를 두면 O(n) 비용으로 이런 버그를 바로 잡을 수 있습니다.

O(log n)이라는 설명은 비교 횟수 기준이라는 점도 기억해 둘 만합니다. std::list처럼 양방향 반복자만 있는 컨테이너에 lower_bound를 쓰면 컴파일은 되지만, 중간으로 점프할 수 없어 반복자를 한 칸씩 옮기므로 실행 시간은 사실상 선형 탐색과 같습니다. 정렬된 상태에서 검색이 잦다면 std::set·std::map의 멤버 lower_bound를 쓰거나 데이터를 vector로 두는 편이 맞습니다. 같은 이유로 std::set에 알고리즘 버전 std::lower_bound(s.begin(), s.end(), x)를 부르면 트리를 활용하지 못하고 느려지므로 항상 멤버 함수 s.lower_bound(x)를 씁니다.


개수: 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(),
         [](int x) { return x > 5; });
    std::cout << "Count > 5: " << n << "\n";  // 3
    // 짝수 개수
    n = std::count_if(vec.begin(), vec.end(),
         [](int x) { return x % 2 == 0; });
    std::cout << "Even count: " << n << "\n";  // 2
    return 0;
}

변환: 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(),
                   [](int x) { return x * 2; });
    // result: {2, 4, 6, 8, 10}
    // 제자리 변환 (원본 수정)
    std::transform(vec.begin(), vec.end(), vec.begin(),
                   [](int x) { 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(),
                   [](double x) { 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(),
                   [](int x, int y) { return x + y; });
    // sum: {11, 22, 33, 44, 55}
    return 0;
}

집계: 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,
         [](int a, int b) { 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(),
         [](std::string a, const std::string& b) { 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);
}

accumulate의 초기값 타입과 reduce (C++17)

std::accumulate의 결과 타입은 초기값의 타입입니다. double 벡터를 std::accumulate(v.begin(), v.end(), 0)으로 더하면 초기값 0이 int라서 매 단계 결과가 int로 잘립니다. {0.5, 0.5, 0.5}를 이렇게 더하면 1.5가 아니라 0이 나옵니다(GCC 10에서 확인). 컴파일 경고도 없으니 초기값은 항상 0.0, 0LL, std::string{}처럼 원하는 결과 타입으로 씁니다.

std::reduce는 연산 순서를 보장하지 않는 대신 병렬 실행(std::execution::par)을 허용합니다. 그래서 연산이 결합법칙·교환법칙을 만족해야 하고(덧셈·곱셈은 되지만 문자열 연결이나 뺄셈은 안 됨), 부동소수점 합은 순서에 따라 마지막 자리가 달라질 수 있습니다.

#include <execution>
#include <numeric>

double a = std::accumulate(v.begin(), v.end(), 0.0);                    // 순차, 왼쪽부터
double r = std::reduce(std::execution::par, v.begin(), v.end(), 0.0);   // 병렬 허용, 순서 비보장

주의할 점은 par가 “병렬로 실행해도 된다”는 허가일 뿐 병렬 실행을 보장하지 않는다는 것입니다. GCC의 libstdc++는 병렬 알고리즘 백엔드로 Intel TBB를 쓰는데, TBB 없이 빌드하면 컴파일은 되지만 순차로 실행됩니다. MinGW GCC 10에서 TBB 없이 1,000만 개 double을 더해 보면 accumulate와 reduce(par)의 결과가 마지막 자리까지 같았는데, 실제로 병렬로 나뉘지 않았다는 뜻입니다. Linux에서 병렬 효과를 보려면 libtbb-dev를 설치하고 -ltbb로 링크해야 합니다.

복사: 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),
         [](int x) { 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),
         [](int x) { 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;
}

제거: remove

remove의 동작 방식 (주의)

std::remove와 std::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(),
         [](int x) { 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(),
         [](int x) { return x > 5; }), vec.end());
    // vec: {1, 2, 3, 4, 5}
    return 0;
}

로그 분석·점수 처리 파이프라인으로 조합해 보기

로그 분석기 (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(),
         [](const LogEntry& a, const LogEntry& b) {
            return a.timestamp < b.timestamp;
        });
    // 2. count_if: ERROR(3) 개수
    int error_count = std::count_if(logs.begin(), logs.end(),
         [](const LogEntry& e) { 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),
         [](const LogEntry& e) { return e.level >= 2; });
    // 4. find_if: 첫 ERROR 찾기
    auto it = std::find_if(logs.begin(), logs.end(),
         [](const LogEntry& e) { 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,
         [](int a, const LogEntry& e) { 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

점수 처리 파이프라인 (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(),
         [](int s) { 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(),
         [](int s) { 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;
}

두 벡터 결합 및 집계 (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(), [](int p, int q) { 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;
}

end 체크 누락·출력 범위 부족 같은 흔한 실수

find 결과를 end 체크 없이 사용

증상: 찾지 못했을 때 *it 접근으로 Segmentation fault 또는 undefined behavior. 원인: std::find가 end를 반환했는데, 존재한다고 가정하고 역참조함.

// ❌ 잘못된 사용
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 {
    // 없을 때 처리
}

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(), [](int x) { return x * 2; });
// ✅ 올바른 사용
std::vector<int> result(vec.size());
std::transform(vec.begin(), vec.end(), result.begin(), [](int x) { return x * 2; });
// ✅ 또는 back_inserter + reserve
std::vector<int> result;
result.reserve(vec.size());
std::transform(vec.begin(), vec.end(), std::back_inserter(result),
               [](int x) { return x * 2; });

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

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());

비교자에서 strict weak ordering 위반

증상: std::sort 호출 시 크래시 또는 무한 루프. 원인: 비교 함수가 a < a가 false, a <= b처럼 <=를 쓰면 a == b일 때 true가 되어 위반.

// ❌ 잘못된 비교자
std::sort(vec.begin(), vec.end(),
     [](int a, int b) { return a <= b; });  // a <= a -> true (위반)
// ✅ 올바른 비교자 (< 만 사용)
std::sort(vec.begin(), vec.end(),
     [](int a, int b) { return a < b; });

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),
              [](int x) { return x % 2 == 0; });

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();

알고리즘 코드를 읽기 쉽고 빠르게 쓰는 습관

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(), [](int x) { return x > 5; });

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

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

// ✅ 람다 (인라인 가능)
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a < b; });
// ❌ std::function (간접 호출)
std::function<bool(int,int)> cmp = [](int a, int b) { return a < b; };
std::sort(vec.begin(), vec.end(), cmp);

reserve로 재할당 최소화

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

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

정렬이 필요할 때만 정렬

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

partial_sort로 상위 k개만

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

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

erase-remove·sort+unique 같은 자주 쓰는 조합

erase-remove idiom

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

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

정렬 + unique로 중복 제거

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

파이프라인 체이닝

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

// 1. copy_if: 필터링
std::vector<int> filtered;
std::copy_if(vec.begin(), vec.end(), std::back_inserter(filtered),
              [](int x) { return x > 0; });
// 2. transform: 변환
std::vector<double> transformed(filtered.size());
std::transform(filtered.begin(), filtered.end(), transformed.begin(),
                [](int x) { return std::sqrt(static_cast<double>(x)); });
// 3. accumulate: 집계
double sum = std::accumulate(transformed.begin(), transformed.end(), 0.0);

find + distance로 인덱스 얻기

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

count_if로 조건 필터 개수

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

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(), [](int v, int w) { 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

알고리즘 적용 전 점검 목록

프로덕션에 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. std::remove를 호출했는데 컨테이너 크기가 줄지 않는 이유는 무엇인가요?

A. std::remove와 remove_if는 반복자만 받기 때문에 컨테이너 크기를 바꿀 수 없고, 남길 원소를 앞쪽으로 옮긴 뒤 새로운 논리적 끝 반복자를 반환할 뿐입니다. 그 뒤에 남은 원소는 지정되지 않은 값이므로 반환된 반복자부터 end()까지를 erase로 지워야 실제로 제거됩니다. C++20부터는 std::erase와 std::erase_if를 쓰면 이 두 단계를 한 번에 처리할 수 있습니다.

Q. std::ranges 알고리즘의 프로젝션은 어떻게 쓰나요?

A. std::ranges::sort(users, {}, &User::age)처럼 세 번째 인자에 멤버 포인터를 넘기면 비교 전에 각 원소에서 age를 꺼내 비교합니다. std::ranges::find_if(users, pred, &User::name)도 같은 방식으로 name에 조건을 적용합니다. 비교용 람다를 매번 새로 쓰지 않아도 되어 코드가 짧아집니다. sort·find·count·transform·accumulate·copy·remove를 마스터하며, 흔한 에러를 피하며 프로덕션 패턴을 적용할 수 있습니다.


같이 보면 좋은 글