C++ STL 알고리즘 기초 | sort·find·count·transform·accumulate 가이드

C++ STL 알고리즘 기초 | sort·find·count·transform·accumulate 가이드

이 글의 핵심

C++ STL 알고리즘 기초에 대한 실전 가이드입니다. sort·find·count·transform·accumulate 가이드 등을 예제와 함께 상세히 설명합니다.

들어가며: 직접 for문 돌리다가 버그가 생겼어요

”정렬·검색·집계를 매번 수동으로 작성하는데 실수가 많아요”

데이터 처리 코드를 작성할 때마다 반복문을 직접 작성했습니다. 정렬은 이중 루프, 검색은 선형 탐색, 합계는 for로 누적… 코드가 길어지고 인덱스 오류·반복자 무효화·경계 조건에서 버그가 자주 발생했습니다.

STL 알고리즘<algorithm>·<numeric>에 정의된 함수들로, 반복자 범위 [begin, end)동작(함수/람다)만 넘기면 정렬·검색·변환·집계를 한 줄로 표현할 수 있습니다. 구현이 최적화되어 있어 직접 for문보다 실수도 적고 성능도 나쁘지 않습니다.

비유하면 STL 알고리즘은 “요리 레시피”입니다. 직접 재료를 하나씩 처리하는 대신, “이 범위를 이렇게 정렬해”, “이 조건에 맞는 것만 찾아”라고 명령만 하면 됩니다. 내부적으로는 검증된 구현이 동작하므로, 직접 루프를 작성하는 것보다 안전하고 빠릅니다.

문제의 코드:

// ❌ 나쁜 예: 수동 정렬·검색·합계
std::vector<int> vec = {5, 2, 8, 1, 9};
for (size_t i = 0; i < vec.size(); ++i) {
    for (size_t j = i + 1; j < vec.size(); ++j) {
        if (vec[i] > vec[j]) std::swap(vec[i], vec[j]);
    }
}
int target = 8;
int index = -1;
for (size_t i = 0; i < vec.size(); ++i) {
    if (vec[i] == target) { index = i; break; }
}
int sum = 0;
for (size_t i = 0; i < vec.size(); ++i) sum += vec[i];

위 코드 설명: 수동 정렬은 O(n²)이고 인덱스 버그가 생기기 쉽습니다. 검색·합계도 for문이 길어지고 경계 조건을 놓치기 쉽습니다. STL 알고리즘은 같은 동작을 한 줄로 표현하면서 구현이 최적화되어 있습니다.

STL 알고리즘으로 해결:

#include <algorithm>
#include <numeric>

std::vector<int> vec = {5, 2, 8, 1, 9};

std::sort(vec.begin(), vec.end());
auto it = std::find(vec.begin(), vec.end(), 8);
int index = (it != vec.end()) ? std::distance(vec.begin(), it) : -1;
int sum = std::accumulate(vec.begin(), vec.end(), 0);

이 글을 읽으면:

  • sort, find, count, transform, accumulate의 완전한 사용법을 익힐 수 있습니다.
  • 문제 시나리오별 선택 기준을 알 수 있습니다.
  • 일반적인 에러와 해결법을 배울 수 있습니다.
  • 프로덕션에서 검증된 패턴을 활용할 수 있습니다.
flowchart TB
  subgraph problem["자주 겪는 실패 시나리오"]
    P1[수동 for문 → 인덱스 버그]
    P2["정렬된데 find 사용 → O(n)"]
    P3[remove만 쓰고 erase 안 함 → size 안 줄음]
    P4[비교자 반대 → 정렬 결과 뒤바뀜]
  end
  subgraph solution["올바른 선택"]
    S1[STL 알고리즘 사용]
    S2[lower_bound / binary_search]
    S3[erase-remove idiom]
    S4[비교자 정확히 지정]
  end
  P1 --> S1
  P2 --> S2
  P3 --> S3
  P4 --> S4

목차

  1. 문제 시나리오와 원인 분석
  2. 정렬 알고리즘: sort
  3. 검색 알고리즘: find
  4. 집계 알고리즘: count와 accumulate
  5. 변환 알고리즘: transform
  6. 완전한 알고리즘 예제 모음
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 구현 체크리스트

1. 문제 시나리오와 원인 분석

시나리오 1: 로그 데이터 정렬 — 수동 버블 정렬이 느림

증상: 10만 건 로그를 타임스탬프 순으로 정렬하는데 10초 이상 걸림.

원인: for 이중 루프로 O(n²) 버블 정렬. std::sort는 O(n log n) 퀵소트/인트로소트 계열.

해결: std::sort(vec.begin(), vec.end()) 또는 std::stable_sort(같은 값 순서 유지 필요 시).

시나리오 2: 정렬된 배열에서 검색 — find 사용

증상: 정렬된 100만 개 배열에서 값 찾는데 선형 탐색으로 시간 초과.

원인: std::find는 O(n). 정렬된 범위에서는 lower_bound가 O(log n).

해결: 정렬된 범위에서는 std::lower_bound, std::binary_search 사용.

시나리오 3: 조건에 맞는 원소 개수 — 수동 카운트

증상: “양수인 개수”, “이름이 A로 시작하는 개수”를 for문으로 세다가 경계 조건 누락.

원인: 수동 루프에서 초기화·증가 조건 실수.

해결: std::count_if(vec.begin(), vec.end(), predicate) 사용.

시나리오 4: 모든 원소 변환 — for문에서 인덱스 오류

증상: vec[i] = f(vec[i]) 형태로 변환하다가 i 범위 초과나 타입 불일치.

원인: 수동 인덱스 관리·반복자 무효화.

해결: std::transform(vec.begin(), vec.end(), vec.begin(), f) 사용.

시나리오 5: 합계·곱셈·문자열 연결 — 누적 초기값 오류

증상: int sum = 0으로 시작했는데 빈 벡터에서 예외, 또는 곱셈에서 초기값 0 사용.

원인: 초기값·빈 범위 처리 미흡.

해결: std::accumulate에 적절한 초기값 전달. 곱셈은 초기값 1.

시나리오 6: remove만 쓰고 erase 안 함

증상: std::remove 호출 후에도 vec.size()가 그대로.

원인: remove는 원소를 지우지 않고 “지울 값이 아닌 것”만 앞으로 모은 뒤 새 논리적 끝 반환. 실제로 지우려면 erase 필요.

해결: vec.erase(std::remove(...), vec.end()) idiom 사용.

시나리오 7: 비교자 반대 — 정렬 결과가 뒤바뀜

증상: 내림차순이 필요한데 오름차순으로 나옴(또는 그 반대).

원인: std::sort의 비교자는 “a가 b보다 앞에 오려면” true일 때 a < b 형태. 오름차순은 a < b, 내림차순은 a > b 또는 std::greater<>().

해결: std::greater<>() 또는 { return a > b; }로 내림차순 명시.


2. 정렬 알고리즘: sort

std::sort 기본 사용법

std::sort반개구간 [begin, end) 에 있는 원소를 기본적으로 오름차순으로 정렬합니다. 내부적으로 보통 퀵소트/인트로소트 계열을 사용하며, O(n log n)입니다.

// 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};
    std::sort(vec.begin(), vec.end());
    for (int x : vec) std::cout << x << " ";  // 1 2 5 8 9
    std::cout << "\n";

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

위 코드 설명: sort(vec.begin(), vec.end())는 기본적으로 오름차순(less)으로 정렬합니다. 세 번째 인자로 std::greater<int>()를 넘기면 내림차순이 됩니다. <algorithm> 헤더가 필요합니다.

커스텀 비교 함수 (구조체 정렬)

원소가 구조체나 클래스일 때는 “어떤 기준으로 순서를 정할지” 비교 함수로 넘겨야 합니다.

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

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<Person> people = {
        {"Alice", 25},
        {"Bob", 30},
        {"Charlie", 20}
    };

    std::sort(people.begin(), people.end(),
               {
                  return a.age < b.age;
              });
    // Charlie(20), Alice(25), Bob(30)
}

위 코드 설명: a.age < b.age면 나이 오름차순. “a가 b보다 앞에 오려면” true를 반환할 조건을 넣습니다. 내림차순은 a.age > b.age 또는 std::greater 활용.

완전한 sort 예제: 다중 조건 정렬

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

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

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

    // 점수 내림차순, 점수 같으면 id 오름차순
    std::sort(students.begin(), students.end(),
               {
                  if (a.score != b.score) return a.score > b.score;
                  return a.id < b.id;
              });
}

위 코드 설명: std::tuple로 비교하면 return std::tie(b.score, a.id) < std::tie(a.score, b.id) 같은 형태로도 가능합니다. 다중 조건은 먼저 비교할 기준을 정하고, 같으면 다음 기준으로 비교합니다.


3. 검색 알고리즘: find

std::find: 값으로 검색

std::find값이 같은 첫 번째 원소를 찾아 그 위치를 가리키는 반복자를 반환합니다. 없으면 end()를 반환하므로, 반환값이 end()인지 꼭 확인한 뒤 사용해야 합니다.

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

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto it = std::find(vec.begin(), vec.end(), 3);

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

위 코드 설명: find(begin, end, value)는 값과 같은 첫 원소의 반복자를 반환하고, 없으면 end()를 반환합니다. it != vec.end()로 있는지 확인한 뒤, distance(vec.begin(), it)로 인덱스를 구할 수 있습니다. 정렬이 없어도 되지만 선형 O(n)입니다.

std::find_if: 조건으로 검색

특정 이 아니라 “짝수”, “이름이 A로 시작”처럼 조건을 만족하는 첫 원소를 찾을 때는 std::find_if를 씁니다.

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

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto it = std::find_if(vec.begin(), vec.end(),
                            { return x % 2 == 0; });

    if (it != vec.end()) {
        std::cout << "First even: " << *it << "\n";  // 2
    }
}

위 코드 설명: find_if는 predicate(참/거짓을 반환하는 람다)를 받아, 조건을 만족하는 첫 원소의 반복자를 반환합니다. 없으면 역시 end()를 반환합니다.

범위가 이미 정렬되어 있을 때는 이진 검색이 O(log n)으로 유리합니다.

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

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // 정렬되어 있어야 함

    if (std::binary_search(vec.begin(), vec.end(), 3)) {
        std::cout << "Found\n";
    }

    auto lower = std::lower_bound(vec.begin(), vec.end(), 3);
    auto upper = std::upper_bound(vec.begin(), vec.end(), 3);
    std::cout << "Count of 3: " << std::distance(lower, upper) << "\n";
}

위 코드 설명: binary_search는 “값이 존재하는지”만 true/false로 반환합니다. lower_bound는 “이 값 이상인 첫 위치”, upper_bound는 “이 값보다 큰 첫 위치”를 반환합니다. 같은 값이 여러 개일 때 [lower, upper) 구간이 그 값들 전체입니다.


4. 집계 알고리즘: count와 accumulate

std::count / count_if

std::count지정한 값과 같은 원소의 개수를 반환하고, std::count_if조건을 만족하는 원소의 개수를 반환합니다.

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

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

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

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

위 코드 설명: count(begin, end, value)는 값과 같은 원소 개수를, count_if(begin, end, predicate)는 조건을 만족하는 원소 개수를 반환합니다. O(n)이며 정렬 여부와 관계없이 쓸 수 있습니다.

std::accumulate: 합산·곱셈·문자열 연결

std::accumulate는 범위를 왼쪽부터 하나씩 접어 나가는(fold) 연산입니다. 세 번째 인자는 초기값이고, 기본 동작은 합입니다.

#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
}

위 코드 설명: accumulate(begin, end, 초기값)은 기본적으로 초기값 + 원소들의 합을 반환합니다. 네 번째 인자로 이항 함수를 주면 곱셈(초기값 1), 문자열 연결 등 다른 집계도 할 수 있습니다. <numeric> 헤더가 필요합니다. 빈 범위면 초기값이 그대로 반환됩니다.


5. 변환 알고리즘: transform

std::transform: 각 원소 변환

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

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

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

    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} (제자리 변환)
}

위 코드 설명: transform(입력시작, 입력끝, 출력시작, 단항함수)는 각 원소에 람다를 적용한 결과를 출력 범위에 씁니다. 출력을 vec.begin()으로 주면 제자리에서 vec 자체가 바뀝니다. 출력 범위 크기가 미리 충분해야 합니다.

두 컨테이너 결합

transform입력 반복자를 두 쌍 주면, 두 시퀀스의 같은 위치 원소를 한 번에 받는 이항 함수를 쓸 수 있습니다.

#include <algorithm>
#include <vector>

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

    std::transform(a.begin(), a.end(), b.begin(), result.begin(),
                    { return x + y; });
    // result: {5, 7, 9}
}

위 코드 설명: 두 범위의 같은 위치 원소를 (x, y) -> x + y 형태로 결합해 result에 씁니다. 두 범위 길이가 다르면 짧은 쪽까지만 처리됩니다.


6. 완전한 알고리즘 예제 모음

예제 1: 로그 데이터 정렬·검색·집계

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

struct LogEntry {
    int64_t timestamp;
    std::string message;
};

int main() {
    std::vector<LogEntry> logs = {
        {1000, "start"},
        {500, "init"},
        {1500, "done"}
    };

    std::sort(logs.begin(), logs.end(),
               {
                  return a.timestamp < b.timestamp;
              });

    auto it = std::find_if(logs.begin(), logs.end(),
                            { return e.message == "done"; });
    if (it != logs.end()) {
        std::cout << "Found at " << it->timestamp << "\n";
    }

    int64_t sum = std::accumulate(logs.begin(), logs.end(), int64_t(0),
                                    {
                                       return acc + e.timestamp;
                                   });
    std::cout << "Sum of timestamps: " << sum << "\n";
}

예제 2: 점수 통계 (평균·최대·최소)

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

int main() {
    std::vector<int> scores = {85, 92, 78, 90, 88};

    int sum = std::accumulate(scores.begin(), scores.end(), 0);
    double avg = static_cast<double>(sum) / scores.size();
    std::cout << "Average: " << avg << "\n";

    auto [minIt, maxIt] = std::minmax_element(scores.begin(), scores.end());
    std::cout << "Min: " << *minIt << ", Max: " << *maxIt << "\n";

    int above90 = std::count_if(scores.begin(), scores.end(),
                                 { return x >= 90; });
    std::cout << "Above 90: " << above90 << "\n";
}

예제 3: 문자열 벡터 변환 (대소문자·접두사)

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

int main() {
    std::vector<std::string> words = {"Hello", "World", "C++"};

    std::vector<std::string> upper;
    upper.reserve(words.size());
    std::transform(words.begin(), words.end(), std::back_inserter(upper),
                    {
                       std::string r = s;
                       for (char& c : r) c = std::toupper(static_cast<unsigned char>(c));
                       return r;
                   });
}

예제 4: 조건 필터링 + 변환 (copy_if + transform)

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

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

    // 방법 1: copy_if로 짝수만 복사 후, transform으로 제곱
    std::vector<int> evens;
    std::copy_if(vec.begin(), vec.end(), std::back_inserter(evens),
                  { return x % 2 == 0; });
    std::vector<int> evenSquared(evens.size());
    std::transform(evens.begin(), evens.end(), evenSquared.begin(),
                    { return x * x; });
    // evenSquared: {4, 16, 36}

    // 방법 2: 한 번에 (람다에서 조건+변환)
    std::vector<int> result;
    for (int x : vec) {
        if (x % 2 == 0) result.push_back(x * x);
    }
}

위 코드 설명: copy_if로 조건에 맞는 원소만 새 벡터로 복사한 뒤, transform으로 변환합니다. C++20 ranges를 쓰면 vec | filter(even) | transform(square) 형태로 파이프라인처럼 쓸 수 있습니다.

예제 5: accumulate로 구조체 집계

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

struct Sale {
    double amount;
    int quantity;
};

int main() {
    std::vector<Sale> sales = {{100.0, 2}, {50.0, 5}, {200.0, 1}};

    double totalAmount = std::accumulate(sales.begin(), sales.end(), 0.0,
                                          {
                                             return acc + s.amount * s.quantity;
                                         });
    std::cout << "Total: " << totalAmount << "\n";  // 100*2 + 50*5 + 200*1 = 650
}

예제 6: stable_sort vs sort (같은 값 순서)

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

struct Task {
    int priority;
    std::string name;
};

int main() {
    std::vector<Task> tasks = {
        {1, "A"}, {2, "B"}, {1, "C"}, {2, "D"}
    };

    std::stable_sort(tasks.begin(), tasks.end(),
                      { return a.priority < b.priority; });
    // priority 1: A, C (원래 순서 유지)
    // priority 2: B, D (원래 순서 유지)
}

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

flowchart TB
    subgraph sort["정렬"]
        S1[sort]
        S2[stable_sort]
        S3[partial_sort]
    end
    subgraph search["검색"]
        F1[find / find_if]
        F2[binary_search]
        F3[lower_bound / upper_bound]
    end
    subgraph transform["변환"]
        T1[transform]
        T2[copy_if]
    end
    subgraph agg["집계"]
        A1[accumulate]
        A2[count / count_if]
        A3[all_of / any_of / none_of]
    end
    input["(begin, end) 반복자 범위"] --> sort
    input --> search
    input --> transform
    input --> agg

위 다이어그램 설명: STL 알고리즘은 모두 반복자 범위 [begin, end)를 받아 동작합니다. 정렬·검색·변환·집계로 분류되며, 람다와 함께 사용하면 복잡한 조건도 표현할 수 있습니다.

accumulate 동작 흐름

flowchart LR
    I["초기값 0"] --> A1[""+ vec(0"]"]
    A1 --> A2[""+ vec(1"]"]
    A2 --> A3[""+ vec(2"]"]
    A3 --> A4["..."]
    A4 --> R["최종 결과"]

위 다이어그램 설명: accumulate(vec.begin(), vec.end(), 0)((0 + vec[0]) + vec[1]) + vec[2] + ... 형태로 합을 구합니다. 네 번째 인자로 이항 함수를 주면 곱셈, 문자열 연결 등 다른 집계도 가능합니다.


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

에러 1: find 반환값을 end()와 비교하지 않음

증상: *std::find(...)로 역참조 시 크래시.

원인: 없을 때 end()를 반환하는데, end()를 역참조하면 미정의 동작.

해결법:

// ❌ 잘못된 사용
auto it = std::find(vec.begin(), vec.end(), 99);
int value = *it;  // 없으면 end() 역참조 → UB

// ✅ 올바른 사용
auto it = std::find(vec.begin(), vec.end(), 99);
if (it != vec.end()) {
    int value = *it;
}

에러 2: remove만 쓰고 erase 안 함

증상: std::remove 호출 후에도 vec.size()가 그대로.

원인: remove는 원소를 지우지 않고 “지울 값이 아닌 것”만 앞으로 모은 뒤 새 논리적 끝 반환.

해결법:

// ❌ 잘못된 사용
std::remove(vec.begin(), vec.end(), 2);
// vec.size()는 그대로!

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

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

증상: transform 출력에 back_inserter 없이 작은 컨테이너 쓰면 범위 초과.

원인: transform은 출력 범위에 직접 쓰므로, 출력 범위 크기가 입력 범위 이상이어야 함.

해결법:

// ❌ 잘못된 사용
std::vector<int> result(vec.size() - 1);  // 작음
std::transform(vec.begin(), vec.end(), result.begin(), ...);

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

// 또는 back_inserter (결과 크기 미리 모를 때)
std::vector<int> result;
result.reserve(vec.size());
std::transform(vec.begin(), vec.end(), std::back_inserter(result), ...);

에러 4: accumulate 곱셈에서 초기값 0

증상: 곱셈 결과가 항상 0.

원인: accumulate(..., 0, { return a * b; })에서 초기값 0이면 0 * x = 0.

해결법:

// ❌ 잘못된 사용
int product = std::accumulate(vec.begin(), vec.end(), 0,
                                { return a * b; });
// 항상 0

// ✅ 올바른 사용
int product = std::accumulate(vec.begin(), vec.end(), 1,
                                { return a * b; });

에러 5: 정렬된 범위에 find 사용

증상: 정렬된 100만 개에서 검색 시 시간 초과.

원인: find는 O(n) 선형 탐색. 정렬된 범위에서는 lower_bound가 O(log n).

해결법:

// ❌ 잘못된 사용 (정렬된 범위에서)
auto it = std::find(vec.begin(), vec.end(), target);

// ✅ 올바른 사용
auto it = std::lower_bound(vec.begin(), vec.end(), target);
if (it != vec.end() && *it == target) {
    // 찾음
}

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

증상: std::sort에서 크래시 또는 무한 루프.

원인: 비교자가 a < a가 true를 반환하거나, a < bb < a가 동시에 true가 되면 안 됨.

해결법:

// ❌ 잘못된 사용
std::sort(vec.begin(), vec.end(),
           { return a <= b; });  // a==b일 때 문제

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

에러 7: 빈 벡터에서 accumulate

증상: 빈 벡터에서 accumulate로 나눗셈 시 division by zero.

원인: sum / vec.size()에서 vec.size()가 0.

해결법:

// ❌ 잘못된 사용
int sum = std::accumulate(vec.begin(), vec.end(), 0);
double avg = sum / vec.size();  // vec가 비면 UB

// ✅ 올바른 사용
int sum = std::accumulate(vec.begin(), vec.end(), 0);
double avg = vec.empty() ? 0.0 : static_cast<double>(sum) / vec.size();

에러 8: 반복자 무효화 — 순회 중 컨테이너 수정

증상: for_each 또는 범위 기반 for 안에서 vec.push_back 등으로 크래시.

원인: 순회 중 컨테이너 수정 시 반복자 무효화.

해결법:

// ❌ 잘못된 사용
for (auto it = vec.begin(); it != vec.end(); ++it) {
    if (*it == 0) vec.erase(it);  // it 무효화
}

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

// 또는 erase 반환값 사용
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it == 0) it = vec.erase(it);
    else ++it;
}

에러 9: 정렬되지 않은 범위에 lower_bound

증상: lower_bound 결과가 예상과 다름.

원인: lower_bound는 정렬된 범위를 가정. 정렬되지 않으면 결과가 무의미함.

해결법:

// ❌ 잘못된 사용
std::vector<int> vec = {5, 2, 8, 1, 9};  // 정렬 안 됨
auto it = std::lower_bound(vec.begin(), vec.end(), 5);

// ✅ 올바른 사용
std::sort(vec.begin(), vec.end());
auto it = std::lower_bound(vec.begin(), vec.end(), 5);

에러 10: 문자열 accumulate에서 초기값

증상: accumulate로 문자열 연결 시 const char* 초기값 사용으로 크래시.

원인: ""const char*이고, const char* + string 연산이 잘못된 동작일 수 있음.

해결법:

// ❌ 잘못된 사용
std::string result = std::accumulate(words.begin(), words.end(), "");

// ✅ 올바른 사용
std::string result = std::accumulate(words.begin(), words.end(), std::string());

성능 비교: 수동 루프 vs STL 알고리즘

작업수동 forSTL 알고리즘비고
정렬O(n²) 버블O(n log n) sortsort가 훨씬 빠름
검색 (비정렬)O(n)O(n) find비슷, STL이 내부 최적화
검색 (정렬)O(n)O(log n) lower_bound정렬이 있으면 이진 검색
개수O(n)O(n) count_if비슷
합계O(n)O(n) accumulate비슷
변환O(n)O(n) transform비슷, STL이 SIMD 활용 가능

실무 팁: STL 알고리즘은 컴파일러·표준 라이브러리 구현에 따라 SIMD·인라인 최적화가 적용될 수 있어, 수동 루프보다 더 빠를 수 있습니다. 가독성과 유지보수성도 우수합니다.


8. 베스트 프랙티스

1. 반복자 범위 [begin, end) 일관 사용

// ✅ begin, end 쌍으로 범위 전달
std::sort(vec.begin(), vec.end());

// ✅ 부분 범위
std::sort(vec.begin() + 2, vec.end() - 1);

2. const 참조로 predicate 인자 전달

// ❌ 값 복사 (구조체에 비효율)
 { return a.age < b.age; }

// ✅ const 참조
 { return a.age < b.age; }

3. reserve로 불필요한 재할당 방지

std::vector<int> result;
result.reserve(vec.size());
std::transform(vec.begin(), vec.end(), std::back_inserter(result), f);

4. 정렬된 범위에서는 이진 검색

if (std::is_sorted(vec.begin(), vec.end())) {
    auto it = std::lower_bound(vec.begin(), vec.end(), target);
} else {
    auto it = std::find(vec.begin(), vec.end(), target);
}

5. 알고리즘 + 람다 조합

int cnt = std::count_if(v.begin(), v.end(),  { return x > 0; });
auto it = std::find_if(v.begin(), v.end(),  { return x % 2 == 0; });

6. nth_element로 k번째 원소만

// 전체 정렬 O(n log n) 대신 O(n) 평균
std::nth_element(v.begin(), v.begin() + k, v.end());
int kth = v[k];

7. partial_sort로 상위 k개만

// 상위 10개만 정렬 (전체 정렬보다 빠름)
std::partial_sort(vec.begin(), vec.begin() + 10, vec.end(), std::greater<int>());

8. 범위 기반 for와 알고리즘 혼용

// 읽기 전용 순회
for (const auto& x : vec) { ... }

// 조건 검사 후 조기 종료
if (std::any_of(vec.begin(), vec.end(), predicate)) { ... }

알고리즘 선택 가이드

요구사항추천 알고리즘대안
전체 정렬sortstable_sort(순서 유지), partial_sort(상위 k개)
값 검색 (비정렬)find, find_if-
값 검색 (정렬)lower_bound, binary_search-
조건 만족 개수count_ifcount(값 일치)
합·곱·연결accumulate-
각 원소 변환transformfor_each(부수 효과만)
조건 제거erase + remove_if-
중복 제거sort + unique + erase-
최대/최소max_element, min_elementminmax_element(둘 다)
조건 분할partitionstable_partition(순서 유지)

9. 프로덕션 패턴

패턴 1: erase-remove idiom

vec.erase(std::remove(vec.begin(), vec.end(), value), vec.end());
vec.erase(std::remove_if(vec.begin(), vec.end(), predicate), vec.end());

패턴 2: 정렬된 벡터에 unique로 중복 제거

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

패턴 3: copy_if로 조건 필터링

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

패턴 4: minmax_element로 한 번에 최소·최대

auto [minIt, maxIt] = std::minmax_element(vec.begin(), vec.end());

패턴 5: partition으로 조건 분할

auto pivot = std::partition(vec.begin(), vec.end(),
                              { return x % 2 == 0; });
// [begin, pivot): 짝수, [pivot, end): 홀수

패턴 6: all_of / any_of / none_of로 빠른 검사

if (std::all_of(vec.begin(), vec.end(),  { return x > 0; })) {
    // 모두 양수
}
if (std::any_of(vec.begin(), vec.end(),  { return x == 0; })) {
    // 0이 하나라도 있음
}

패턴 7: 로그·메트릭 집계

struct Metric {
    std::string name;
    double value;
};

double totalByCategory(const std::vector<Metric>& metrics, const std::string& cat) {
    return std::accumulate(metrics.begin(), metrics.end(), 0.0,
                           [&cat](double acc, const Metric& m) {
                               return m.name == cat ? acc + m.value : acc;
                           });
}

패턴 8: 유효성 검사 (all_of)

bool allValid(const std::vector<int>& ids) {
    return std::all_of(ids.begin(), ids.end(),
                        { return id > 0 && id < 1000000; });
}

패턴 9: 정렬된 벡터 병합 (merge)

std::vector<int> a = {1, 3, 5};
std::vector<int> b = {2, 4, 6};
std::vector<int> merged(a.size() + b.size());
std::merge(a.begin(), a.end(), b.begin(), b.end(), merged.begin());
// merged: {1, 2, 3, 4, 5, 6}

패턴 10: 조건부 변환 (transform + 조건)

std::vector<int> vec = {1, -2, 3, -4, 5};
std::vector<int> result(vec.size());
std::transform(vec.begin(), vec.end(), result.begin(),
                { return x < 0 ? 0 : x; });

10. 구현 체크리스트

알고리즘 선택 체크리스트

  • 정렬된 범위에서 검색하는가? → lower_bound, binary_search
  • 정렬되지 않은 범위에서 검색하는가? → find, find_if
  • 조건 만족 개수가 필요한가? → count_if
  • 합·곱·문자열 연결이 필요한가? → accumulate
  • 각 원소 변환이 필요한가? → transform
  • 조건에 맞는 원소 제거가 필요한가? → erase-remove idiom
  • 같은 값 순서 유지가 필요한가? → stable_sort

에러 방지 체크리스트

  • find/find_if 반환값을 end()와 비교하는가?
  • remove 후 erase를 호출하는가?
  • transform 출력 범위 크기가 충분한가?
  • accumulate 곱셈 시 초기값 1을 사용하는가?
  • 비교자가 strict weak ordering을 만족하는가?

실무 팁

개발 시 주의사항

  1. [팁 1]: [설명]

    // 예시 코드
  2. [팁 2]: [설명]

    // 예시 코드
  3. [팁 3]: [설명]

디버깅 방법

  • [방법 1]: [설명]
  • [방법 2]: [설명]
  • [방법 3]: [설명]

FAQ

Q: “기본적으로 뭘 쓰면 되나요?”

A: 정렬은 sort, 검색은 정렬 여부에 따라 find / lower_bound, 개수는 count_if, 합계는 accumulate, 변환은 transform입니다. sort·find·count·transform·accumulate만 익혀도 대부분의 반복 로직을 대체할 수 있습니다.

Q: “정렬된 vector와 set 중 뭘 쓰나요?”

A: (1) 한 번 만들고 검색만 많으면 정렬된 vector + lower_bound가 캐시 친화적이라 빠를 수 있음. (2) 삽입/삭제가 동적으로 반복되면 set이 O(log n)으로 유리합니다.

Q: “람다 대신 함수 포인터를 쓸 수 있나요?”

A: 네. bool cmp(int a, int b) { return a < b; }와 같이 정의한 뒤 std::sort(vec.begin(), vec.end(), cmp)로 넘길 수 있습니다. 람다는 인라인으로 최적화되기 쉽고, 캡처가 필요할 때는 람다가 유리합니다.

Q: “C++20 ranges는?”

A: std::ranges::sort(vec)처럼 vec 전체를 넘길 수 있어 반복자 쌍을 쓰지 않아도 됩니다. C++20 이상에서 사용 가능합니다.

Q: “for_each와 transform의 차이는?”

A: for_each는 void를 반환하는 동작(출력, 수정 등)에 쓰고, transform은 새 값을 반환해 출력 범위에 씁니다. “새 컨테이너를 만드는” 변환이면 transform, 부수 효과만 필요하면 for_each입니다.

Q: “sort와 stable_sort 중 뭘 쓰나요?”

A: 같은 값의 원래 순서를 유지해야 하면 stable_sort. 그렇지 않으면 sort가 더 빠르고 메모리도 적게 씁니다.

Q: “프로덕션에서 주의할 점은?”

A: find 반환값 end() 비교, remove 후 erase, 정렬된 범위에서만 lower_bound, accumulate 곱셈 시 초기값 1, 빈 컨테이너 처리.


참고 자료


한 줄 요약: sort·find·count·transform·accumulate로 직접 for문 대신 STL 알고리즘을 쓰면 코드가 짧고 안전해집니다. find 반환값은 end()와 비교하고, remove 후 erase를 호출하며, 정렬된 범위에서는 lower_bound를 사용하세요. 다음으로 STL 알고리즘 심화를 읽어보면 좋습니다.

이전 글: C++ vector 기초 | 초기화·연산·용량 관리와 실전 패턴
다음 글: C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
  • C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
  • C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴

이 글에서 다루는 키워드 (관련 검색어)

C++, STL, 알고리즘, std::sort, std::find, std::count, std::transform, std::accumulate, 람다, predicate 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ STL 고급 알고리즘 | partition·merge·집합 연산·힙으로 데이터 처리 마스터
  • C++ Move Semantics | std::move로 불필요한 복사 제거하고 성능 최적화
  • C++ Perfect Forwarding | std::forward로
  • C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
  • C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴