알고리즘
반복자 범위에 정렬·검색·복사·변환 알고리즘을 적용하고 조건자와 사용자 정의 타입을 연결하는 방법을 익힙니다.
컨테이너는 데이터를 저장하고, 반복자는 그 데이터에 접근하는 통로 역할을 합니다.
이번 절에서는 STL의 마지막 주요 구성 요소인 알고리즘(Algorithms)을 다룹니다.
알고리즘은 컨테이너 이름이 아니라 각 함수가 요구하는 반복자 범위와 연산 계약에 대해 일반화된 함수 템플릿입니다.
정렬, 검색, 복사, 변환 같은 표준화된 동작과 복잡도 보장을 재사용할 수 있지만, 호출 전에는 반복자 범주와 요소 연산, 전제 조건을 확인해야 합니다. 앞 절의 begin/end와 무효화 규칙을 바탕으로 읽습니다. 한글 이름을 쓴 아래 형식은 문법 자리 표시이고, 각 main이 있는 예제는 별도 파일로 실행합니다.
C++ · STANDARD LIBRARY
범위·전제·반환 계약을 확인하고 알고리즘을 호출한다
[first, last) 범위와 반복자 능력은 입력 계약이다. 반환값과 변경된 범위는 호출 후 계약이다.
반환 반복자는 end일 수 있음remove는 size를 줄이지 않음
반환 반복자는 end일 수 있음remove는 size를 줄이지 않음
- 요소 소유
컨테이너나 배열이 요소를 보관하고 반복자를 제공합니다.
- 반열린 범위 구성
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: 범위의 끝(마지막 요소 다음)을 가리키는 반복자입니다.
주요 알고리즘 범주와 예시
알고리즘은 타입 추론, 호출 계약, 재사용 경계를 기준으로 확인합니다. 원 for_each의 int 인수는 원소 값의 복사입니다. 두 배로 출력하는 람다도 원 vector의 원소를 두 배로 저장하지 않습니다. 출력과 저장 상태를 구분하고 원 v의 값은 10·20·30·40·50 그대로임을 확인하세요.
C++ · STANDARD LIBRARY
표준 알고리즘을 목적과 호출 계약으로 비교한다
대표 이름을 고른 뒤, 범위의 요구 조건과 반환·후처리 계약을 함께 확인한다.
- 읽기 · 검색
- 대표
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; }원 vector를 오름차순으로 정렬하면 1·2·5·8·9이고, 내림차순으로 다시 정렬하면 9·8·5·2·1입니다. 이진 탐색에도 descending을 넘기는 이유는 탐색이 같은 비교 관계로 앞뒤 영역을 나눠야 하기 때문입니다. 오름차순 기본 비교로 바꿔도 같은 결과가 보장된다고 생각하면 안 됩니다.
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같은 초깃값을 사용합니다. 반환값을 마지막에 double 변수에 담는 것만으로 중간 int 누적이 실수 누적으로 바뀌지는 않습니다.
사용자 정의 타입과 알고리즘
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;
}알고리즘 사용 시 고려사항
알고리즘을 고를 때는 기능 이름뿐 아니라 반복자 요구 조건, 실제 컨테이너 크기 변화 여부, 비교/대입 가능성을 함께 확인해야 합니다.
C++ · STANDARD LIBRARY
알고리즘 호출 전후에 여섯 계약을 점검한다
헤더·범위·비교·검색 전제를 먼저 확인하고, 반환 경계와 실제 비용을 해석한다.
| 검토 항목 | 호출 전 질문 | 반환 · 후속 처리 |
|---|---|---|
| 직접 헤더 | 사용 이름을 선언하는 헤더를 직접 포함했는가? | <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입니다. 위치는lower_bound로 찾습니다.- 경계 · 논리 끝
- 빈 범위의
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 범위, 비교 함수, 반환값, 후처리 방식을 함께 읽을 때 컨테이너와 자연스럽게 연결됩니다.
원 remove의 중간 상태 따라가기
목록 1·2·3·2·4·2·5에서 remove가 반환한 new_end는 남길 범위 1·3·4·5 뒤의 위치입니다. 이때 vector의 size는 여전히 7이고, std::distance(v.begin(), new_end)는 4입니다. 뒤의 세 칸을 특정 숫자로 예측하지 않습니다. 이어 erase하면 세 원소의 수명이 끝나 size가 4로 줄고, 저장 목록은 1·3·4·5가 됩니다. remove의 논리 끝과 남은 상태가 실제 컨테이너 삭제와 다른 이유입니다.
예시 copy의 목적지에는 원소가 있어야 대입할 수 있습니다. 빈 vector에 reserve만 해 둔 뒤 destination.begin()으로 복사하지 않습니다. 빈 목적지에 원소를 추가하고 싶다면 삽입 반복자를 사용할 수 있습니다.
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
int main() {
std::vector<int> source = {2, 4, 6};
std::vector<int> destination;
destination.reserve(source.size());
std::cout << destination.size() << '\n';
std::copy(source.begin(), source.end(), std::back_inserter(destination));
std::cout << destination.size() << '\n';
for (int value : destination) std::cout << value << ' ';
std::cout << '\n';
}출력은 0, 3, 2 4 6입니다. reserve는 저장 공간만 준비하고 back_inserter의 대입이 push_back으로 실제 원소를 만듭니다. 원 copy 예제처럼 크기만큼 원소를 먼저 구성한 뒤 begin으로 대입하는 방식도 유효합니다. 단항 transform의 출력 범위와 이항 transform의 두 번째 입력 범위도 처리할 첫 범위 길이만큼 유효해야 합니다.
스스로 확인하기
① 별도 프로그램에서 {7, 2, 2, 8}에 remove로 2를 제거하려고 합니다. erase 전의 size와 논리 길이, erase 후의 목록을 구합니다. ② double 목록 {0.6, 1.6}을 accumulate할 때 초기값 0과 0.0의 결과를 각각 계산합니다. int 누산기에서는 단계마다 소수 부분이 버려진다는 점을 적용하세요. ③ find로 없는 값 99를 찾아 end가 반환되면 그 값을 역참조할 수 있을까요?
논리 끝·누산 타입·실패 경계 풀이
① erase 전 size는 4, 논리 길이는 2이며 erase 후에는 7·8만 남습니다. ② int 초깃값 0에서는 0+0.6을 int 0으로 저장하고, 이어 0+1.6을 int 1로 저장해 최종 1입니다. double 초깃값 0.0에서는 약 2.2입니다. 이 작은 값 범위의 변환 결과를 설명한 것으로 범위를 넘는 실수→정수 변환을 안전하다고 일반화하지 않습니다. ③ end는 찾지 못했다는 경계이며 역참조할 원소가 없습니다.