C++ STL 알고리즘 완벽 가이드 | sort·transform·accumulate [#54-1]
이 글의 핵심
STL 알고리즘 마스터: sort, binary_search, transform, accumulate, 커스텀 비교자. 문제 시나리오, 완전한 예제, 흔한 에러, 성능 팁, 프로덕션 패턴.
들어가며: STL 알고리즘으로 반복 코드를 제거하자
”매번 for문 작성하다 보니 버그가 자꾸 생겨요”
데이터 처리 코드를 작성할 때마다 직접 반복문을 작성하다 보면, 인덱스 오류, 경계 조건 실수, 성능 이슈가 반복됩니다. C++ STL 알고리즘은 <algorithm>, <numeric>에 정의된 검증된 구현으로, 정렬·검색·변환·집계를 한 줄로 표현하면서 직접 루프보다 실수도 적고 성능도 나쁘지 않습니다. 이 글에서는 sort·binary_search·transform·accumulate를 중심으로, 문제 시나리오부터 프로덕션 패턴까지 실전 활용법을 다룹니다.
요구 환경: C++17 이상
이 글을 읽으면:
- 핵심 STL 알고리즘을 완전히 이해할 수 있습니다.
- 문제 상황별 적절한 알고리즘을 선택할 수 있습니다.
- 흔한 에러를 피하고 성능을 최적화할 수 있습니다.
- 프로덕션 수준의 패턴을 적용할 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오
- 정렬 알고리즘 (sort)
- 검색 알고리즘 (binary_search)
- 변환 알고리즘 (transform)
- 집계 알고리즘 (accumulate)
- 완전한 STL 알고리즘 예제
- 자주 발생하는 에러와 해결법
- 성능 최적화 팁
- 프로덕션 패턴
1. 문제 시나리오
시나리오 1: 대량 로그 데이터 정렬
상황: 수백만 건의 로그를 타임스탬프 기준으로 정렬해야 합니다. 직접 버블 정렬이나 삽입 정렬을 구현하면 O(n²)로 시간 초과가 발생합니다.
해결: std::sort는 내부적으로 인트로소트(퀵소트 + 힙소트 + 삽입정렬)를 사용해 O(n log n)에 정렬합니다. 같은 값의 순서가 중요하면 std::stable_sort를 사용합니다.
시나리오 2: 정렬된 배열에서 빠른 검색
상황: 이미 정렬된 100만 개 ID 목록에서 특정 ID가 있는지 확인해야 합니다. std::find로 선형 검색하면 O(n)으로 느립니다.
해결: std::binary_search는 O(log n)에 “존재 여부”만 반환하고, std::lower_bound/upper_bound로 위치나 개수를 구할 수 있습니다.
시나리오 3: 데이터 변환 파이프라인
상황: JSON 파싱 결과를 DTO로 변환하거나, 센서 값을 정규화하는 등 “각 원소에 함수 적용”이 반복됩니다. for문으로 작성하면 가독성이 떨어지고 실수하기 쉽습니다.
해결: std::transform으로 변환 로직을 선언적으로 표현하고, 람다와 조합해 복잡한 변환도 한 줄로 처리합니다.
시나리오 4: 집계 연산 (합계, 곱셈, 문자열 연결)
상황: 벡터의 합, 곱, 또는 문자열 리스트를 하나로 연결하는 등 “접기(fold)” 연산이 필요합니다. 수동 루프는 초기값 처리나 타입 변환에서 실수하기 쉽습니다.
해결: std::accumulate로 초기값부터 순차적으로 이항 함수를 적용해 집계합니다. <numeric> 헤더가 필요합니다.
STL 알고리즘 분류 다이어그램
flowchart TB
subgraph sort[정렬]
S1[sort]
S2[stable_sort]
S3[partial_sort]
end
subgraph search[검색]
F1[binary_search]
F2[lower_bound]
F3[upper_bound]
end
subgraph transform[변환]
T1[transform]
T2[transform 두 범위]
end
subgraph agg[집계]
A1[accumulate]
A2[reduce C++17]
end
input["[begin, end) 반복자"] --> sort
input --> search
input --> transform
input --> agg
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;
});
}
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) - 같은 점수 내 순서 유지
}
partial_sort: 상위 k개만 정렬
전체를 정렬할 필요 없이 “상위 N개만 올바른 순서”가 필요할 때 사용합니다. “상위 10명만 뽑기” 같은 경우 전체 정렬보다 효율적입니다.
#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>());
// vec: {9, 8, 7, ...} (나머지 순서 보장 안 됨)
for (int i = 0; i < 3; ++i)
std::cout << vec[i] << " "; // 9 8 7
std::cout << "\n";
return 0;
}
3. 검색 알고리즘 (binary_search)
전제 조건: 범위가 정렬되어 있어야 함
std::binary_search, std::lower_bound, std::upper_bound는 정렬된 범위에서만 올바르게 동작합니다. 정렬되지 않은 범위에 사용하면 undefined behavior입니다.
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
}
4. 변환 알고리즘 (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;
}
DTO 변환 예제
JSON 파싱 결과를 DTO로 변환하는 실전 패턴입니다.
#include <algorithm>
#include <vector>
#include <string>
struct JsonRecord {
std::string id;
std::string name;
int value;
};
struct Dto {
int id;
std::string name;
double normalized_value;
};
std::vector<Dto> toDtos(const std::vector<JsonRecord>& records, double scale) {
std::vector<Dto> result;
result.reserve(records.size());
std::transform(records.begin(), records.end(), std::back_inserter(result),
[scale](const JsonRecord& r) {
return Dto{
std::stoi(r.id),
r.name,
static_cast<double>(r.value) / scale
};
});
return result;
}
5. 집계 알고리즘 (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);
}
accumulate vs reduce (C++17)
std::reduce는 병렬 실행을 고려한 집계이며, 연산 순서가 보장되지 않습니다. 부동소수점 합산에서 accumulate와 결과가 다를 수 있지만, 병렬화 시 성능이 좋습니다. 순서가 중요한 경우(예: 문자열 연결)에는 accumulate를 사용하세요.
#include <numeric>
#include <vector>
#include <execution>
int main() {
std::vector<int> vec(1000000, 1);
// 순차, 순서 보장
int sum1 = std::accumulate(vec.begin(), vec.end(), 0);
// 병렬 가능, 순서 비보장 (C++17)
int sum2 = std::reduce(std::execution::par, vec.begin(), vec.end(), 0);
}
6. 완전한 STL 알고리즘 예제
예제 1: 로그 분석기 (정렬 + 검색 + 집계)
로그 분석기는 정렬·검색·집계를 모두 활용하는 대표적인 예제입니다. 타임스탬프 정렬, 레벨별 개수 집계, 조건 필터링을 STL 알고리즘으로 처리합니다.
// 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. 타임스탬프 오름차순 정렬
std::sort(logs.begin(), logs.end(),
{
return a.timestamp < b.timestamp;
});
// 2. ERROR(3) 개수
int error_count = std::count_if(logs.begin(), logs.end(),
{ return e.level == 3; });
std::cout << "Errors: " << error_count << "\n";
// 3. WARN 이상만 필터링 (copy_if)
std::vector<LogEntry> critical;
std::copy_if(logs.begin(), logs.end(), std::back_inserter(critical),
{ return e.level >= 2; });
// 4. 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
Avg level: 1.8
예제 2: 점수 처리 파이프라인 (transform + sort + binary_search)
#include <algorithm>
#include <numeric>
#include <vector>
#include <cmath>
#include <iostream>
int main() {
std::vector<int> scores = {72, 85, 91, 68, 78, 88, 95, 62};
// 1. 정규화 (0-100 -> 0-1)
std::vector<double> normalized(scores.size());
std::transform(scores.begin(), scores.end(), normalized.begin(),
{ return s / 100.0; });
// 2. 정렬 (상위 5명)
std::partial_sort(scores.begin(), scores.begin() + 5, scores.end(),
std::greater<int>());
// 3. 80점 이상인지 이진 검색 (정렬 후)
std::sort(scores.begin(), scores.end());
bool has_high = std::binary_search(scores.begin(), scores.end(), 80,
{ return a < b; }); // 기본이지만 명시
// 4. 평균
double avg = std::accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
std::cout << "Average: " << avg << "\n";
return 0;
}
예제 3: 두 벡터 결합 및 집계
#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};
// 각 품목별 금액 = 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}
// 총 금액
int total = std::accumulate(amounts.begin(), amounts.end(), 0);
std::cout << "Total: " << total << "\n"; // 1450
return 0;
}
7. 자주 발생하는 에러와 해결법
에러 1: binary_search를 정렬되지 않은 범위에 사용
증상: 검색 결과가 잘못되거나 예측 불가능합니다.
원인: binary_search, lower_bound, upper_bound는 정렬된 범위에서만 올바르게 동작합니다.
// ❌ 잘못된 사용
std::vector<int> vec = {5, 2, 8, 1, 9};
bool found = std::binary_search(vec.begin(), vec.end(), 5); // UB!
// ✅ 올바른 사용
std::sort(vec.begin(), vec.end());
bool found = std::binary_search(vec.begin(), vec.end(), 5);
에러 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 (크기 미리 모름)
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: 비교자에서 strict weak ordering 위반
증상: std::sort 호출 시 크래시 또는 무한 루프.
원인: 비교 함수가 a < a가 false, a < b와 b < a가 동시에 true가 되면 안 되며, a < b와 b < c이면 a < c여야 합니다.
// ❌ 잘못된 비교자 (a == b일 때 true 반환 가능)
std::sort(vec.begin(), vec.end(),
{ return a <= b; }); // a <= a -> true (위반)
// ✅ 올바른 비교자 (< 만 사용)
std::sort(vec.begin(), vec.end(),
{ return a < b; });
에러 5: 반복자 무효화 후 사용
증상: Segmentation fault 또는 예측 불가능한 동작.
원인: vec.push_back 등으로 재할당이 일어나면 기존 반복자가 무효화됩니다.
// ❌ 위험한 패턴
auto it = vec.begin();
vec.push_back(100); // 재할당 가능 -> it 무효화
*it; // UB
// ✅ 반복자 사용 직후에만 사용하거나, 인덱스 사용
size_t idx = std::distance(vec.begin(), it);
vec.push_back(100);
// idx는 여전히 유효 (단, 삽입 위치에 따라 의미 변경됨)
에러 6: empty 범위에서 accumulate
증상: 초기값이 그대로 반환됩니다. 에러는 아니지만, vec.empty()일 때 accumulate 결과가 초기값이므로 나눗셈 등에서 0으로 나누기 주의.
// ✅ empty 체크
double avg = vec.empty()
? 0.0
: std::accumulate(vec.begin(), vec.end(), 0.0) / vec.size();
8. 성능 최적화 팁
팁 1: 정렬이 필요할 때만 정렬
binary_search를 쓰려면 정렬이 필요하지만, 한 번만 검색할 거면 std::find(O(n))가 더 나을 수 있습니다. n이 작을 때는 정렬 비용 O(n log n)이 검색 비용 O(n)보다 클 수 있습니다.
// 한 번만 검색 -> find가 나을 수 있음
auto it = std::find(vec.begin(), vec.end(), target);
// 여러 번 검색 -> 정렬 후 binary_search
std::sort(vec.begin(), vec.end());
for (int key : keys) {
if (std::binary_search(vec.begin(), vec.end(), key)) { /* ... */ }
}
팁 2: reserve로 재할당 줄이기
transform + back_inserter 또는 copy_if + back_inserter 사용 시, 결과 크기를 대략 알 수 있으면 reserve로 재할당을 줄입니다.
std::vector<int> result;
result.reserve(vec.size()); // 최대 vec.size()개
std::copy_if(vec.begin(), vec.end(), std::back_inserter(result),
{ return x % 2 == 0; });
팁 3: partial_sort로 상위 k개만
전체 정렬이 필요 없으면 partial_sort가 sort보다 빠릅니다. k가 작을수록 유리합니다.
// 상위 10개만 필요
std::partial_sort(vec.begin(), vec.begin() + 10, vec.end(), std::greater<int>());
팁 4: 커스텀 비교자 인라인
람다 비교자는 컴파일러가 인라인하기 쉽습니다. 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);
팁 5: std::reduce로 병렬 집계 (C++17)
대용량 데이터 집계 시 std::reduce와 std::execution::par로 병렬화할 수 있습니다. 연산 순서가 중요하지 않을 때만 사용하세요.
#include <numeric>
#include <execution>
int sum = std::reduce(std::execution::par, vec.begin(), vec.end(), 0);
성능 비교 요약
| 연산 | 알고리즘 | 복잡도 | 비고 |
|---|---|---|---|
| 정렬 | sort | O(n log n) | 일반적 |
| 정렬 | stable_sort | O(n log n) | 순서 유지 필요 시 |
| 부분 정렬 | partial_sort | O(n log k) | 상위 k개만 |
| 선형 검색 | find | O(n) | 정렬 불필요 |
| 이진 검색 | binary_search | O(log n) | 정렬 필수 |
| 변환 | transform | O(n) | 한 번 순회 |
| 집계 | accumulate | O(n) | 순차 |
| 집계 | reduce (par) | O(n/p) | 병렬 |
9. 프로덕션 패턴
패턴 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. 필터링
std::vector<int> filtered;
std::copy_if(vec.begin(), vec.end(), std::back_inserter(filtered),
{ return x > 0; });
// 2. 변환
std::vector<double> transformed(filtered.size());
std::transform(filtered.begin(), filtered.end(), transformed.begin(),
{ return std::sqrt(static_cast<double>(x)); });
// 3. 집계
double sum = std::accumulate(transformed.begin(), transformed.end(), 0.0);
패턴 4: 안전한 이진 검색 래퍼
정렬 여부를 확인한 뒤 binary_search를 호출하는 래퍼입니다.
template <typename Container, typename T>
bool safe_binary_search(const Container& c, const T& value) {
if (!std::is_sorted(c.begin(), c.end()))
return false; // 또는 정렬 후 검색
return std::binary_search(c.begin(), c.end(), value);
}
패턴 5: 범위 기반 알고리즘 적용 흐름
sequenceDiagram
participant Input as 입력 데이터
participant Filter as copy_if
participant Transform as transform
participant Sort as sort
participant Aggregate as accumulate
Input->>Filter: 원본 벡터
Filter->>Transform: 필터링된 벡터
Transform->>Sort: 변환된 벡터
Sort->>Aggregate: 정렬된 벡터
Aggregate->>Aggregate: 집계 결과
패턴 6: 알고리즘 선택 체크리스트
| 요구사항 | 추천 알고리즘 |
|---|---|
| 전체 정렬 | sort |
| 같은 값 순서 유지 | stable_sort |
| 상위 k개만 | partial_sort |
| 존재 여부 (정렬됨) | binary_search |
| 위치/개수 (정렬됨) | lower_bound, upper_bound, equal_range |
| 각 원소 변환 | transform |
| 조건 만족 개수 | count_if |
| 합/곱/연결 | accumulate |
| 조건 제거 | remove_if + erase |
| 중복 제거 | sort + unique + erase |
패턴 7: 구현 체크리스트
프로덕션에 STL 알고리즘을 적용할 때 확인할 항목입니다.
-
binary_search계열 사용 전 범위가 정렬되어 있는지 확인 -
transform출력 범위 크기가 입력 이상인지 확인 (또는back_inserter+reserve) -
accumulate초기값 타입이 집계 결과와 일치하는지 확인 (double이면0.0) - 커스텀 비교자가 strict weak ordering을 만족하는지 확인
-
remove/remove_if후 반드시erase로 논리적 끝 구간 제거 - 대용량 데이터 시
reserve로 재할당 최소화 - 여러 번 검색 시 정렬 비용 대비 이진 검색 이득이 있는지 검토
정리
| 항목 | 설명 |
|---|---|
| 정렬 | sort, stable_sort, partial_sort - 상황에 맞게 선택 |
| 검색 | binary_search, lower_bound, upper_bound - 정렬 필수 |
| 변환 | transform - 단항/이항, 출력 범위 크기 확인 |
| 집계 | accumulate - 초기값 타입 일치, reduce로 병렬화 |
| 에러 | 정렬 없이 binary_search, 출력 범위 부족, 비교자 위반 |
| 성능 | reserve, partial_sort, 람다 비교자 |
| 프로덕션 | erase-remove, sort+unique, 파이프라인 |
핵심 원칙:
- 정렬된 범위에서만 binary_search 계열 사용
- transform 출력 범위 크기 확보
- accumulate 초기값 타입 일치 (0.0 for double)
- 비교자는 strict weak ordering 준수
- reserve로 불필요한 재할당 방지
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 데이터 처리, 정렬, 변환, 집계 등 일상적인 프로그래밍 작업에 필수입니다. 본문의 문제 시나리오와 프로덕션 패턴을 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: sort·transform·accumulate·binary_search를 마스터하고, 흔한 에러를 피하며 프로덕션 패턴을 적용할 수 있습니다.
관련 글
- C++ 알고리즘 선택 완벽 가이드 | 상황별 STL·커스텀 알고리즘 선택법 [#54-1]
- C++ STL 알고리즘 기초 완벽 가이드 | sort·find
- C++ 알고리즘 |
- C++ Execution Policy |
- C++ 병렬 알고리즘 완벽 가이드 | std::execution::par·par_unseq