알고리즘
반복자 범위에 정렬·검색·복사·변환 알고리즘을 적용하고 조건자와 사용자 정의 타입을 연결하는 방법을 익힙니다.
컨테이너는 데이터를 저장하고, 반복자는 그 데이터에 접근하는 통로 역할을 합니다.
이번 절에서는 STL의 마지막 주요 구성 요소인 알고리즘(Algorithms)을 다룹니다.
알고리즘은 컨테이너 이름이 아니라 각 함수가 요구하는 반복자 범위와 연산 계약에 대해 일반화된 함수 템플릿입니다.
정렬, 검색, 복사, 변환 같은 표준화된 동작과 복잡도 보장을 재사용할 수 있지만, 호출 전에는 반복자 범주와 요소 연산, 전제 조건을 확인해야 합니다.
- 요소 소유
컨테이너나 배열이 요소를 보관하고 반복자를 제공합니다.
- 반열린 범위 구성
first는 포함하고last는 제외합니다.last는 경계이므로 역참조하지 않습니다. - 호출 계약 확인
반복자 범주, 요소 연산, 비교 함수나 조건자가 알고리즘의 요구를 만족하는지 확인합니다.
- 결과 처리
반환된 반복자·불리언·새 논리 끝·값의 의미와 범위 변경 여부에 맞춰 후속 작업을 합니다.
알고리즘(Algorithms)이란 무엇인가?
알고리즘은 컨테이너의 요소들을 대상으로 특정 작업을 수행하는 함수 템플릿입니다.
많은 표준 알고리즘은 <algorithm>에, std::accumulate 같은 수치 연산은 <numeric>에 선언됩니다. <functional>은 알고리즘 정의 헤더가 아니라 std::greater 같은 함수 객체를 제공하며, 이 이름들은 대부분 std:: 네임스페이스 안에 있습니다.
- 제네릭 프로그래밍: 컨테이너 이름이 아니라 반복자 쌍을 받습니다. 각 알고리즘의 요구 조건을 만족한다면 표준 컨테이너뿐 아니라
std::begin/std::end로 얻은 C-스타일 배열 범위에도 적용할 수 있습니다. - 명시적인 계약: 표준이 동작과 복잡도 요구를 정의합니다. 실제 적용 가능성과 비용은 반복자 범주, 요소 타입, 호출 방식에 따라 달라집니다.
- 재사용성: 한 번 구현된 알고리즘은 다양한 컨테이너와 데이터 타입에 대해 재사용될 수 있습니다.
- 선형 탐색, 정렬, 복사, 변환, 조건부 실행 등 다양한 기능 제공.
알고리즘의 일반적인 사용법
대부분의 전통적인 STL 알고리즘은 하나 이상의 반복자 쌍을 매개변수로 받아 범위(Range)를 지정합니다.
전체 컨테이너를 처리할 때는 보통 begin()과 end()로 시작 포함·끝 제외의 반열린 범위 [first, last)를 만듭니다.
end()는 마지막 요소 다음 위치를 나타내는 경계이므로 역참조하면 안 됩니다.
std::알고리즘이름(first_iterator, last_iterator, ... 추가_매개변수 ...);first_iterator: 범위의 시작을 가리키는 반복자입니다.last_iterator: 범위의 끝(마지막 요소 다음)을 가리키는 반복자입니다.
주요 알고리즘 범주와 예시
알고리즘은 타입 추론, 호출 계약, 재사용 경계를 기준으로 확인합니다.
| 목적 | 대표 알고리즘 | 핵심 계약 |
|---|---|---|
| 읽기 · 검색 | find, count_if |
값 비교나 조건자를 만족해야 한다. find는 없으면 last를 반환한다. |
| 순서 · 이진 탐색 | sort, binary_search |
sort는 임의 접근 반복자가 필요하다. 이진 탐색 범위는 comp(e, value)와 !comp(value, e)에 대해 각각 분할되어야 하고, 고전 오버로드의 양방향 비교는 모순되면 안 된다. 결과는 bool이다. |
| 복사 · 변환 | copy, transform |
출력 반복자가 기록 가능한 유효 범위를 가리키도록 목적지 크기나 삽입 반복자를 준비한다. |
| 논리적 제거 | remove, remove_if |
새 논리 끝을 반환할 뿐 컨테이너 크기는 줄지 않는다. 필요하면 이어서 erase한다. |
| 누적 | accumulate |
<numeric>에 정의되며 초기값의 타입이 누산기와 반환 타입을 결정한다. |
- 읽기 · 검색
- 대표
find,count_if - 값 비교나 조건자를 만족해야 하며,
find는 없으면last를 반환합니다. - 순서 · 이진 탐색
- 대표
sort,binary_search sort에는 임의 접근 반복자가 필요합니다. 이진 탐색 범위는comp(e, value)와!comp(value, e)에 대해 각각 분할되어야 하고, 고전 오버로드의 양방향 비교는 모순되면 안 됩니다. 결과는bool입니다.- 복사 · 변환
- 대표
copy,transform - 출력 반복자가 기록 가능한 유효 범위를 가리키도록 목적지 크기나 삽입 반복자를 준비합니다.
- 논리적 제거
- 대표
remove,remove_if - 새 논리 끝을 반환할 뿐 크기는 줄지 않으므로 필요하면 이어서
erase합니다. - 누적
- 대표
accumulate <numeric>에 정의되며 초기값의 타입이 누산기와 반환 타입을 결정합니다.
STL 알고리즘은 기능에 따라 여러 범주로 나눌 수 있습니다.
여기서는 자주 사용되는 몇 가지 알고리즘을 소개합니다.
비수정 시퀀스 연산 (Non-modifying Sequence Operations)
읽기와 탐색이 중심인 연산입니다. 다만 std::for_each에 전달한 함수가 변경 가능한 참조를 받는다면 요소를 바꿀 수 있으므로, 호출 가능 객체의 부작용도 함께 확인해야 합니다.
std::for_each: 범위 내의 모든 요소에 대해 지정된 함수를 적용합니다.std::find: 특정 값을 검색하여 해당 값을 가리키는 반복자를 반환합니다. 찾지 못하면end()를 반환합니다.std::count: 범위 내에서 특정 값과 일치하는 요소의 개수를 셉니다.std::count_if: 특정 조건(프레디케이트)을 만족하는 요소의 개수를 셉니다.#include <vector> #include <algorithm> // for_each #include <iostream> #include <iterator> // distance void print(int n) { std::cout << n << " "; } int main() { std::vector<int> v = {10, 20, 30, 40, 50}; std::cout << "std::for_each: "; // std::for_each 사용 예시 std::for_each(v.begin(), v.end(), print); // 각 요소에 대해 print 함수 호출 std::cout << std::endl; // 출력: 10 20 30 40 50 // 람다 함수 사용 (C++11 이상) std::cout << "std::for_each with lambda: "; std::for_each(v.begin(), v.end(), [](int n){ std::cout << n * 2 << " "; // 각 요소를 두 배로 만들어서 출력 }); std::cout << std::endl; // 출력: 20 40 60 80 100 // std::find 사용 예시 auto it_find = std::find(v.begin(), v.end(), 30); if (it_find != v.end()) { std::cout << "Found 30 at position: " << std::distance(v.begin(), it_find) << std::endl; } else { std::cout << "30 not found.\n"; } // 출력: Found 30 at position: 2 // std::count 사용 예시 std::vector<int> v2 = {1, 2, 2, 3, 2, 4}; int count_2 = std::count(v2.begin(), v2.end(), 2); std::cout << "Count of 2s: " << count_2 << std::endl; // 출력: 3 // std::count_if 사용 예시 int count_even = std::count_if(v2.begin(), v2.end(), [](int n){ return n % 2 == 0; }); std::cout << "Count of even numbers: " << count_even << std::endl; // 출력: 4 }
수정 시퀀스 연산 (Modifying Sequence Operations) 컨테이너의 내용을 변경하는 작업을 수행합니다.
-
std::sort: 컨테이너의 요소들을 정렬합니다. 임의 접근 반복자를 지원하는 컨테이너에만 적용됩니다. (예:vector,deque, 배열)std::sort 사용 예시 #include <vector> #include <algorithm> // sort #include <functional> // greater #include <iostream> #include <list> // list 예시 int main() { std::vector<int> v = {5, 2, 8, 1, 9}; std::sort(v.begin(), v.end()); // 오름차순 정렬 std::cout << "std::sort (vector): "; for (int n : v) std::cout << n << " "; // 출력: 1 2 5 8 9 std::cout << std::endl; // 같은 비교식을 정렬과 이진 탐색에 사용 const auto descending = std::greater<int>{}; std::sort(v.begin(), v.end(), descending); std::cout << "std::sort (desc): "; for (int n : v) std::cout << n << " "; // 출력: 9 8 5 2 1 std::cout << std::endl; bool has_8 = std::binary_search(v.begin(), v.end(), 8, descending); std::cout << "contains 8: " << has_8 << std::endl; // 출력: 1 // std::list는 sort() 멤버 함수를 제공 (양방향 반복자는 std::sort 사용 불가) std::list<int> l = {5, 2, 8, 1, 9}; l.sort(); // list의 멤버 함수 sort 호출 std::cout << "std::list::sort(): "; for (int n : l) std::cout << n << " "; // 출력: 1 2 5 8 9 std::cout << std::endl; }std::sort에 전달하는 비교 함수는 엄격 약순서(strict weak ordering)를 만들어야 합니다.std::binary_search는 각 요소e에 대해comp(e, value)와!comp(value, e)두 표현으로 범위가 각각 분할되어 있어야 합니다. 고전std::binary_search비교 함수 오버로드에서는 두 방향의 비교가 모두 가능해야 하고,comp(e, value)가 참인데comp(value, e)도 참인 모순을 만들면 안 됩니다. 보통 같은 비교식으로 먼저 정렬해 이 전제를 보장합니다. 반환값은 존재 여부를 나타내는bool뿐이므로 위치가 필요하면 같은 순서 관계로std::lower_bound를 사용합니다. -
std::copy: 한 범위의 요소를 다른 위치로 복사합니다.std::copy 사용 예시 #include <vector> #include <algorithm> // copy #include <iostream> int main() { std::vector<int> source = {1, 2, 3, 4, 5}; std::vector<int> destination(source.size()); // 복사할 크기만큼 미리 할당 std::copy(source.begin(), source.end(), destination.begin()); std::cout << "std::copy: "; for (int n : destination) std::cout << n << " "; // 출력: 1 2 3 4 5 std::cout << std::endl; } -
std::transform: 한 범위의 요소를 변환하여 다른 위치(또는 동일한 위치)에 저장합니다.std::transform 사용 예시 #include <vector> #include <algorithm> // transform #include <iostream> int main() { std::vector<int> original = {1, 2, 3, 4, 5}; std::vector<int> squared(original.size()); std::transform(original.begin(), original.end(), squared.begin(), [](int n){ return n * n; }); std::cout << "std::transform (squared): "; for (int n : squared) std::cout << n << " "; // 출력: 1 4 9 16 25 std::cout << std::endl; } -
std::remove: 일치하지 않는 요소를 범위 앞쪽으로 이동하고 새 논리 끝을 반환합니다. 컨테이너 크기는 바뀌지 않으며, 새 논리 끝부터 기존end()까지의 값은 유효하지만 지정되지 않은 상태입니다. 실제 크기를 줄이려면erase와 함께 사용합니다.std::remove 사용 예시 #include <vector> #include <algorithm> // remove #include <iostream> int main() { std::vector<int> v = {1, 2, 3, 2, 4, 2, 5}; auto new_end = std::remove(v.begin(), v.end(), 2); // [v.begin(), new_end)는 {1, 3, 4, 5} // [new_end, v.end())의 값은 유효하지만 지정되지 않음 v.erase(new_end, v.end()); // 컨테이너의 실제 크기를 줄임 std::cout << "std::remove + erase: "; for (int n : v) std::cout << n << " "; // 출력: 1 3 4 5 std::cout << std::endl; }
-
std::min_element,std::max_element: 범위 내에서 최소/최대 요소를 가리키는 반복자를 반환합니다.std::min_element, std::max_element 사용 예시 #include <vector> #include <algorithm> // min_element, max_element #include <iostream> int main() { std::vector<int> v = {10, 20, 5, 30, 15}; auto min_it = std::min_element(v.begin(), v.end()); auto max_it = std::max_element(v.begin(), v.end()); if (min_it != v.end() && max_it != v.end()) { std::cout << "Min element: " << *min_it << std::endl; // 출력: 5 std::cout << "Max element: " << *max_it << std::endl; // 출력: 30 } }빈 범위에서
std::min_element와std::max_element는last를 반환하므로, 반환 반복자를 역참조하기 전에 경계와 비교해야 합니다.
<numeric> 헤더-
std::accumulate: 범위 내의 요소들을 합산합니다.std::accumulate 사용 예시 #include <vector> #include <numeric> // accumulate #include <iostream> #include <string> int main() { std::vector<int> v = {1, 2, 3, 4, 5}; int sum = std::accumulate(v.begin(), v.end(), 0); // 초기값 0 std::cout << "Sum: " << sum << std::endl; // 출력: 15 // 문자열 연결도 가능 std::vector<std::string> words = {"Hello", " ", "World", "!"}; std::string concat = std::accumulate(words.begin(), words.end(), std::string("")); std::cout << "Concatenated string: " << concat << std::endl; // 출력: Hello World! }std::accumulate의 누산기와 반환 타입은 초기값의 타입으로 정해집니다. 예를 들어double요소를 실수로 누적하려면 정수0대신0.0같은 초기값을 사용합니다.
사용자 정의 타입과 알고리즘
STL 알고리즘은 템플릿으로 구현되어 있기 때문에, 사용자 정의 타입에도 적용할 수 있습니다.
단, 해당 타입과 호출 방식이 알고리즘이 요구하는 연산을 지원해야 합니다. 비교 함수를 생략한 정렬은 기본 순서 연산을 사용하지만, 비교 함수 오버로드를 사용하면 타입 자체에 <를 추가하지 않아도 됩니다. std::find는 값의 동등 비교가 가능해야 합니다.
정렬 비교 함수는 어떤 두 원소를 비교해도 일관된 엄격 약순서를 만들어야 하며, 정렬된 범위에 std::binary_search 같은 순서 기반 알고리즘을 적용할 때도 요소와 검색값을 양방향으로 비교할 수 있는 같은 순서 관계를 사용해야 합니다.
#include <iostream>
#include <vector>
#include <algorithm> // sort
#include <string>
class Person {
public:
std::string name;
int age;
Person(std::string n, int a) : name(n), age(a) {}
// << 연산자 오버로딩 (출력용)
friend std::ostream& operator<<(std::ostream& os, const Person& p) {
os << "[" << p.name << ", " << p.age << "]";
return os;
}
};
// 1. 나이 기준 오름차순 비교 함수 (전역 함수 또는 람다)
bool comparePersonsByAge(const Person& p1, const Person& p2) {
return p1.age < p2.age;
}
// 2. 이름 기준 오름차순 비교를 위한 함수 객체 (Functor)
struct ComparePersonsByName {
bool operator()(const Person& p1, const Person& p2) const {
return p1.name < p2.name;
}
};
int main() {
std::vector<Person> people = {
{"Alice", 30},
{"Charlie", 25},
{"Bob", 35}
};
std::cout << "Original order:\n";
for (const auto& p : people) {
std::cout << p << " ";
}
std::cout << std::endl;
// 나이 기준 정렬 (비교 함수 사용)
std::sort(people.begin(), people.end(), comparePersonsByAge);
std::cout << "Sorted by age:\n";
for (const auto& p : people) {
std::cout << p << " ";
}
std::cout << std::endl; // 출력: [Charlie, 25] [Alice, 30] [Bob, 35]
// 이름 기준 정렬 (함수 객체 사용)
std::sort(people.begin(), people.end(), ComparePersonsByName());
std::cout << "Sorted by name:\n";
for (const auto& p : people) {
std::cout << p << " ";
}
std::cout << std::endl; // 출력: [Alice, 30] [Bob, 35] [Charlie, 25]
return 0;
}알고리즘 사용 시 고려사항
알고리즘을 고를 때는 기능 이름뿐 아니라 반복자 요구 조건, 실제 컨테이너 크기 변화 여부, 비교/대입 가능성을 함께 확인해야 합니다.
| 검토 항목 | 호출 전 질문 | 반환 · 후속 처리 |
|---|---|---|
| 직접 헤더 | 사용 이름을 선언하는 헤더를 직접 포함했는가? | <algorithm>, <numeric>, <functional> 등을 기능에 맞게 선택한다. |
| 범위 · 반복자 | [first, last)가 유효하고 필요한 반복자 범주를 만족하는가? |
sort는 임의 접근, min_element는 전방 반복자를 요구한다. |
| 비교 함수 | 정렬 비교자가 일관된 엄격 약순서를 만드는가? | 정렬 뒤 순서 기반 검색에도 같은 순서 관계를 사용한다. |
| 검색 전제 | comp(e, value)와 !comp(value, e)에 대해 각각 분할되어 있는가? |
고전 오버로드의 양방향 비교는 모순되면 안 된다. 반환은 bool이며 위치는 lower_bound로 찾는다. |
| 경계 · 논리 끝 | 반환 반복자를 역참조하기 전에 last와 비교했는가? |
빈 범위의 min_element는 last, remove는 새 논리 끝을 반환한다. |
| 비용 · 효과 | 표준 복잡도와 요소의 복사·이동 비용이 요구에 맞는가? | 실제 컨테이너 크기 변화와 필요한 erase, 목적지 용량, 측정 결과를 확인한다. |
- 직접 헤더
- 사용 이름을 선언하는
<algorithm>,<numeric>,<functional>등을 직접 포함합니다. - 범위 · 반복자
[first, last)가 유효한지,sort의 임의 접근이나min_element의 전방 반복자 요구를 만족하는지 확인합니다.- 비교 함수
- 정렬 비교자가 일관된 엄격 약순서를 만들고, 이어지는 순서 기반 검색도 같은 관계를 사용하는지 확인합니다.
- 검색 전제
binary_search범위는comp(e, value)와!comp(value, e)에 대해 각각 분할되어야 하고, 고전 오버로드의 양방향 비교는 모순되면 안 됩니다. 결과는 위치가 아닌bool입니다.- 경계 · 논리 끝
- 빈 범위의
min_element는last,remove는 새 논리 끝을 반환하므로 의미에 맞게 검사하고 후처리합니다. - 비용 · 효과
- 표준 복잡도와 복사·이동 비용, 목적지 용량, 실제 크기 변화와 필요한
erase를 확인하고 필요하면 측정합니다.
- 헤더 파일 포함: 사용하는 이름을 선언하는 헤더를 직접 포함합니다. 많은 알고리즘은
<algorithm>, 누적은<numeric>,std::greater같은 함수 객체는<functional>에 있습니다. - 범위와 반복자 확인:
[first, last)가 유효한 범위인지, 알고리즘이 요구하는 반복자 범주를 컨테이너가 제공하는지 확인합니다. 예를 들어std::sort에는 임의 접근 반복자가 필요합니다. - 비교와 전제 조건 확인: 정렬 비교자는 엄격 약순서를 만들어야 합니다.
std::binary_search범위는comp(e, value)와!comp(value, e)에 대해 각각 분할되어 있어야 하며, 고전 비교 함수 오버로드는 두 방향의 비교 결과가 모순되지 않아야 합니다. - 반환값과 후처리 확인:
std::min_element의last,std::binary_search의bool,std::remove의 새 논리 끝처럼 반환 계약이 서로 다릅니다. 역참조나erase전에 그 의미를 확인합니다. - 복사·이동과 비용 확인:
std::copy와std::move는 요소 타입의 복사·이동 의미론을 따릅니다. 표준의 복잡도 보장을 먼저 읽고, 실제 데이터와 구현에서 필요한 경우 측정합니다.
STL 알고리즘은 이름만 외우기보다 begin/end 범위, 비교 함수, 반환값, 후처리 방식을 함께 읽을 때 컨테이너와 자연스럽게 연결됩니다.