C++ STL 알고리즘 기초 완벽 가이드 | sort·find
이 글의 핵심
C++ STL 알고리즘 기초 입니다. sort, find, transform, accumulate 등 핵심 알고리즘의 사용법과 성능 특성을 실전 예제로 설명합니다. 데이터 처리 코드를 작성할 때마다 직접 for문을 돌리다 보면 이런 일이 반복됩니다: 인덱스 범위를 잘못 써서 Segmentation fault가 나요. 정렬할 때 같은 값 처리 로직을 빼먹었어요.
들어가며: “for문 작성하다 보니 버그가 자꾸 생겨요”
실제 겪는 문제 시나리오
데이터 처리 코드를 작성할 때마다 직접 for문을 돌리다 보면 이런 일이 반복됩니다:
"인덱스 범위를 잘못 써서 Segmentation fault가 나요."
"정렬할 때 같은 값 처리 로직을 빼먹었어요."
"값을 찾을 때 존재하지 않으면 end() 체크를 깜빡했어요."
"조건에 맞는 원소를 제거하려다 반복자 무효화로 크래시가 났어요."
"합계·평균 계산할 때 초기값 타입을 잘못 넣어서 부동소수점 오차가 나요."
STL 알고리즘으로 해결:
| 문제 | STL 해결 |
|---|---|
| 인덱스 오류 | [begin, end) 반복자로 범위 명시, 경계 검증된 구현 사용 |
| 정렬 실수 | std::sort, std::stable_sort — 검증된 O(n log n) 구현 |
| 검색 실수 | std::find, std::find_if — end 체크 패턴 통일 |
| 개수 세기 | std::count, std::count_if — 한 줄로 집계 |
| 변환 실수 | std::transform — 선언적 변환, 출력 범위만 확보 |
| 복사 실수 | std::copy, std::copy_if — 범위 기반 복사 |
| 제거 실수 | std::remove + erase — erase-remove idiom |
| 요구 환경: C++17 이상 권장 | |
| 이 글을 읽으면: |
- sort, find, count, transform, accumulate, copy, remove를 완전히 이해할 수 있습니다.
- 문제 상황별 적절한 알고리즘을 선택할 수 있습니다.
- 흔한 에러를 피하고 베스트 프랙티스를 적용할 수 있습니다.
- 프로덕션 수준의 패턴을 사용할 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
1. 문제 시나리오 상세
시나리오 1: 대량 데이터 정렬 시 성능·정확성 문제
상황: 수십만 건의 로그를 타임스탬프 기준으로 정렬해야 합니다. 직접 버블 정렬을 구현하면 O(n²)로 시간 초과가 발생하고, 같은 값의 순서를 잘못 처리하기 쉽습니다.
해결: std::sort는 인트로소트(퀵소트 + 힙소트 + 삽입정렬)로 O(n log n)에 정렬합니다. 같은 값의 순서가 중요하면 std::stable_sort를 사용합니다.
시나리오 2: 컨테이너에서 값 검색 시 end 체크 누락
상황: ID 목록에서 특정 ID가 있는지 확인하고, 있으면 인덱스를 사용합니다. for문으로 돌리다 vec[i] == target 체크 후 i를 사용하는데, 없을 때 -1이나 size()를 반환하는 로직을 빼먹기 쉽습니다.
해결: std::find는 “찾으면 반복자, 없으면 end”를 반환합니다. it != vec.end()로 존재 여부를 일관되게 확인할 수 있습니다.
시나리오 3: 조건 만족 개수 세기
상황: “에러 로그가 몇 개인지”, “80점 이상이 몇 명인지” 등을 세어야 합니다. 수동 카운터를 쓰면 초기화를 빼먹거나, 다른 변수와 섞이기 쉽습니다.
해결: std::count, std::count_if로 한 줄에 집계합니다.
시나리오 4: 데이터 변환 파이프라인
상황: JSON 파싱 결과를 DTO로 변환하거나, 센서 값을 정규화하는 등 “각 원소에 함수 적용”이 반복됩니다. for문은 가독성이 떨어지고 인덱스 실수가 납니다.
해결: std::transform으로 변환 로직을 선언적으로 표현합니다.
시나리오 5: 합계·평균·곱셈 등 집계
상황: 벡터의 합, 곱, 또는 문자열 리스트를 하나로 연결하는 “접기(fold)” 연산이 필요합니다. 수동 루프는 초기값 처리나 타입 변환에서 실수하기 쉽습니다.
해결: std::accumulate로 초기값부터 순차적으로 이항 함수를 적용해 집계합니다.
시나리오 6: 조건에 맞는 원소만 복사
상황: “양수만”, “에러 레벨만” 등 조건을 만족하는 원소만 새 컨테이너로 복사해야 합니다. for문으로 push_back하다 범위 오류가 나기 쉽습니다.
해결: std::copy_if + std::back_inserter로 한 줄에 처리합니다.
시나리오 7: 조건에 맞는 원소 제거
상황: “짝수 제거”, “만료된 항목 제거” 등입니다. 반복문 안에서 erase를 호출하면 반복자 무효화로 크래시가 납니다.
해결: std::remove_if + vec.erase 조합(erase-remove idiom)으로 안전하게 제거합니다.
STL 알고리즘 분류 다이어그램
flowchart TB
subgraph sort[정렬]
S1[sort]
S2[stable_sort]
S3[partial_sort]
end
subgraph search[검색]
F1[find]
F2[find_if]
F3[find_if_not]
end
subgraph count[개수]
C1[count]
C2[count_if]
end
subgraph transform[변환]
T1[transform]
end
subgraph agg[집계]
A1[accumulate]
end
subgraph copy[복사]
CP1[copy]
CP2[copy_if]
end
subgraph remove[제거]
R1[remove]
R2[remove_if]
end
input["[begin, end) 반복자"] --> sort
input --> search
input --> count
input --> transform
input --> agg
input --> copy
input --> remove
2. 정렬: sort
std::sort: 기본 정렬
std::sort는 반개구간 [begin, end) 에 있는 원소를 기본적으로 오름차순으로 정렬합니다. 내부적으로 인트로소트를 사용하며, 비교 연산만 정의되어 있으면 어떤 타입이든 정렬할 수 있습니다.
// g++ -std=c++17 -o sort_basic sort_basic.cpp && ./sort_basic
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
std::sort(vec.begin(), vec.end());
for (int x : vec) std::cout << x << " "; // 1 2 3 5 7 8 9
std::cout << "\n";
// 내림차순
std::sort(vec.begin(), vec.end(), std::greater<int>());
for (int x : vec) std::cout << x << " "; // 9 8 7 5 3 2 1
std::cout << "\n";
return 0;
}
주의: std::sort는 같은 값끼리의 원래 순서를 보장하지 않습니다. 같은 값의 순서가 중요하면 std::stable_sort를 사용하세요.
커스텀 비교자
구조체나 클래스를 정렬할 때는 비교 함수로 기준을 넘깁니다. 람다 { return a.x < b.x; }는 “a가 b보다 앞에 오려면 a.x < b.x”를 의미합니다.
#include <algorithm>
#include <vector>
#include <string>
struct LogEntry {
std::string timestamp;
int level;
std::string message;
};
int main() {
std::vector<LogEntry> logs = {
{"2026-04-23 10:00:00", 2, "Warning"},
{"2026-04-23 09:00:00", 1, "Info"},
{"2026-04-23 10:00:00", 1, "Error"}
};
// 타임스탬프 오름차순, 같으면 level 오름차순
std::sort(logs.begin(), logs.end(),
{
if (a.timestamp != b.timestamp)
return a.timestamp < b.timestamp;
return a.level < b.level;
});
return 0;
}
stable_sort: 안정 정렬
같은 값끼리 원래 입력 순서를 유지해야 할 때 사용합니다.
#include <algorithm>
#include <vector>
struct Student {
std::string name;
int score;
};
int main() {
std::vector<Student> students = {
{"Alice", 90}, {"Bob", 85}, {"Charlie", 90}, {"David", 85}
};
std::stable_sort(students.begin(), students.end(),
{ return a.score > b.score; });
// Alice(90), Charlie(90), Bob(85), David(85) - 같은 점수 내 순서 유지
return 0;
}
partial_sort: 상위 k개만 정렬
전체를 정렬할 필요 없이 “상위 N개만 올바른 순서”가 필요할 때 사용합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
// 상위 3개만 정렬 (가장 큰 3개)
std::partial_sort(vec.begin(), vec.begin() + 3, vec.end(),
std::greater<int>());
for (int i = 0; i < 3; ++i)
std::cout << vec[i] << " "; // 9 8 7
std::cout << "\n";
return 0;
}
3. 검색: find
std::find: 값으로 검색
std::find는 선형 검색으로 범위 내에서 value와 같은 첫 번째 원소의 반복자를 반환합니다. 없으면 end를 반환합니다. O(n)이지만 정렬이 필요 없습니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
auto it = std::find(vec.begin(), vec.end(), 8);
if (it != vec.end()) {
std::cout << "Found at index: " << std::distance(vec.begin(), it) << "\n"; // 2
} else {
std::cout << "Not found\n";
}
// 없는 값
auto it2 = std::find(vec.begin(), vec.end(), 100);
if (it2 == vec.end()) {
std::cout << "100 not found\n";
}
return 0;
}
std::find_if: 조건으로 검색
조건(술어)을 만족하는 첫 번째 원소의 반복자를 반환합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
// 5보다 큰 첫 번째 원소
auto it = std::find_if(vec.begin(), vec.end(),
{ return x > 5; });
if (it != vec.end()) {
std::cout << "First > 5: " << *it << "\n"; // 8
}
// 짝수인 첫 번째 원소
auto it2 = std::find_if(vec.begin(), vec.end(),
{ return x % 2 == 0; });
if (it2 != vec.end()) {
std::cout << "First even: " << *it2 << "\n"; // 2
}
return 0;
}
std::find_if_not: 조건을 만족하지 않는 첫 원소
조건을 만족하지 않는 첫 번째 원소의 반복자를 반환합니다.
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {2, 4, 6, 7, 8, 10};
// 홀수인 첫 번째 원소
auto it = std::find_if_not(vec.begin(), vec.end(),
{ return x % 2 == 0; });
if (it != vec.end()) {
// *it == 7
}
return 0;
}
4. 개수: count
std::count: 값과 같은 원소 개수
범위 내에서 value와 같은 원소의 개수를 반환합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 2, 2, 3, 4, 2, 5};
size_t n = std::count(vec.begin(), vec.end(), 2);
std::cout << "Count of 2: " << n << "\n"; // 4
n = std::count(vec.begin(), vec.end(), 10);
std::cout << "Count of 10: " << n << "\n"; // 0
return 0;
}
std::count_if: 조건 만족 개수
조건(술어)을 만족하는 원소의 개수를 반환합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
// 5보다 큰 원소 개수
size_t n = std::count_if(vec.begin(), vec.end(),
{ return x > 5; });
std::cout << "Count > 5: " << n << "\n"; // 3
// 짝수 개수
n = std::count_if(vec.begin(), vec.end(),
{ return x % 2 == 0; });
std::cout << "Even count: " << n << "\n"; // 2
return 0;
}
5. 변환: transform
std::transform: 단항 변환
범위의 각 원소에 함수(람다)를 적용한 결과를 출력 범위에 씁니다. 출력을 입력과 같은 범위로 주면 제자리(in-place) 변환이 됩니다.
#include <algorithm>
#include <vector>
#include <cmath>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int> result(vec.size());
// 각 원소 2배
std::transform(vec.begin(), vec.end(), result.begin(),
{ return x * 2; });
// result: {2, 4, 6, 8, 10}
// 제자리 변환 (원본 수정)
std::transform(vec.begin(), vec.end(), vec.begin(),
{ return x * 2; });
// vec: {2, 4, 6, 8, 10}
// 실수 변환 (sqrt)
std::vector<double> values = {1.0, 4.0, 9.0, 16.0};
std::vector<double> roots(values.size());
std::transform(values.begin(), values.end(), roots.begin(),
{ return std::sqrt(x); });
// roots: {1, 2, 3, 4}
return 0;
}
std::transform: 두 범위 결합 (이항 변환)
두 시퀀스의 같은 위치 원소를 한 번에 받아 결과를 출력합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> a = {1, 2, 3, 4, 5};
std::vector<int> b = {10, 20, 30, 40, 50};
std::vector<int> sum(a.size());
std::transform(a.begin(), a.end(), b.begin(), sum.begin(),
{ return x + y; });
// sum: {11, 22, 33, 44, 55}
return 0;
}
6. 집계: accumulate
std::accumulate: 기본 동작
std::accumulate는 왼쪽부터 차례로 접어 나가는(fold) 연산입니다. 세 번째 인자는 초기값이고, 기본 동작은 초기값 + vec[0] + vec[1] + ...입니다. <numeric> 헤더가 필요합니다.
flowchart LR
I["초기값 0"] --> A1["+ vec[0]"]
A1 --> A2["+ vec[1]"]
A2 --> A3["+ vec[2]"]
A3 --> R["최종 결과"]
#include <numeric>
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
int sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << "Sum: " << sum << "\n"; // 15
int product = std::accumulate(vec.begin(), vec.end(), 1,
{ return a * b; });
std::cout << "Product: " << product << "\n"; // 120
std::vector<std::string> words = {"Hello", " ", "World"};
std::string concat = std::accumulate(words.begin(), words.end(), std::string(),
{ return a + b; });
std::cout << concat << "\n"; // "Hello World"
return 0;
}
accumulate로 평균, 분산 계산
#include <numeric>
#include <vector>
#include <cmath>
double mean(const std::vector<double>& vec) {
if (vec.empty()) return 0;
double sum = std::accumulate(vec.begin(), vec.end(), 0.0);
return sum / vec.size();
}
double variance(const std::vector<double>& vec) {
if (vec.size() < 2) return 0;
double m = mean(vec);
double sq_diff = std::accumulate(vec.begin(), vec.end(), 0.0,
[m](double a, double x) { return a + (x - m) * (x - m); });
return sq_diff / (vec.size() - 1);
}
7. 복사: copy
std::copy: 전체 복사
[first, last) 범위의 원소를 출력 반복자로 복사합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(src.size());
std::copy(src.begin(), src.end(), dst.begin());
// dst: {1, 2, 3, 4, 5}
// back_inserter로 동적 추가
std::vector<int> dst2;
dst2.reserve(src.size());
std::copy(src.begin(), src.end(), std::back_inserter(dst2));
return 0;
}
std::copy_if: 조건 만족 원소만 복사
조건(술어)을 만족하는 원소만 출력 범위로 복사합니다.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> even;
std::copy_if(src.begin(), src.end(), std::back_inserter(even),
{ return x % 2 == 0; });
// even: {2, 4, 6, 8, 10}
// reserve로 재할당 최소화
std::vector<int> result;
result.reserve(src.size());
std::copy_if(src.begin(), src.end(), std::back_inserter(result),
{ return x > 5; });
// result: {6, 7, 8, 9, 10}
return 0;
}
std::copy_n: n개만 복사
처음 n개 원소만 복사합니다.
#include <algorithm>
#include <vector>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(3);
std::copy_n(src.begin(), 3, dst.begin());
// dst: {1, 2, 3}
return 0;
}
8. 제거: remove
remove의 동작 방식 (주의)
std::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(),
{ return x % 2 == 0; }), vec2.end());
// vec2: {1, 3, 5}
return 0;
}
std::remove: 값으로 제거
지정한 값과 같은 모든 원소를 “제거”하고, 새 끝 반복자를 반환합니다.
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 2, 3, 2, 4};
auto new_end = std::remove(vec.begin(), vec.end(), 2);
vec.erase(new_end, vec.end());
// vec: {1, 3, 4}
return 0;
}
std::remove_if: 조건으로 제거
조건을 만족하는 모든 원소를 제거합니다.
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 5보다 큰 원소 제거
vec.erase(std::remove_if(vec.begin(), vec.end(),
{ return x > 5; }), vec.end());
// vec: {1, 2, 3, 4, 5}
return 0;
}
9. 완전한 예제 모음
예제 1: 로그 분석기 (sort + find + count + copy + accumulate)
// g++ -std=c++17 -o log_analyzer log_analyzer.cpp && ./log_analyzer
#include <algorithm>
#include <numeric>
#include <vector>
#include <string>
#include <iostream>
struct LogEntry {
std::string timestamp;
int level; // 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR
std::string message;
};
int main() {
std::vector<LogEntry> logs = {
{"2026-04-23 10:00:00", 2, "Connection timeout"},
{"2026-04-23 09:55:00", 1, "User login"},
{"2026-04-23 10:01:00", 3, "Disk full"},
{"2026-04-23 09:58:00", 1, "Request received"},
{"2026-04-23 10:00:30", 2, "Retry attempt"}
};
// 1. sort: 타임스탬프 오름차순 정렬
std::sort(logs.begin(), logs.end(),
{
return a.timestamp < b.timestamp;
});
// 2. count_if: ERROR(3) 개수
int error_count = std::count_if(logs.begin(), logs.end(),
{ return e.level == 3; });
std::cout << "Errors: " << error_count << "\n";
// 3. copy_if: WARN 이상만 필터링
std::vector<LogEntry> critical;
std::copy_if(logs.begin(), logs.end(), std::back_inserter(critical),
{ return e.level >= 2; });
// 4. find_if: 첫 ERROR 찾기
auto it = std::find_if(logs.begin(), logs.end(),
{ return e.level == 3; });
if (it != logs.end()) {
std::cout << "First error: " << it->message << "\n";
}
// 5. accumulate: level 합계 (평균 레벨 계산용)
int level_sum = std::accumulate(logs.begin(), logs.end(), 0,
{ return a + e.level; });
double avg_level = static_cast<double>(level_sum) / logs.size();
std::cout << "Avg level: " << avg_level << "\n";
return 0;
}
실행 결과:
Errors: 1
First error: Disk full
Avg level: 1.8
예제 2: 점수 처리 파이프라인 (transform + sort + find + remove)
#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>
int main() {
std::vector<int> scores = {72, 85, 91, 68, 78, 88, 95, 62, -1, 0};
// 1. remove_if: 유효하지 않은 점수(-1, 0) 제거
scores.erase(std::remove_if(scores.begin(), scores.end(),
{ return s <= 0; }), scores.end());
// 2. transform: 정규화 (0-100 -> 0.0-1.0)
std::vector<double> normalized(scores.size());
std::transform(scores.begin(), scores.end(), normalized.begin(),
{ return s / 100.0; });
// 3. sort: 상위 5명 (partial_sort)
std::partial_sort(scores.begin(), scores.begin() + 5, scores.end(),
std::greater<int>());
// 4. find: 80점인 사람이 있는지
auto it = std::find(scores.begin(), scores.end(), 80);
bool has_80 = (it != scores.end());
// 5. accumulate: 평균
double avg = std::accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
std::cout << "Average: " << avg << "\n";
return 0;
}
예제 3: 두 벡터 결합 및 집계 (transform + accumulate)
#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>
int main() {
std::vector<int> prices = {100, 200, 150, 300};
std::vector<int> quantities = {2, 1, 3, 2};
// transform: 각 품목별 금액 = price * quantity
std::vector<int> amounts(prices.size());
std::transform(prices.begin(), prices.end(), quantities.begin(),
amounts.begin(), { return p * q; });
// amounts: {200, 200, 450, 600}
// accumulate: 총 금액
int total = std::accumulate(amounts.begin(), amounts.end(), 0);
std::cout << "Total: " << total << "\n"; // 1450
return 0;
}
10. 자주 발생하는 에러와 해결법
에러 1: find 결과를 end 체크 없이 사용
증상: 찾지 못했을 때 *it 접근으로 Segmentation fault 또는 undefined behavior.
원인: std::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 {
// 없을 때 처리
}
에러 2: transform 출력 범위 크기 부족
증상: Segmentation fault 또는 메모리 손상.
원인: 출력 범위가 입력보다 작으면 범위를 벗어나 씁니다.
// ❌ 잘못된 사용
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int> result(2); // 크기 부족!
std::transform(vec.begin(), vec.end(), result.begin(), { return x * 2; });
// ✅ 올바른 사용
std::vector<int> result(vec.size());
std::transform(vec.begin(), vec.end(), result.begin(), { return x * 2; });
// ✅ 또는 back_inserter + reserve
std::vector<int> result;
result.reserve(vec.size());
std::transform(vec.begin(), vec.end(), std::back_inserter(result),
{ return x * 2; });
에러 3: accumulate 초기값 타입 불일치
증상: 정수 합계는 괜찮은데, 부동소수점 합계가 부정확해짐.
원인: std::accumulate(vec.begin(), vec.end(), 0)에서 0은 int이므로, double 벡터를 합할 때 매 단계 int로 변환됩니다.
// ❌ 잘못된 사용 (double 벡터에 int 초기값)
std::vector<double> vec = {0.1, 0.2, 0.3};
double sum = std::accumulate(vec.begin(), vec.end(), 0); // int 0 -> 부정확
// ✅ 올바른 사용
double sum = std::accumulate(vec.begin(), vec.end(), 0.0); // 0.0
에러 4: remove만 하고 erase 안 함
증상: 벡터 크기는 그대로인데, 뒤쪽에 “쓰레기” 값이 남아 있음.
원인: remove/remove_if는 원소를 삭제하지 않고, 새 논리적 끝만 반환합니다.
// ❌ 잘못된 사용
std::vector<int> vec = {1, 2, 2, 3};
std::remove(vec.begin(), vec.end(), 2); // vec 크기 그대로, 뒤에 쓰레기
// ✅ 올바른 사용 (erase-remove idiom)
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
에러 5: 비교자에서 strict weak ordering 위반
증상: std::sort 호출 시 크래시 또는 무한 루프.
원인: 비교 함수가 a < a가 false, a <= b처럼 <=를 쓰면 a == b일 때 true가 되어 위반.
// ❌ 잘못된 비교자
std::sort(vec.begin(), vec.end(),
{ return a <= b; }); // a <= a -> true (위반)
// ✅ 올바른 비교자 (< 만 사용)
std::sort(vec.begin(), vec.end(),
{ return a < b; });
에러 6: copy_if에서 출력 범위 reserve 누락
증상: 대량 데이터에서 copy_if + back_inserter 사용 시 재할당이 반복되어 느려짐.
해결: 결과 크기를 대략 알 수 있으면 reserve로 재할당을 줄입니다.
// ✅ reserve로 재할당 최소화
std::vector<int> result;
result.reserve(src.size()); // 최대 src.size()개
std::copy_if(src.begin(), src.end(), std::back_inserter(result),
{ return x % 2 == 0; });
에러 7: empty 범위에서 accumulate 후 나눗셈
증상: vec.empty()일 때 accumulate 결과가 초기값인데, vec.size()로 나누면 0으로 나누기.
// ✅ empty 체크
double avg = vec.empty()
? 0.0
: std::accumulate(vec.begin(), vec.end(), 0.0) / vec.size();
11. 베스트 프랙티스
1. for문 대신 알고리즘 우선
반복문으로 “찾기, 세기, 복사, 제거”를 직접 구현하기 전에, STL에 해당 알고리즘이 있는지 확인합니다. 가독성과 안정성이 좋아집니다.
// ❌ 수동 루프
int count = 0;
for (size_t i = 0; i < vec.size(); ++i) {
if (vec[i] > 5) ++count;
}
// ✅ count_if
int count = std::count_if(vec.begin(), vec.end(), { return x > 5; });
2. 람다 비교자 사용 (std::function 피하기)
std::function을 넘기면 간접 호출 비용이 생깁니다. 람다는 컴파일러가 인라인하기 쉽습니다.
// ✅ 람다 (인라인 가능)
std::sort(vec.begin(), vec.end(), { return a < b; });
// ❌ std::function (간접 호출)
std::function<bool(int,int)> cmp = { return a < b; };
std::sort(vec.begin(), vec.end(), cmp);
3. reserve로 재할당 최소화
back_inserter와 함께 쓸 때, 결과 크기를 대략 알 수 있으면 reserve를 호출합니다.
std::vector<int> result;
result.reserve(vec.size());
std::copy_if(vec.begin(), vec.end(), std::back_inserter(result),
{ return x > 0; });
4. 정렬이 필요할 때만 정렬
한 번만 검색할 거면 std::find(O(n))가 나을 수 있습니다. 여러 번 검색할 때만 정렬 후 binary_search를 고려합니다.
5. partial_sort로 상위 k개만
전체 정렬이 필요 없으면 partial_sort가 sort보다 빠릅니다.
// 상위 10개만 필요
std::partial_sort(vec.begin(), vec.begin() + 10, vec.end(), std::greater<int>());
12. 프로덕션 패턴
패턴 1: erase-remove idiom
조건에 맞는 원소를 제거할 때 remove_if + erase 조합을 사용합니다.
// 짝수 제거
vec.erase(std::remove_if(vec.begin(), vec.end(),
{ return x % 2 == 0; }),
vec.end());
패턴 2: 정렬 + unique로 중복 제거
std::sort(vec.begin(), vec.end());
vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
패턴 3: 파이프라인 체이닝
여러 알고리즘을 순차 적용해 데이터 파이프라인을 구성합니다.
// 1. copy_if: 필터링
std::vector<int> filtered;
std::copy_if(vec.begin(), vec.end(), std::back_inserter(filtered),
{ return x > 0; });
// 2. transform: 변환
std::vector<double> transformed(filtered.size());
std::transform(filtered.begin(), filtered.end(), transformed.begin(),
{ return std::sqrt(static_cast<double>(x)); });
// 3. accumulate: 집계
double sum = std::accumulate(transformed.begin(), transformed.end(), 0.0);
패턴 4: find + distance로 인덱스 얻기
auto it = std::find(vec.begin(), vec.end(), target);
if (it != vec.end()) {
size_t index = std::distance(vec.begin(), it);
// index 사용
}
패턴 5: count_if로 조건 필터 개수
size_t error_count = std::count_if(logs.begin(), logs.end(),
{ return e.level >= 3; });
패턴 6: transform + accumulate로 가중 합
std::vector<int> values = {1, 2, 3, 4, 5};
std::vector<int> weights = {1, 2, 1, 2, 1};
std::vector<int> weighted(values.size());
std::transform(values.begin(), values.end(), weights.begin(),
weighted.begin(), { return v * w; });
int total = std::accumulate(weighted.begin(), weighted.end(), 0);
성능 비교 요약
| 연산 | 알고리즘 | 복잡도 | 비고 |
|---|---|---|---|
| 정렬 | sort | O(n log n) | 일반적 |
| 정렬 | stable_sort | O(n log n) | 순서 유지 필요 시 |
| 부분 정렬 | partial_sort | O(n log k) | 상위 k개만 |
| 선형 검색 | find | O(n) | 정렬 불필요 |
| 개수 | count/count_if | O(n) | 한 번 순회 |
| 변환 | transform | O(n) | 한 번 순회 |
| 집계 | accumulate | O(n) | 순차 |
| 복사 | copy/copy_if | O(n) | 한 번 순회 |
| 제거 | remove + erase | O(n) | erase-remove |
13. 구현 체크리스트
프로덕션에 STL 알고리즘을 적용할 때 확인할 항목입니다.
-
find/find_if결과 사용 전it != end체크 -
transform출력 범위 크기가 입력 이상인지 확인 (또는back_inserter+reserve) -
accumulate초기값 타입이 집계 결과와 일치하는지 확인 (double이면0.0) - 커스텀 비교자가 strict weak ordering을 만족하는지 확인
-
remove/remove_if후 반드시erase로 논리적 끝 구간 제거 -
copy_if+back_inserter사용 시reserve로 재할당 최소화 - empty 범위에서 나눗셈 시 0으로 나누기 방지
정리
| 항목 | 설명 |
|---|---|
| 정렬 | sort, stable_sort, partial_sort — 상황에 맞게 선택 |
| 검색 | find, find_if, find_if_not — end 체크 필수 |
| 개수 | count, count_if — 한 줄 집계 |
| 변환 | transform — 단항/이항, 출력 범위 크기 확인 |
| 집계 | accumulate — 초기값 타입 일치 |
| 복사 | copy, copy_if — reserve로 재할당 최소화 |
| 제거 | remove + erase — erase-remove idiom |
| 에러 | end 체크, 출력 범위 부족, 비교자 위반, erase 누락 |
| 프로덕션 | erase-remove, sort+unique, 파이프라인 체이닝 |
| 핵심 원칙: |
- find 결과 사용 전 반드시 end 체크
- transform 출력 범위 크기 확보
- accumulate 초기값 타입 일치 (0.0 for double)
- remove/remove_if 후 반드시 erase
- reserve로 불필요한 재할당 방지
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 데이터 정렬, 검색, 집계, 변환, 복사, 제거 등 일상적인 컨테이너 처리에 필수입니다. for문 대신 STL 알고리즘을 쓰면 버그가 줄고 가독성이 올라갑니다.
Q. 선행으로 읽으면 좋은 글은?
A. STL 컨테이너 기초와 람다 표현식을 먼저 익히면 좋습니다.
Q. 더 깊이 공부하려면?
A. cppreference의 algorithm, numeric 헤더 문서와 C++20 Ranges를 참고하세요. 한 줄 요약: sort·find·count·transform·accumulate·copy·remove를 마스터하고, 흔한 에러를 피하며 프로덕션 패턴을 적용할 수 있습니다.
관련 글
- C++ 알고리즘 선택 완벽 가이드 | 상황별 STL·커스텀 알고리즘 선택법 [#54-1]
- C++ STL 알고리즘 완벽 가이드 | sort·transform·accumulate [#54-1]
- C++ 알고리즘 |
- C++ Algorithm Sort |
- C++ Algorithm Search |
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ STL 알고리즘 기초 완벽 가이드 | sort·find」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ STL 알고리즘 기초 완벽 가이드 | sort·find」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ STL 알고리즘 기초 | sort·find·count·transform·accumulate 가이드
- C++ STL 알고리즘 완벽 가이드 | sort·transform·accumulate [#54-1]
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
이 글에서 다루는 키워드 (관련 검색어)
C++, STL, 알고리즘, sort, find, count, transform 등으로 검색하시면 이 글이 도움이 됩니다.