C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
이 글의 핵심
std::sort·find·find_if·transform·accumulate·copy·remove·partition·merge 사용법, 람다 표현식과 함께 쓰기, 커스텀 비교자, 실전 패턴(필터링·변환·집계·병합), 자주 발생하는 실수와 베스트 프랙티스.
들어가며: 직접 루프 돌리지 말고 STL 알고리즘을 쓰자
“정렬, 검색, 변환… 매번 for문 작성하기 귀찮아요”
데이터 처리 코드를 작성할 때마다 반복문을 직접 작성했습니다.
STL(Standard Template Library, 표준 템플릿 라이브러리) 알고리즘은 반복자(iterator—컨테이너 요소를 순회하는 객체. 포인터처럼 *it, ++it로 사용) 범위 + 동작으로 표현하므로, 정렬·검색·변환·집계를 한 줄로 쓸 수 있고, 구현이 최적화되어 있어서 직접 for문을 돌리는 것보다 실수도 적고 성능도 나쁘지 않습니다. 람다와 함께 쓰면 복잡한 조건도 표현할 수 있어서, 이 글에서 자주 쓰는 패턴만 익혀 두면 실무 코드가 많이 줄어듭니다.
실무 팁: std::sort, std::find, std::transform, std::accumulate만 익혀도 대부분의 반복 로직을 대체할 수 있고, <algorithm>에 있는 함수는 반복자 범위 [begin, end)를 받으므로 vector·array·일부 커스텀 컨테이너에서 동일하게 사용할 수 있습니다.
실무에서 자주 겪는 문제 시나리오
| 시나리오 | 문제 | STL로 해결 |
|---|---|---|
| 로그 분석 | 수백만 줄 로그에서 특정 에러 패턴이 처음 나오는 위치 찾기 | find_if + 람다 |
| 주문 대시보드 | 완료된 주문만 필터링해 금액순 정렬 후 상위 10개 합계 | copy_if → sort → accumulate |
| 사용자 목록 | 활성 사용자만 앞으로 모아 비활성 사용자와 분리 | partition |
| 두 정렬된 리스트 병합 | A팀·B팀 점수표를 합쳐 전체 순위표 만들기 | merge |
| 데이터 정제 | 벡터에서 조건에 맞지 않는 항목 제거 | remove_if + erase |
| 집계 리포트 | 매출 합계, 평균, 최대값 한 번에 계산 | accumulate, max_element |
위 시나리오들은 모두 수동 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;
}
}
// 변환 (모든 값 2배)
for (size_t i = 0; i < vec.size(); ++i) {
vec[i] *= 2;
}
위 코드 설명: 정렬·검색·변환을 모두 수동 for로 구현하면 코드가 길어지고 버그가 생기기 쉽습니다. 이중 루프 정렬은 O(n²), 선형 검색은 O(n)이고, STL 알고리즘은 같은 동작을 한 줄로 표현하면서 구현이 최적화되어 있습니다.
STL 알고리즘으로 해결:
#include <algorithm>
std::vector<int> vec = {5, 2, 8, 1, 9};
// 정렬
std::sort(vec.begin(), vec.end());
// 검색
auto it = std::find(vec.begin(), vec.end(), 8);
if (it != vec.end()) {
int index = std::distance(vec.begin(), it);
}
// 변환
std::transform(vec.begin(), vec.end(), vec.begin(),
{ return x * 2; });
위 코드 설명: sort(begin, end)로 오름차순 정렬하고, find로 값 8의 위치 반복자를 얻은 뒤 distance로 인덱스를 구합니다. transform은 각 원소에 람다를 적용해 결과를 같은 벡터에 다시 쓰면 제자리에서 2배로 바뀝니다. 반복자 범위 [begin, end)만 맞추면 컨테이너에 관계없이 같은 방식으로 쓸 수 있습니다.
장점:
- 코드가 짧고 명확함
- 버그 가능성 낮음
- 최적화된 구현
이 글을 읽으면:
- STL 알고리즘의 핵심 함수들을 사용할 수 있습니다.
- 람다 표현식을 활용할 수 있습니다.
- 실전에서 자주 쓰는 패턴을 익힐 수 있습니다.
- 직접 루프를 작성하는 대신 STL을 활용할 수 있습니다.
목차
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[for_each]
end
subgraph agg["집계 알고리즘"]
A1[accumulate]
A2[count / count_if]
A3[all_of / any_of / none_of]
end
subgraph util["유틸리티"]
U1[remove / remove_if]
U2[copy / copy_if]
U3[max_element / min_element]
U4[merge / inplace_merge]
end
input["(begin, end) 반복자 범위"] --> sort
input --> search
input --> transform
input --> agg
input --> util
위 다이어그램 설명: STL 알고리즘은 모두 반복자 범위 [begin, end)를 받아 동작합니다. 정렬·검색·변환·집계·유틸리티로 분류되며, 람다와 함께 사용하면 복잡한 조건도 표현할 수 있습니다.
1. 정렬 알고리즘
sort vs stable_sort vs partial_sort 비교
flowchart LR
subgraph sort["sort"]
S1["같은 값 순서br/보장 안 함"]
S2["가장 빠름"]
end
subgraph stable["stable_sort"]
ST1["같은 값 순서br/유지"]
ST2["메모리 더 사용"]
end
subgraph partial["partial_sort"]
P1["상위 k개만br/정렬"]
P2["k 작으면br/더 빠름"]
end
위 다이어그램 설명: sort는 일반 정렬에 가장 빠르지만 같은 값의 순서를 보장하지 않습니다. stable_sort는 같은 값끼리 원래 순서를 유지하지만 메모리를 더 씁니다. partial_sort는 상위 k개만 필요할 때 전체 정렬보다 효율적입니다.
std::sort: 기본 정렬
std::sort는 반개구간 [begin, end) 에 있는 원소를 기본적으로 오름차순으로 정렬합니다. 내부적으로 보통 퀵소트 계열 알고리즘을 쓰며, 비교 연산만 정의되어 있으면 어떤 타입이든 정렬할 수 있습니다. 내림차순이 필요하면 세 번째 인자로 std::greater<int>() 같은 비교 함수(또는 함수 객체)를 넘기면 됩니다.
// 복사해 붙여넣은 뒤: 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)으로 정렬해 1 2 5 8 9가 됩니다. 세 번째 인자로 std::greater<int>()를 넘기면 내림차순으로 9 8 5 2 1이 됩니다. 비교만 가능한 타입이면 어떤 원소든 정렬할 수 있습니다.
실행 결과: 첫 줄에 1 2 5 8 9, 둘째 줄에 9 8 5 2 1 이 출력됩니다.
커스텀 비교 함수
원소가 숫자가 아니라 구조체나 클래스일 때는 “어떤 기준으로 순서를 정할지”를 비교 함수로 넘겨야 합니다. 람다 { return a.age < b.age; }는 “a가 b보다 앞에 오려면 a.age가 b.age보다 작아야 한다”는 뜻이므로, 나이 오름차순 정렬이 됩니다. std::greater처럼 “a가 b보다 뒤에 오려면” 조건을 반대로 주면 내림차순이 됩니다.
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면 나이 오름차순이 되고, 반환값이 true일 때 a가 b보다 앞에 옵니다. greater처럼 반대 조건을 주면 내림차순이 됩니다.
stable_sort: 안정 정렬
std::sort는 같은 값끼리의 원래 순서를 보장하지 않습니다. 예를 들어 점수가 같은 Alice와 Charlie의 순서가 바뀔 수 있습니다. 안정 정렬이 필요할 때는 std::stable_sort를 사용합니다. 비교 결과가 같은 원소들 사이에서는 입력 순서가 그대로 유지되므로, “점수로 정렬하되 같은 점수면 원래 순서 유지” 같은 요구사항에 맞습니다.
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)
// 같은 점수 내에서 원래 순서 유지됨
}
위 코드 설명: std::sort는 동일한 값끼리 원래 순서를 보장하지 않습니다. stable_sort는 비교 결과가 같은 원소들(같은 점수) 사이에서는 입력 순서를 유지하므로, “점수로 정렬하되 같은 점수면 원래 순서 유지”가 필요할 때 사용합니다.
partial_sort: 부분 정렬
전체를 정렬할 필요 없이 “상위 N개만 올바른 순서로 알고 싶다” 할 때 std::partial_sort를 씁니다. 인자는 (정렬 결과를 쓸 시작, 정렬 결과를 쓸 끝, 전체 범위 끝)입니다. 예를 들어 vec.begin() + 3까지를 정렬하면, 전체 중 가장 작은(또는 비교 함수에 따라 가장 큰) 3개만 그 구간에 들어가고, 나머지 원소들의 순서는 정해지지 않습니다. “상위 3명만 뽑기” 같은 경우에 유용합니다.
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
// 상위 3개만 정렬
std::partial_sort(vec.begin(), vec.begin() + 3, vec.end(),
std::greater<int>());
// {9, 8, 7, ...} (나머지는 순서 보장 안 됨)
위 코드 설명: partial_sort(시작, 정렬끝, 전체끝)은 “시작~정렬끝” 구간만 올바른 순서로 채웁니다. greater를 쓰면 그 구간에 상위 3개(9, 8, 7)만 들어가고, 나머지 원소 순서는 정해지지 않습니다. “상위 N개만 필요할 때” 전체 정렬보다 빠릅니다.
std::merge: 두 정렬된 시퀀스 병합
std::merge는 이미 정렬된 두 범위를 하나의 정렬된 범위로 병합합니다. 병합 정렬(merge sort)의 핵심 연산이며, 두 팀의 점수표를 합치거나 여러 정렬된 로그 스트림을 하나로 합칠 때 사용합니다. 입력 범위 두 개가 모두 정렬되어 있어야 올바른 결과가 나옵니다. 출력 범위는 입력 두 범위 길이의 합만큼 공간이 있어야 합니다.
std::vector<int> a = {1, 3, 5, 7, 9}; // 정렬됨
std::vector<int> b = {2, 4, 6, 8, 10}; // 정렬됨
std::vector<int> result;
result.reserve(a.size() + b.size());
std::merge(a.begin(), a.end(), b.begin(), b.end(),
std::back_inserter(result));
// result: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
위 코드 설명: merge는 두 정렬된 범위를 하나의 정렬된 시퀀스로 합칩니다. back_inserter로 출력하면 크기를 미리 지정할 필요가 없고, O(n + m) 시간에 동작합니다.
제자리 병합 (inplace_merge): [begin, mid)와 [mid, end) 두 정렬된 구간을 제자리에서 병합할 때 std::inplace_merge(begin, mid, end)를 사용합니다. 병합 정렬 구현 시 유용합니다.
2. 검색 알고리즘
std::find: 선형 검색
std::find는 값이 같은 첫 번째 원소를 찾아 그 위치를 가리키는 반복자를 반환합니다. 없으면 vec.end()를 반환하므로, 반환값이 end()인지 꼭 확인한 뒤 사용해야 합니다. 인덱스가 필요하면 std::distance(vec.begin(), it)로 구할 수 있습니다. 정렬이 되어 있지 않아도 되지만, 복잡도는 선형 O(n)입니다.
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";
}
위 코드 설명: find(begin, end, value)는 값과 같은 첫 원소의 반복자를 반환하고, 없으면 end()를 반환합니다. it != vec.end()로 있는지 확인한 뒤, distance(vec.begin(), it)로 인덱스를 구할 수 있습니다. 정렬이 없어도 되지만 선형 O(n)입니다.
std::find_if: 조건 검색
특정 값이 아니라 “짝수”, “이름이 A로 시작”처럼 조건을 만족하는 첫 원소를 찾을 때는 std::find_if를 씁니다. 세 번째 인자로 predicate(참/거짓을 반환하는 함수나 람다)를 넘기면, 조건이 참이 되는 첫 반복자를 반환합니다. 역시 없으면 end()를 반환합니다.
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(참/거짓을 반환하는 람다)를 받아, 조건을 만족하는 첫 원소의 반복자를 반환합니다. x % 2 == 0이면 첫 번째 짝수(2)를 찾습니다. 없으면 역시 end()를 반환합니다.
std::binary_search: 이진 검색
범위가 이미 정렬되어 있을 때는 선형 검색 대신 이진 검색을 쓰면 O(log n)에 “값이 존재하는지”만 알 수 있습니다. std::binary_search는 참/거짓만 반환하고, “그 위치”가 필요하면 아래의 lower_bound / upper_bound를 사용합니다.
std::vector<int> vec = {1, 2, 3, 4, 5}; // 정렬되어 있어야 함
if (std::binary_search(vec.begin(), vec.end(), 3)) {
std::cout << "Found\n";
}
위 코드 설명: 범위가 이미 정렬되어 있을 때 binary_search(begin, end, value)는 “값이 존재하는지”만 true/false로 반환합니다. O(log n)에 판별할 수 있지만, “그 위치(반복자)“가 필요하면 lower_bound/upper_bound를 씁니다.
std::lower_bound / upper_bound
정렬된 범위에서 “이 값 이상인 첫 위치”가 필요할 때는 std::lower_bound, “이 값보다 큰 첫 위치”가 필요할 때는 std::upper_bound를 씁니다. 같은 값이 여러 개 있을 때 그 구간을 [lower_bound, upper_bound)로 표현할 수 있어서, 개수나 삽입 위치를 구할 때 자주 쓰입니다.
std::vector<int> vec = {1, 2, 2, 2, 3, 4, 5};
// 2 이상인 첫 위치
auto lower = std::lower_bound(vec.begin(), vec.end(), 2);
std::cout << "Lower: " << std::distance(vec.begin(), lower) << "\n"; // 1
// 2 초과인 첫 위치
auto upper = std::upper_bound(vec.begin(), vec.end(), 2);
std::cout << "Upper: " << std::distance(vec.begin(), upper) << "\n"; // 4
위 코드 설명: lower_bound(begin, end, 2)는 “2 이상인 첫 위치”, upper_bound(begin, end, 2)는 “2 초과인 첫 위치”의 반복자를 줍니다. 같은 값이 여러 개일 때 [lower, upper) 구간이 그 값들 전체이므로, 개수나 삽입 위치를 구할 때 자주 씁니다.
3. 변환 알고리즘
std::transform: 각 원소 변환
std::transform은 범위의 각 원소에 함수(또는 람다)를 적용한 결과를 다른 범위에 씁니다. 입력 범위 두 개와 출력 범위 하나를 받는 오버로드는 두 시퀀스를 한 원소씩 짝 지어서(예: 더하기) 결과를 쓸 때 사용합니다. 출력을 입력과 같은 범위로 주면 제자리(in-place) 변환이 됩니다. 원본을 바꾸지 않으려면 미리 result 같은 컨테이너를 준비해 두고 그 쪽에 쓰면 됩니다.
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}
위 코드 설명: transform(입력시작, 입력끝, 출력시작, 단항함수)는 각 원소에 람다를 적용한 결과를 출력 범위에 씁니다. 출력을 result.begin()으로 주면 새 벡터에 쓰고, vec.begin()으로 주면 제자리(in-place)에서 vec 자체가 바뀝니다.
두 컨테이너 결합
transform에 입력 반복자를 두 쌍 주면, 두 시퀀스의 같은 위치 원소를 한 번에 받는 이항 함수를 쓸 수 있습니다. 위 예는 a[i] + b[i]를 result[i]에 쓰는 형태입니다. 두 범위의 길이가 다르면 짧은 쪽까지만 처리되므로, 길이가 같다고 가정하고 사용하는 것이 안전합니다.
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}
위 코드 설명: transform에 입력 반복자를 두 쌍(첫 번째 범위, 두 번째 범위)과 출력 시작을 주면, 두 원소를 받는 이항 람다 (x, y) -> x + y 로 같은 위치끼리 더한 결과를 result에 씁니다. 두 범위 길이가 다르면 짧은 쪽까지만 처리됩니다.
std::for_each: 각 원소에 함수 적용
std::for_each는 각 원소에 void를 반환하는 동작을 적용할 때 씁니다. 출력하거나, 참조로 받아서 원소를 수정하는 식으로 사용할 수 있습니다. “새 컨테이너를 만드는” 변환이 아니라 “반복만 하고 부수 효과만 필요할 때” 적합하고, 범위 기반 for문으로 대체할 수도 있습니다.
std::vector<int> vec = {1, 2, 3, 4, 5};
// 각 원소 출력
std::for_each(vec.begin(), vec.end(),
{ std::cout << x << " "; });
// 1 2 3 4 5
// 각 원소 수정
std::for_each(vec.begin(), vec.end(),
{ x *= 2; });
// vec: {2, 4, 6, 8, 10}
위 코드 설명: for_each는 각 원소에 void를 반환하는 동작(출력, 수정 등)을 적용합니다. 람다가 int x를 받으면 복사본만 바꾸고, int& x를 받으면 원소를 직접 수정할 수 있습니다. “새 컨테이너를 만드는” 변환이 아니라 부수 효과만 필요할 때 적합합니다.
std::copy / copy_if: 범위 복사
std::copy는 입력 범위 전체를 출력 범위로 복사합니다. std::copy_if는 조건(predicate)을 만족하는 원소만 복사합니다. 출력 범위는 입력과 겹치지 않아야 하며, copy는 출력 크기가 입력 이상이어야 하고, copy_if는 back_inserter를 쓰면 크기를 미리 알 필요가 없습니다.
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(src.size());
// 전체 복사
std::copy(src.begin(), src.end(), dst.begin());
// 조건에 맞는 것만 복사 (짝수만)
std::vector<int> evens;
std::copy_if(src.begin(), src.end(), std::back_inserter(evens),
{ return x % 2 == 0; });
// evens: {2, 4}
// copy_n: 처음 n개만 복사
std::vector<int> first3(3);
std::copy_n(src.begin(), 3, first3.begin());
위 코드 설명: copy는 전체 복사, copy_if는 조건에 맞는 원소만 복사합니다. 출력이 입력과 겹치면 std::copy_backward를 사용합니다.
4. 집계 알고리즘
accumulate 동작 흐름
std::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] + ... 형태로 합을 구합니다. 네 번째 인자로 이항 함수를 주면 곱셈, 문자열 연결 등 다른 집계도 가능합니다.
std::accumulate: 합산
std::accumulate는 범위를 왼쪽부터 하나씩 접어 나가는(fold) 연산입니다. 세 번째 인자는 초기값이고, 기본 동작은 초기값 + vec[0] + vec[1] + ...처럼 합을 구하는 것입니다. 네 번째 인자로 이항 함수를 주면 곱셈, 문자열 연결, 커스텀 구조체 합치기 등 어떤 집계든 표현할 수 있습니다.
#include <numeric>
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
위 코드 설명: accumulate(begin, end, 초기값)은 기본적으로 초기값 + 원소들을 순서대로 더한 합을 반환합니다. 네 번째 인자로 이항 함수를 주면 곱셈(초기값 1), 문자열 연결 등 다른 집계도 할 수 있습니다. <numeric> 헤더가 필요합니다.
std::count / count_if
std::count는 지정한 값과 같은 원소의 개수를 반환하고, std::count_if는 조건을 만족하는 원소의 개수를 반환합니다. 둘 다 범위를 한 번 훑으므로 O(n)이며, 정렬 여부와 상관없이 사용할 수 있습니다.
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::all_of / any_of / none_of
조건이 범위 전체에 대해 한 번에 참/거짓인지 알고 싶을 때 쓰는 논리 연산입니다. all_of는 모든 원소가 조건을 만족할 때만 true, any_of는 하나라도 만족하면 true, none_of는 하나도 만족하지 않을 때 true를 반환합니다. 최대 한 번 훑다가 결과가 확정되면 바로 반환하므로, 불필요한 루프를 줄일 수 있습니다.
std::vector<int> vec = {2, 4, 6, 8};
// 모두 짝수?
bool allEven = std::all_of(vec.begin(), vec.end(),
{ return x % 2 == 0; });
std::cout << "All even: " << allEven << "\n"; // true
// 하나라도 10 이상?
bool anyLarge = std::any_of(vec.begin(), vec.end(),
{ return x >= 10; });
std::cout << "Any >= 10: " << anyLarge << "\n"; // false
// 모두 홀수가 아님?
bool noneOdd = std::none_of(vec.begin(), vec.end(),
{ return x % 2 == 1; });
std::cout << "None odd: " << noneOdd << "\n"; // true
위 코드 설명: all_of는 모든 원소가 조건을 만족할 때만 true, any_of는 하나라도 만족하면 true, none_of는 하나도 만족하지 않을 때 true를 반환합니다. 결과가 확정되면 더 이상 훑지 않고 반환하므로 불필요한 루프를 줄일 수 있습니다.
5. 실전 패턴
패턴 1: erase-remove idiom
std::remove와 std::remove_if는 실제로 원소를 지우지 않고, 제거할 값이 아닌 것들을 앞쪽으로 모은 뒤, “새 논리적 끝”을 가리키는 반복자를 반환합니다. 그래서 그 반환값부터 vec.end()까지를 erase로 지워야 진짜 크기가 줄어듭니다. 이 조합을 erase-remove idiom이라고 부르며, vector에서 조건에 맞는 원소를 제거할 때 표준적으로 쓰는 방법입니다.
erase-remove 동작 흐름:
sequenceDiagram
participant V as vector
participant R as remove
participant E as erase
Note over V: {1, 2, 3, 2, 4, 2, 5}
V->>R: remove(2)
Note over R: "2가 아닌 것"만 앞으로 이동
R->>V: 새 논리적 끝 반복자 반환
Note over V: {1, 3, 4, 5, ?, ?, ?} size=7
V->>E: erase(반환값, end())
E->>V: 뒤쪽 "쓰레기" 구간 제거
Note over V: {1, 3, 4, 5} size=4
위 다이어그램 설명: remove는 값 2를 지우지 않고, 2가 아닌 원소(1, 3, 4, 5)만 앞으로 모읍니다. 반환된 반복자부터 end()까지가 “논리적으로 제거된” 구간이므로, erase로 이 구간을 지워야 실제 크기가 줄어듭니다.
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
// 모든 2 제거
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
// {1, 3, 4, 5}
// 조건 제거 (짝수 제거)
vec.erase(std::remove_if(vec.begin(), vec.end(),
{ return x % 2 == 0; }),
vec.end());
위 코드 설명: remove/remove_if는 원소를 지우지 않고 “지울 값이 아닌 것”만 앞으로 모은 뒤 새 논리적 끝 반복자를 반환합니다. erase(그 반복자, vec.end())로 그 구간을 지워야 실제 크기가 줄어듭니다. 이 조합을 erase-remove idiom이라고 부릅니다.
패턴 2: 중복 제거
std::unique는 연속된 같은 값만 하나로 묶고, “중복이 제거된 구간의 끝” 반복자를 반환합니다. 따라서 정렬된 벡터에 써야 “값 기준으로” 중복이 제거됩니다. 정렬 후 unique를 적용한 다음, 반환된 반복자부터 end까지 erase하면 됩니다.
std::vector<int> vec = {1, 2, 2, 3, 3, 3, 4};
// 1. 정렬 (이미 정렬되어 있으면 생략)
std::sort(vec.begin(), vec.end());
// 2. unique로 중복 제거
vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
// {1, 2, 3, 4}
위 코드 설명: unique는 연속된 같은 값만 하나로 묶고, “중복 제거된 구간의 끝” 반복자를 반환합니다. 정렬된 벡터에 써야 값 기준으로 중복이 제거되고, 반환 반복자부터 end()까지 erase하면 실제 크기가 줄어듭니다.
패턴 3: 조건 분할
std::partition은 조건이 참인 원소를 앞쪽으로, 거짓인 원소를 뒤쪽으로 모읍니다. 반환값은 “거짓인 구간의 첫 반복자”이므로, [begin, pivot)이 참인 원소, [pivot, end)가 거짓인 원소입니다. 원소들 사이의 상대 순서는 보장되지 않으며, 순서를 유지하려면 std::stable_partition을 사용합니다.
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
// 짝수를 앞으로, 홀수를 뒤로
auto pivot = std::partition(vec.begin(), vec.end(),
{ return x % 2 == 0; });
// {2, 4, 6, 1, 3, 5} (순서는 보장 안 됨)
// pivot은 홀수 시작 위치
위 코드 설명: partition(begin, end, predicate)은 조건이 참인 원소를 앞쪽으로, 거짓인 원소를 뒤쪽으로 모읍니다. 반환값은 “거짓인 구간의 첫 반복자”이므로 [begin, pivot)이 참, [pivot, end)가 거짓입니다. 원래 순서를 유지하려면 stable_partition을 씁니다.
패턴 4: 최대/최소 찾기
std::max_element와 std::min_element는 최대/최소인 원소의 위치(반복자)를 반환합니다. 값이 필요하면 *maxIt처럼 역참조하면 됩니다. std::minmax_element는 한 번의 순회로 최소·최대 반복자 둘 다 반환하므로, 두 번 훑을 필요가 없습니다. 비교 기준을 바꾸려면 마지막 인자로 비교 함수를 넘기면 됩니다.
std::vector<int> vec = {5, 2, 8, 1, 9};
// 최대값
auto maxIt = std::max_element(vec.begin(), vec.end());
std::cout << "Max: " << *maxIt << "\n"; // 9
// 최소값
auto minIt = std::min_element(vec.begin(), vec.end());
std::cout << "Min: " << *minIt << "\n"; // 1
// 최대/최소 동시
auto [min, max] = std::minmax_element(vec.begin(), vec.end());
std::cout << "Min: " << *min << ", Max: " << *max << "\n";
위 코드 설명: max_element/min_element는 최대/최소인 원소의 반복자를 반환하고, 값은 *반복자로 얻습니다. minmax_element는 한 번의 순회로 최소·최대 반복자 쌍을 반환해, 두 번 훑을 필요가 없습니다. 비교 기준을 바꾸려면 마지막 인자로 비교 함수를 넘깁니다.
패턴 5: 복사와 필터링
std::copy_if는 조건을 만족하는 원소만 출력 반복자로 복사합니다. std::back_inserter(result)를 쓰면 result에 push_back으로 추가되므로, 결과 컨테이너 크기를 미리 알 필요가 없습니다. 원본은 그대로 두고 짝수만 모은 새 벡터를 만들 때 유용합니다.
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
std::vector<int> result;
// 짝수만 복사
std::copy_if(vec.begin(), vec.end(), std::back_inserter(result),
{ return x % 2 == 0; });
// result: {2, 4, 6}
위 코드 설명: copy_if(begin, end, 출력반복자, predicate)는 조건을 만족하는 원소만 출력 반복자로 복사합니다. back_inserter(result)를 쓰면 result.push_back으로 추가되므로 결과 크기를 미리 잡을 필요가 없습니다. 원본은 그대로 두고 조건에 맞는 원소만 새 벡터로 뽑을 때 유용합니다.
패턴 6: 문자열 변환
std::string도 반복자를 제공하므로 std::transform을 그대로 쓸 수 있습니다. 각 문자에 std::toupper / std::tolower를 적용하면 대소문자 변환이 됩니다. 출력 범위를 자기 자신으로 주면 제자리 변환이 됩니다.
std::string str = "Hello World";
// 대문자로 변환
std::transform(str.begin(), str.end(), str.begin(),
{ return std::toupper(c); });
// "HELLO WORLD"
// 소문자로 변환
std::transform(str.begin(), str.end(), str.begin(),
{ return std::tolower(c); });
// "hello world"
위 코드 설명: string도 begin()/end()를 제공하므로 transform을 그대로 쓸 수 있습니다. 각 문자에 toupper/tolower를 적용하고 출력 범위를 str.begin()으로 주면 제자리에서 대소문자가 바뀝니다.
패턴 7: 실전 통합 예시 — 주문 데이터 처리
여러 알고리즘을 조합해 실무에서 자주 쓰는 패턴을 구현한 예시입니다. 주문 목록에서 “완료된 주문만 필터링 → 금액순 정렬 → 상위 5개 합계”를 한 흐름으로 처리합니다.
#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>
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),
{ return o.status == "completed"; });
// 2. 금액 내림차순 정렬
std::sort(completed.begin(), completed.end(),
{ return a.amount > b.amount; });
// 3. 상위 5개 금액 합계
size_t topN = std::min(size_t(5), completed.size());
double total = std::accumulate(completed.begin(), completed.begin() + topN, 0.0,
{ return sum + o.amount; });
std::cout << "Top 5 completed orders total: " << total << "\n"; // 770.0
return 0;
}
위 코드 설명: copy_if로 조건에 맞는 원소만 추출하고, sort로 정렬한 뒤, accumulate로 상위 N개 합계를 구합니다. 이처럼 알고리즘을 파이프라인처럼 연결하면 가독성과 유지보수성이 좋아집니다.
6. 자주 발생하는 실수
실수 1: find/binary_search 반환값 혼동
문제: std::find는 반복자를, std::binary_search는 bool을 반환합니다. binary_search로 “위치”를 얻으려고 하면 컴파일 에러가 납니다.
// ❌ 잘못된 사용: binary_search는 위치를 반환하지 않음
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = std::binary_search(vec.begin(), vec.end(), 3); // 컴파일 에러! bool 반환
// ✅ 올바른 사용: 위치가 필요하면 lower_bound 사용
auto it = std::lower_bound(vec.begin(), vec.end(), 3);
if (it != vec.end() && *it == 3) {
std::cout << "Found at index: " << std::distance(vec.begin(), it) << "\n";
}
해결법: “존재 여부”만 필요하면 binary_search, “위치(반복자)“가 필요하면 lower_bound를 사용합니다.
실수 2: remove 후 erase 누락
문제: std::remove는 원소를 실제로 지우지 않고, “지울 값이 아닌 것”만 앞으로 모은 뒤 새 논리적 끝 반복자를 반환합니다. erase를 호출하지 않으면 벡터 크기가 그대로라서, 뒤쪽에 “쓰레기” 값이 남습니다.
// ❌ 잘못된 사용: remove만 하고 erase 안 함
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
std::remove(vec.begin(), vec.end(), 2);
// vec.size()는 여전히 7! 뒤쪽에 2가 남아 있을 수 있음
// ✅ 올바른 사용: erase-remove idiom
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
// vec: {1, 3, 4, 5}, size: 4
해결법: remove/remove_if의 반환값부터 end()까지 erase로 지워야 실제 크기가 줄어듭니다.
실수 3: 정렬되지 않은 범위에 binary_search 사용
문제: binary_search, lower_bound, upper_bound는 정렬된 범위에서만 올바르게 동작합니다. 정렬되지 않은 벡터에 쓰면 잘못된 결과가 나옵니다.
// ❌ 잘못된 사용: 정렬되지 않은 범위
std::vector<int> vec = {5, 2, 8, 1, 9};
if (std::binary_search(vec.begin(), vec.end(), 8)) { // 미정의 동작!
// ...
}
// ✅ 올바른 사용: 먼저 정렬
std::sort(vec.begin(), vec.end());
if (std::binary_search(vec.begin(), vec.end(), 8)) {
// ...
}
해결법: 이진 검색 전에 반드시 std::sort로 정렬하거나, 처음부터 정렬된 컨테이너(set, map)를 사용합니다.
실수 4: transform 출력 범위 크기 부족
문제: transform의 출력 범위가 입력보다 작으면 버퍼 오버런이 발생합니다. 미리 result.reserve()만 하고 size()를 늘리지 않으면, result.begin()에 쓰는 동안 범위를 벗어납니다.
// ❌ 잘못된 사용: result가 비어 있음
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int> result; // size=0
std::transform(vec.begin(), vec.end(), result.begin(), // 💥 버퍼 오버런!
{ return x * 2; });
// ✅ 올바른 사용 1: 크기 미리 지정
std::vector<int> result(vec.size());
std::transform(vec.begin(), vec.end(), result.begin(),
{ return x * 2; });
// ✅ 올바른 사용 2: back_inserter 사용 (copy_if 등에서)
std::vector<int> result;
std::transform(vec.begin(), vec.end(), std::back_inserter(result),
{ return x * 2; });
해결법: 출력 범위 크기를 입력과 맞추거나, std::back_inserter로 push_back하게 합니다. transform은 back_inserter와 함께 쓸 수 있습니다.
실수 5: 비교 함수에서 strict weak ordering 위반
문제: std::sort의 비교 함수는 strict weak ordering을 만족해야 합니다. a < b와 b < a가 동시에 true이거나, a < a가 true가 되면 미정의 동작입니다.
// ❌ 잘못된 사용: <= 사용 (a == b일 때 true 반환)
std::sort(vec.begin(), vec.end(),
{ return a <= b; }); // 💥 미정의 동작
// ✅ 올바른 사용: < 사용
std::sort(vec.begin(), vec.end(),
{ return a < b; });
해결법: 비교 함수는 a < b 형태로 작성하고, <=는 사용하지 않습니다. 같을 때는 false를 반환해야 합니다.
실수 6: merge 입력이 정렬되지 않음
문제: std::merge는 두 입력 범위가 모두 정렬되어 있어야 올바른 결과를 냅니다.
// ❌ 잘못된 사용
std::vector<int> a = {5, 1, 9}, b = {8, 2, 6};
std::merge(a.begin(), a.end(), b.begin(), b.end(), ...); // 순서 보장 안 됨
// ✅ 올바른 사용: 먼저 정렬
std::sort(a.begin(), a.end());
std::sort(b.begin(), b.end());
std::merge(a.begin(), a.end(), b.begin(), b.end(), ...);
실수 7: copy 시 출력 범위와 입력 범위 겹침
문제: std::copy에서 출력 범위가 입력 범위와 겹치면 미정의 동작입니다. 왼쪽에서 오른쪽으로 복사할 때 겹치는 구간이 있으면 덮어쓰기 순서가 꼬입니다.
// ❌ 잘못된 사용: 출력이 입력과 겹침
std::vector<int> vec = {1, 2, 3, 4, 5};
std::copy(vec.begin(), vec.end(), vec.begin() + 2); // 💥 미정의 동작
// ✅ 올바른 사용: 겹칠 때는 copy_backward
std::copy_backward(vec.begin(), vec.end() - 2, vec.end());
해결법: 입력과 출력이 겹치는 구간이 있으면 std::copy_backward를 사용하거나, 임시 버퍼를 써서 겹치지 않게 합니다.
실수 8: partition 후 pivot 경계 오해
문제: std::partition의 반환값은 “조건이 거짓인 구간의 첫 반복자”입니다. [begin, pivot)이 참, [pivot, end)가 거짓입니다. 개수가 필요하면 std::distance(vec.begin(), pivot)를 사용합니다.
7. 성능 고려사항
정렬 알고리즘 선택
| 알고리즘 | 시간 복잡도 | 안정성 | 사용 시점 |
|---|---|---|---|
sort | O(n log n) 평균 | ❌ | 일반적인 정렬, 가장 빠름 |
stable_sort | O(n log n) | ✅ | 같은 값의 순서 유지 필요 시 |
partial_sort | O(n log k) | ❌ | 상위 k개만 필요할 때 |
실무 팁: “상위 10명만 뽑기”처럼 k가 작으면 partial_sort가 전체 sort보다 빠릅니다. “점수 같으면 등록 순서 유지”가 필요하면 stable_sort를 씁니다.
검색 알고리즘 선택
flowchart TD
A[검색 필요] --> B{정렬되어 있나?}
B -->|아니오| C["find / find_ifbr/O(n) 선형 검색"]
B -->|예| D{위치가 필요한가?}
D -->|아니오| E["binary_searchbr/O(log n)"]
D -->|예| F{같은 값 여러 개?}
F -->|아니오| G["lower_boundbr/위치 반환"]
F -->|예| H["lower_bound + upper_boundbr/(lower, upper) 구간"]
실무 팁: 검색이 자주 일어나면 vector 대신 set/map을 고려합니다. 삽입·삭제가 많으면 unordered_set/unordered_map이 평균 O(1)로 더 빠를 수 있습니다.
reserve와 알고리즘 조합
copy_if, transform으로 새 컨테이너를 채울 때, 결과 크기를 대략 알 수 있으면 reserve로 재할당을 줄일 수 있습니다.
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> result;
result.reserve(vec.size() / 2); // 짝수만 복사 → 대략 절반
std::copy_if(vec.begin(), vec.end(), std::back_inserter(result),
{ return x % 2 == 0; });
위 코드 설명: reserve로 대략적인 크기를 잡아 두면 push_back 시 재할당 횟수가 줄어듭니다. 정확한 크기를 모르면 vec.size() 전체를 예약해도 됩니다(최대한 많이 복사될 경우).
반복자 무효화 주의
vector에서 erase를 하면, 그 위치 이후의 반복자가 무효화됩니다. 루프 안에서 erase할 때는 반환값을 받아 사용해야 합니다.
// ❌ 잘못된 사용: erase 후 it++ → 무효화된 반복자 사용
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it % 2 == 0) vec.erase(it); // 💥 it 무효화
}
// ✅ 올바른 사용
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it % 2 == 0) it = vec.erase(it);
else ++it;
}
// ✅ 더 나은 방법: erase-remove idiom
vec.erase(std::remove_if(vec.begin(), vec.end(),
{ return x % 2 == 0; }),
vec.end());
8. 알고리즘 선택 가이드
작업별 추천 함수
| 하고 싶은 일 | 추천 알고리즘 | 대안 |
|---|---|---|
| 정렬 | sort | stable_sort(순서 유지), partial_sort(상위 N개) |
| 값 찾기 | find | find_if(조건), binary_search(정렬됨, 존재 여부만) |
| 위치 찾기 (정렬됨) | lower_bound | upper_bound(초과 첫 위치) |
| 각 원소 변환 | transform | for_each(부수 효과만) |
| 합/곱/집계 | accumulate | - |
| 개수 세기 | count / count_if | - |
| 조건 검사 | all_of / any_of / none_of | - |
| 조건 제거 | remove + erase | remove_if + erase |
| 조건 복사 | copy / copy_if | copy_n |
| 두 시퀀스 병합 | merge | inplace_merge |
| 최대/최소 | max_element / min_element | minmax_element(둘 다) |
구현 체크리스트
알고리즘을 적용할 때 다음을 확인하면 실수를 줄일 수 있습니다.
- 반복자 범위:
[begin, end)가 올바른지 - end() 체크:
find/find_if반환값이end()인지 확인했는지 - 정렬 전제:
binary_search/lower_bound/upper_bound사용 전 정렬 여부 - erase-remove:
remove/remove_if후erase호출했는지 - 출력 크기:
transform출력 범위가 충분한지 - 비교 함수:
sort비교자가 strict weak ordering을 만족하는지
9. 베스트 프랙티스
알고리즘 우선 원칙
“직접 루프를 돌리기 전에 STL에 해당 알고리즘이 있는지 확인한다”는 원칙을 지킵니다. <algorithm>과 <numeric>에 100개 이상의 알고리즘이 있습니다.
// ❌ 수동 루프
int sum = 0;
for (size_t i = 0; i < vec.size(); ++i) sum += vec[i];
// ✅ STL 알고리즘
int sum = std::accumulate(vec.begin(), vec.end(), 0);
반복자 범위 일관성
모든 STL 알고리즘은 반개구간 [begin, end) 를 사용합니다. end()는 “마지막 원소의 다음”을 가리키므로, begin()과 end()를 쌍으로 사용하는 습관을 들이면 실수가 줄어듭니다.
const와 참조 활용
람다에서 원소를 수정하지 않을 때는 const auto&를 사용해 복사를 피합니다. 수정할 때는 비const 참조를 사용합니다.
알고리즘 조합 시 순서 고려
파이프라인 연결 시 필터 → 정렬 → 집계 순서가 효율적입니다. 먼저 조건에 맞는 것만 추리면 정렬·집계할 데이터 양이 줄어듭니다.
reserve로 재할당 최소화
copy_if, transform으로 새 컨테이너를 채울 때 결과 크기를 예측할 수 있으면 reserve로 재할당 횟수를 줄입니다.
10. 프로덕션 패턴
패턴 A: 로그 스트림 병합 (merge)
여러 소스에서 오는 정렬된 로그를 시간순으로 하나로 합칠 때 std::merge를 사용합니다.
struct LogEntry { int64_t timestamp; std::string message; };
std::vector<LogEntry> merge_log_streams(
const std::vector<LogEntry>& a, const std::vector<LogEntry>& b)
{
std::vector<LogEntry> merged;
merged.reserve(a.size() + b.size());
std::merge(a.begin(), a.end(), b.begin(), b.end(),
std::back_inserter(merged),
{
return x.timestamp < y.timestamp;
});
return merged;
}
패턴 B: 활성/비활성 사용자 분할 (partition)
관리자 대시보드에서 “활성 사용자만 먼저 보여주기” 같은 요구가 있을 때 std::partition으로 앞뒤를 나눕니다. 순서를 유지하려면 stable_partition을 사용합니다.
struct User {
std::string id;
int lastLoginDays;
};
void prioritize_active_users(std::vector<User>& users) {
auto pivot = std::partition(users.begin(), users.end(),
{ return u.lastLoginDays <= 30; });
// [begin, pivot): 활성, [pivot, end): 비활성
}
패턴 C: 배치 처리 파이프라인 (filter → accumulate)
대량의 레코드를 필터링·집계할 때 copy_if로 유효 레코드만 추출한 뒤 accumulate로 합계를 구합니다.
struct Record { std::string region; double amount; bool valid; };
double process_batch(const std::vector<Record>& records) {
std::vector<Record> valid;
std::copy_if(records.begin(), records.end(), std::back_inserter(valid),
{ return r.valid; });
return std::accumulate(valid.begin(), valid.end(), 0.0,
{ return sum + r.amount; });
}
패턴 D: Top-K 선택 (partial_sort)
“상위 10개만 필요할 때” 전체 정렬 대신 partial_sort를 쓰면 k가 작을수록 더 빠릅니다.
std::vector<Score> get_top_scores(std::vector<Score>& scores, size_t k) {
k = std::min(k, scores.size());
std::partial_sort(scores.begin(), scores.begin() + k, scores.end(),
{ return a.value > b.value; });
return std::vector<Score>(scores.begin(), scores.begin() + k);
}
패턴 E: 안전한 erase-remove 래퍼
erase와 remove를 매번 함께 쓰기 번거로우므로 헬퍼로 감싸 둡니다.
template<typename Container, typename Predicate>
void remove_if_erase(Container& c, Predicate pred) {
c.erase(std::remove_if(c.begin(), c.end(), pred), c.end());
}
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
- C++ map vs unordered_map | “어떤 걸 써야 하죠?” 선택 기준과 성능 비교
이 글에서 다루는 키워드 (관련 검색어)
C++ STL 알고리즘, sort find_if, 반복자, algorithm 헤더, 범위 기반 알고리즘, 람다 정렬 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 카테고리 | 함수 | 용도 |
|---|---|---|
| 정렬 | sort, stable_sort, partial_sort, merge | 정렬·병합 |
| 검색 | find, find_if, binary_search | 검색 |
| 변환 | transform, for_each | 각 원소 변환 |
| 집계 | accumulate, count, all_of | 합산, 개수 |
| 제거 | remove, remove_if, unique | 조건 제거 |
| 복사 | copy, copy_if | 필터링 복사 |
| 최대/최소 | max_element, min_element | 최대/최소 |
핵심 원칙:
- 직접 루프 작성 대신 STL 알고리즘 사용
- 람다로 조건 표현
erase-removeidiom 활용- 범위 기반 for보다 알고리즘이 명확할 때 선택
참고 자료
- cppreference — Algorithm library: STL 알고리즘 전체 목록과 시그니처
- cppreference — Numeric library:
accumulate,inner_product등 수치 알고리즘 - C++ 람다 표현식: predicate와 비교자 작성에 필요한 람다 문법
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++ STL 알고리즘 완벽 가이드. std::sort·find·find_if·transform·accumulate·for_each 사용법, 람다 표현식과 함께 쓰기, 커스텀 비교자, 실전 패턴(필터링·변환·집계),… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. for 루프와 STL 알고리즘, 어떤 걸 써야 하나요?
A. “한 번만 훑고 부수 효과만 필요할 때”(예: 로그 출력)는 범위 기반 for문 for (auto& x : vec)이 간단합니다. “정렬·검색·변환·집계”처럼 의미가 명확한 연산은 STL 알고리즘이 가독성과 유지보수에 유리합니다. 팀 컨벤션에 맞추면 됩니다.
Q. 정렬된 vector와 set/map, 언제 뭘 쓰나요?
A. vector + sort: 한 번 정렬 후 여러 번 검색할 때, 메모리 연속성으로 캐시에 유리합니다. set/map: 삽입·삭제가 자주 일어나면서 항상 정렬 상태를 유지해야 할 때 적합합니다. 검색만 자주 하고 정렬이 거의 없으면 vector를 한 번 정렬해 두고 binary_search/lower_bound를 쓰는 편이 종종 더 빠릅니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference의 Algorithm library와 해당 라이브러리 공식 문서를 참고하세요. C++ 람다 표현식 글에서 predicate 작성법을 더 배울 수 있습니다.
한 줄 요약: sort·find·transform 등 STL 알고리즘으로 반복문을 줄이고 가독성을 높일 수 있습니다. 다음으로 파일 I/O 기초(#11-1)를 읽어보면 좋습니다.
다음 글: C++ 실전 가이드 #11-1: 파일 I/O 기초. 시리즈 전체 목차는 C++ 시리즈 전체 목차에서 확인할 수 있습니다.
관련 글
- C++ vector 성능 |
- C++ map vs unordered_map (STL 시리즈) |
- C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴
- C++ 람다 심화 | 초기화 캡처·완벽 전달·IIFE·재귀 람다와 실전 패턴
- C++ STL 알고리즘 기초 | sort·find·count·transform·accumulate 가이드