C++ STL 알고리즘 기초: sort·find·transform·accumulate 실전 활용

sort, find/find_if, accumulate, transform만 익혀도 직접 짜던 반복문의 상당 부분을 대체할 수 있습니다. 이 글은 이 기본 알고리즘들의 사용법과, 실제로 자주 틀리는 지점(remove 뒤 erase 누락, accumulate 초기값 타입, 비교자 규칙)을 함께 다룹니다. 13-1 vector와 함께 보면 좋습니다.

들어가며: 직접 짠 반복문의 문제

정렬은 이중 루프, 검색은 선형 탐색, 합계는 for로 누적하는 식으로 매번 반복문을 직접 쓰면 코드가 길어지고, 인덱스 범위·경계 조건·반복자 무효화에서 버그가 생기기 쉽습니다. STL 알고리즘은 <algorithm>·<numeric>에 정의된 함수들로, 반복자 범위 [begin, end)와 동작(함수/람다)만 넘기면 정렬·검색·변환·집계를 한 줄로 표현합니다. 이름만 보고도 “무엇을 하는 코드인지”가 드러나고, 경계 처리는 이미 검증된 구현이 맡습니다.

예를 들어 다음 코드는 정렬·검색·합계를 모두 직접 구현합니다.

// 수동 정렬·검색·합계
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²)이고, 검색에서는 size_t인 i를 int index에 대입하는 부호 변환도 숨어 있습니다. 같은 동작을 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와 다중 조건 비교 함수

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(),
               [](const Person& a, const Person& b) {
                  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(),
               [](const Student& a, const Student& b) {
                  if (a.score != b.score) return a.score > b.score;
                  return a.id < b.id;
              });
}

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


find, find_if와 정렬된 범위의 이진 검색

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(),
                            [](int x) { 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) 구간이 그 값들 전체입니다.


count_if와 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(),
                                    [](int x) { 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,
                                    [](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(),
                                           [](const std::string& a, const std::string& b) {
                                              return a + b;
                                          });
    std::cout << concat << "\n";  // Hello World
}

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


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

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(),
                    [](int x, int y) { return x + y; });
    // result: {5, 7, 9}
}

두 범위의 같은 위치 원소를 (x, y) -> x + y 형태로 결합해 result에 씁니다. 처리 길이는 첫 번째 범위 [a.begin(), a.end())가 정하고, 두 번째 범위는 시작 반복자만 받으므로 최소한 그만큼 길다고 가정합니다. b가 a보다 짧으면 범위 밖을 읽는 미정의 동작이 되므로, 길이가 다를 수 있다면 호출 전에 확인해야 합니다.


로그 집계, 점수 통계, 필터링 예제

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

#include <algorithm>
#include <cstdint>
#include <numeric>
#include <string>
#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(),
               [](const LogEntry& a, const LogEntry& b) {
                  return a.timestamp < b.timestamp;
              });
    auto it = std::find_if(logs.begin(), logs.end(),
                            [](const LogEntry& e) { 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),
                                    [](int64_t acc, const LogEntry& e) {
                                       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(),
                                 [](int x) { return x >= 90; });
    std::cout << "Above 90: " << above90 << "\n";
}

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

#include <algorithm>
#include <cctype>
#include <iterator>
#include <string>
#include <vector>
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),
                    [](const std::string& s) {
                       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),
                  [](int x) { return x % 2 == 0; });
    std::vector<int> evenSquared(evens.size());
    std::transform(evens.begin(), evens.end(), evenSquared.begin(),
                    [](int x) { 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으로 변환하면 각 단계의 의도는 분명하지만 중간 벡터가 하나 생깁니다. 이 정도 조합이라면 방법 2처럼 루프 하나로 쓰는 편이 오히려 간단할 수 있습니다. C++20 ranges를 쓰면 vec | std::views::filter(is_even) | std::views::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,
                                          [](double acc, const Sale& s) {
                                             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 <string>
#include <vector>
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(),
                      [](const Task& a, const Task& b) { return a.priority < b.priority; });
    // priority 1: A, C (원래 순서 유지)
    // priority 2: B, D (원래 순서 유지)
}

예제 7: 필터 → 정렬 → 상위 N개 합계 (주문 데이터)

“완료된 주문만 골라 → 금액순으로 정렬하고 → 상위 5개 금액을 합친다”처럼 여러 알고리즘을 한 흐름으로 이어 쓰는 경우입니다.

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

struct Order {
    int id;
    std::string status;  // "pending", "completed", "cancelled"
    double amount;
};

int main() {
    std::vector<Order> orders = {
        {1, "completed", 150.0}, {2, "pending", 80.0},   {3, "completed", 200.0},
        {4, "completed", 90.0},  {5, "cancelled", 50.0}, {6, "completed", 120.0},
    };

    // 1. 완료된 주문만 추리기
    std::vector<Order> completed;
    std::copy_if(orders.begin(), orders.end(), std::back_inserter(completed),
                 [](const Order& o) { return o.status == "completed"; });

    // 2. 상위 N개만 필요하므로 전체 정렬 대신 partial_sort
    const std::size_t topN = std::min<std::size_t>(5, completed.size());
    std::partial_sort(completed.begin(), completed.begin() + topN, completed.end(),
                      [](const Order& a, const Order& b) { return a.amount > b.amount; });

    // 3. 상위 N개 금액 합계 (초기값을 0.0으로 줘야 double로 누적됨)
    double total = std::accumulate(completed.begin(), completed.begin() + topN, 0.0,
                                   [](double sum, const Order& o) { return sum + o.amount; });

    std::cout << "Top " << topN << " completed total: " << total << "\n";  // 560
}

완료된 주문이 4개뿐이라 topN은 4가 되고 합계는 560입니다. topN을 min으로 자르지 않으면 completed.begin() + 5가 범위를 벗어납니다. 또 상위 N개만 필요할 때는 전체를 sort하는 대신 partial_sort(또는 순서가 필요 없으면 nth_element)를 쓰면 정렬할 원소 수가 줄어듭니다.

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] + ... 형태로 합을 구합니다. 네 번째 인자로 이항 함수를 주면 곱셈, 문자열 연결 등 다른 집계도 가능합니다.


end() 비교 누락, remove 후 erase 누락, accumulate 초기값 실수

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

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를 써야 합니다.

// 잘못된 사용
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으로 주면 첫 단계부터 0 * x = 0이 되어 결과가 항상 0입니다. 곱셈의 항등원인 1을 줘야 합니다.

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

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

이미 정렬된 큰 배열에서 검색을 반복한다면 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의 비교자는 strict weak ordering을 만족해야 합니다. cmp(a, a)는 항상 false여야 하고, cmp(a, b)와 cmp(b, a)가 동시에 true가 되면 안 됩니다. <=를 쓰면 같은 값에서 이 규칙이 깨지고, 구현이 경계 검사를 생략한 분할 루프에서 범위 밖을 읽어 크래시가 날 수 있습니다.

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

에러 7: 빈 벡터에서 accumulate

평균을 구할 때 빈 벡터면 vec.size()가 0이라 0으로 나누게 됩니다. 또 int / size_t는 정수 나눗셈이라 소수점이 버려지므로 double로 변환한 뒤 나눠야 합니다.

// 잘못된 사용
int sum = std::accumulate(vec.begin(), vec.end(), 0);
double avg = sum / vec.size();  // 정수 나눗셈이라 소수점 버림, vec가 비면 0으로 나누기(UB)
// 올바른 사용
int sum = std::accumulate(vec.begin(), vec.end(), 0);
double avg = vec.empty() ? 0.0 : static_cast<double>(sum) / vec.size();

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

순회 중에 erase나 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·binary_search는 범위가 정렬되어 있다고 가정하고 반씩 건너뛰므로, 정렬되지 않은 범위에서는 에러 없이 의미 없는 결과를 돌려줍니다.

// 잘못된 사용
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* + std::string의 결과인 std::string을 다시 const char* 누적값에 대입할 수 없기 때문입니다.

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

에러 11: 정렬되지 않은 두 범위를 merge

std::merge 결과가 정렬되어 있지 않습니다. merge는 두 입력 범위가 이미 같은 기준으로 정렬되어 있다고 가정하고 앞에서부터 하나씩 비교해 내보낼 뿐입니다. 입력이 정렬되지 않았으면 결과도 정렬되지 않으며, 에러는 나지 않습니다.

std::vector<int> a = {5, 1, 9}, b = {8, 2, 6};
std::sort(a.begin(), a.end());
std::sort(b.begin(), b.end());
std::vector<int> out;
std::merge(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(out));  // 1 2 5 6 8 9

에러 12: 입력과 겹치는 위치로 copy

같은 vector 안에서 원소를 오른쪽으로 밀려고 copy를 썼더니 값이 같은 숫자로 덮여 버립니다. std::copy는 앞에서부터 복사하므로, 출력 시작 위치가 입력 범위 안(입력보다 뒤쪽)에 있으면 아직 읽지 않은 원소를 먼저 덮어씁니다. 표준은 이 경우를 미정의 동작으로 둡니다.

std::vector<int> v = {1, 2, 3, 4, 5};
// 잘못: v[0..2]를 v[2..4]로 밀기: 출력이 입력과 겹침
// std::copy(v.begin(), v.begin() + 3, v.begin() + 2);
// 올바름: 뒤에서부터 복사하는 copy_backward (출력 범위의 끝을 넘김)
std::copy_backward(v.begin(), v.begin() + 3, v.end());  // v = 1 2 1 2 3

반대로 원소를 왼쪽으로 당길 때(출력 시작이 입력 시작보다 앞)는 std::copy가 안전합니다. std::move/std::move_backward도 같은 규칙을 따릅니다.

에러 13: partition 반환값의 의미를 반대로 이해

조건을 만족하는 원소 개수를 잘못 셉니다. std::partition은 조건이 참인 원소를 앞으로 모으고, 조건이 거짓인 첫 원소를 가리키는 반복자를 돌려줍니다. [begin, 반환값)이 참인 구간, [반환값, end)가 거짓인 구간입니다. 참인 원소 수는 std::distance(v.begin(), 반환값)입니다. 원래 순서를 유지해야 하면 stable_partition을 씁니다.

성능: 수동 루프와 비교하면

작업수동 forSTL 알고리즘
정렬흔히 짜는 이중 루프는 O(n²)sort는 O(n log n)
검색 (비정렬)O(n)find도 O(n)
검색 (정렬)직접 이진 검색을 짜지 않으면 O(n)lower_bound는 O(log n)
개수·합계·변환O(n)count_if·accumulate·transform도 O(n)

선형 작업(검색·집계·변환)은 같은 루프를 알고리즘 함수 안에서 돌리는 것이라, 최적화 빌드에서는 손으로 쓴 루프와 성능 차이가 거의 없습니다. 차이가 크게 나는 것은 정렬처럼 직접 짜면 더 나쁜 복잡도의 알고리즘을 고르기 쉬운 경우, 그리고 정렬된 데이터에서 이진 검색을 쓰느냐 마느냐입니다. STL 알고리즘을 쓰는 주된 이유는 속도보다 의도가 이름에 드러나고 경계 실수가 줄어든다는 점입니다.


nth_element와 partial_sort: 전체 정렬이 필요 없을 때

k번째 값 하나만 필요하면 nth_element가 평균 O(n)으로 충분합니다. 호출 뒤 v[k]에는 정렬했을 때 그 자리에 올 값이 들어가고, 앞쪽은 그보다 작거나 같은 값, 뒤쪽은 크거나 같은 값이 순서 없이 놓입니다.

std::nth_element(v.begin(), v.begin() + k, v.end());
int kth = v[k];  // 중앙값 등을 구할 때

상위 k개를 순서대로 원하면 partial_sort를 씁니다. 비용은 O(n log k)라 k가 n보다 훨씬 작을 때 전체 정렬보다 유리합니다. vec.size()가 10보다 작으면 vec.begin() + 10이 범위를 벗어나므로 앞의 예제 7처럼 min으로 잘라야 합니다.

std::partial_sort(vec.begin(), vec.begin() + 10, vec.end(), std::greater<int>());

알고리즘 선택 가이드

요구사항추천 알고리즘대안
전체 정렬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(순서 유지)
두 정렬 범위의 합·교·차집합set_union, set_intersection, set_differenceset_symmetric_difference
제자리 수정fill, generate, replace, reverse, rotatereplace_if

집합 연산과 제자리 수정 알고리즘

위 표 아래 두 줄은 앞 장에서 다루지 않은 계열입니다. 집합 연산은 두 입력이 모두 정렬되어 있다는 전제로 한 번씩만 훑어서(O(n + m)) 결과를 만듭니다. 정렬 안 된 입력을 넣으면 에러 없이 틀린 결과가 나오므로, 입력이 std::set이 아니라 vector라면 앞에 sort가 있는지 먼저 확인하세요.

std::vector<int> a = {1, 2, 3, 4, 5};
std::vector<int> b = {3, 4, 5, 6, 7};
std::vector<int> out;

std::set_union(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(out));        // 1 2 3 4 5 6 7
out.clear();
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(out)); // 3 4 5
out.clear();
std::set_difference(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(out));   // 1 2

제자리 수정 알고리즘은 새 컨테이너를 만들지 않고 범위 안의 값을 바꿉니다. rotate는 “가운데 지점을 맨 앞으로” 옮기는 연산이라 리스트에서 원소 하나를 다른 위치로 옮기는 UI 로직(드래그 정렬)에 자주 쓰입니다.

std::vector<int> v = {1, 2, 3, 4, 5};
std::fill(v.begin(), v.end(), 0);                          // 0 0 0 0 0
std::iota(v.begin(), v.end(), 1);                          // 1 2 3 4 5  (<numeric>)
std::replace(v.begin(), v.end(), 3, 99);                   // 1 2 99 4 5
std::reverse(v.begin(), v.end());                          // 5 4 99 2 1
std::rotate(v.begin(), v.begin() + 2, v.end());            // 99 2 1 5 4

자주 쓰는 조합

정렬된 벡터에서 중복 제거 (sort + unique + erase)

unique는 인접한 같은 값만 하나로 줄이므로, 먼저 정렬해서 같은 값을 붙여 놓아야 합니다. remove와 마찬가지로 크기는 바꾸지 않으므로 erase가 필요합니다.

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

조건부 합계 (accumulate + 람다)

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

C++20 std::erase_if로 erase-remove를 한 줄로

C++20부터는 remove_if 뒤에 erase를 붙이는 관용구를 std::erase_if(v, pred) 한 줄로 쓸 수 있고, 지운 개수도 돌려받습니다. vector·deque·string뿐 아니라 list·map·unordered_map에도 오버로드가 있어, 컨테이너 종류가 바뀌어도 같은 코드를 쓸 수 있습니다.

std::vector<int> v = {1, 2, 3, 4, 5, 6};
auto removed = std::erase_if(v, [](int x) { return x % 2 == 0; });  // v = {1, 3, 5}, removed = 3

C++17 이하 환경이라면 같은 동작을 하는 작은 헬퍼(c.erase(std::remove_if(c.begin(), c.end(), pred), c.end()))를 하나 만들어 두고 쓰면 erase 누락을 막을 수 있습니다.

STL 알고리즘 실무 주의점

개발 시 주의사항

  1. 한 번만 찾을 거면 정렬하지 마세요. 정렬(O(n log n)) 후 binary_search(O(log n))는 여러 번 검색할 때만 이득입니다. 검색이 한 번뿐이면 find(O(n))가 더 쌉니다. “검색이니까 정렬부터”라는 습관은 오히려 코드를 느리게 만들 수 있습니다.
  2. accumulate 초기값의 타입이 결과 타입입니다. double 벡터를 0으로 시작하면 매 단계 int로 잘려서 합이 틀립니다. 0.0(또는 0LL)처럼 원하는 결과 타입으로 적으세요.
    std::vector<double> prices = {1.5, 2.5, 3.5};
    auto wrong = std::accumulate(prices.begin(), prices.end(), 0);    // 6 (int로 잘림)
    auto right = std::accumulate(prices.begin(), prices.end(), 0.0);  // 7.5
  3. 출력 범위는 알고리즘이 늘려 주지 않습니다. transform·copy의 출력 반복자가 빈 vector의 begin()이면 범위 밖에 씁니다. 크기를 미리 맞추거나 std::back_inserter를 넘기세요.

디버깅 방법

  • 디버그 모드 반복자 검사: MSVC Debug 빌드나 GCC의 -D_GLIBCXX_DEBUG를 켜면 범위 밖 접근, 정렬 안 된 범위에 binary_search 같은 실수를 런타임에 잡아 줍니다.
  • 중간 결과 출력: 알고리즘을 여러 개 연결했다면 단계마다 for (auto x : v) std::cout << x << ' ';로 찍어 보면 어느 단계에서 틀어졌는지 바로 보입니다.
  • 비교자 검증: 정렬이 이상하면 std::is_sorted(v.begin(), v.end(), cmp)로 결과를 확인하고, 비교자가 a < a에 true를 돌려주지 않는지(strict weak ordering) 점검합니다.

참고 자료


이전 글: C++ vector 기초 | 초기화·연산·용량 관리와 실전 패턴
다음 글: C++ STL 고급 알고리즘: partition·merge·집합 연산·힙 연산


같이 보면 좋은 글