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++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오 상세
- 정렬: sort
- 검색: find
- 개수: count
- 변환: transform
- 집계: accumulate
- 복사: copy
- 제거: remove
- 완전한 예제 모음
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
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 |