[전문가를 위한 C++] 표준 라이브러리 알고리즘 완전 정복
‘전문가를 위한 C++ - Marc Gregoire 지음, 남기혁 옮김’ 책을 참고하여 작성한 포스트입니다.
표준 라이브러리는 거의 모든 컨테이너에 적용 가능한 제네릭 알고리즘을 다양하게 제공한다.
모든 작업을 반복자 인터페이스만으로 처리한다.
각 원소의 타입뿐만 아니라 컨테이너 타입에도 독립적이다.
알고리즘 개요
- 표준 라이브러리 알고리즘의 강력함은 컨테이너를 직접 다루지 않고 반복자라는 매개체로 작동하는 데 있다. 특정 컨테이너에 종속되지 않는다.
- 모두 함수 템플릿으로 구현되었고, 템플릿 타입 매개변수도 대부분 반복자이다.
- 반복자 자체는 함수의 인수로 지정하고, 함수 템플릿은 전달된 인수를 보고 템플릿 타입을 유추하기 때문에 알고리즘이 템플릿이 아닌 일반 함수인 것처럼 호출 가능하다.
- 반복자가 알고리즘의 요건에 맞아야 한다.
- 대부분
<algorithm>
에 정의되어 있음. 일부 수치 관련은<numeric>
에 정의되어 있음. 모두 std 네임스페이스에 속해 있음 - c++20 부터는 대부분의 알고리즘이 constexpr 이다.
- 제네릭 알고리즘과 동일한 기능을 제공하는 메서드가 컨테이너에 있다면 컨테이너 메서드를 사용하는 것이 좋다.
- 예를 들어 map의 경우 제네릭 알고리즘인 find()는 선형 시간인 반면, map에서 제공하는 find()는 로그 시간에 수행한다.
find(), find_if()
find()
- 주어진 반복자 범위 내에서 특정한 원소를 찾는다. 모든 컨테이너에 사용 가능
- 원소를 찾으면 그 원소를 참조하는 반복자르 리턴하고, 그렇지 않으면 주어진 반복자 범위의 끝 반복자를 리턴함.
find_if()
- 인수로 predicate function callback을 받는다. predicate은 true나 false를 리턴하며, 지정한 범위 안에 있는 원소에 대해 predicate가 true를 리턴할 때까지 계속 호출한다.
- true를 리턴하면 그 원소를 가리키는 반복자를 리턴함
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
bool PerfectScore(int Num) { return (Num >= 5); }
int main()
{
vector<int> MyVector {1, 2, 3, 4, 5};
int NumberToFind = 3;
if(auto iter = find(cbegin(MyVector), cend(MyVector), NumberToFind); iter == cend(MyVector))
{
cout << "Could not find " << NumberToFind << endl;
}
else
{
cout << *iter <<" " << NumberToFind << endl;
}
auto iter2 = find_if(cbegin(MyVector), cend(MyVector), PerfectScore);
auto iter3 = find_if(cbegin(MyVector), cend(MyVector), [](int i) -> bool {return i >= 5;});
cout << "iter 2 = " << *iter2 << endl;
cout << "iter 3 = " << *iter3 << endl;
}
/* 출력결과
3 3
iter 2 = 5
iter 3 = 5
*/
accumulate()
<numeric>
에 정의되어 있음- 디폴트는 컨테이너에 있는 원소들을 모두 더한 값을 리턴한다.
- 세번째 인수로 초깃값을 받는다.
- 네번째 인수로 이진(인수가 두개) 콜백함수(binary callback)를 넣으면 다른 사칙 연산 수행이 가능하다.
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;
int ArithmeticSum(const vector<int> values)
{
int sum = accumulate(cbegin(values), cend(values), 0);
return sum;
}
int Product(int value1, int value2) { return value1 * value2 ;}
int ArithmeticProduct(const vector<int> values)
{
int mult = accumulate(cbegin(values), cend(values), 1, Product);
// 투명 multiplies 함수 객체 이용
int mult2 = accumulate(
cbegin(values), cend(values), 1,
multiplies<>{}
);
return mult2;
}
int main()
{
vector<int> MyVector {1, 2, 3, 4, 5};
cout << ArithmeticSum(MyVector) << endl;
cout << ArithmeticProduct(MyVector) << endl;
}
/* 출력결과
15
120
*/
알고리즘과 의미 이동론
- 표준 라이브러리 컨테이너와 마찬가지로 알고리즘도 필요에 따라 이동 의미론 적용해 최적화 가능하다.
알고리즘 콜백(나중에 내용 추가)
- 펑터나 람다 표현식 등으로 주어진 콜백에 대해 복제본을 여럿 만들어 다양한 원소에 대해 별도 호출 가능하다.
- 기본적으로 콜백은 상태가 없어야 한다.
- 콜백이 알고리즘에 의해 복제되지 않게 하려면 std::ref() 헬퍼 함수를 이용해 콜백 레퍼런스를 전달하면 된다.
표준 라이브러리 알고리즘 심층 분석
불변형 순차 알고리즘(non-modifying sequence algorithm)
- 주어진 범위에서 원소 검색하거나 두 범위를 서로 비교하는 함수
탐색 알고리즘(searching algorithm)
- 표준 라이브러리 알고리즘은 모두 operator== 나 operator<를 이폴트 연산자로 사용한다.
- 콜백을 직접 지정할 수 있는 오버로딩 버전을 제공한다.
특수 탐색 알고리즘
- c++17 부터 search() 알고리즘에 원하는 탐색 알고리즘을 옵션으로 지정할 수 있도록 매개변수가 추가 되었다.
- 크게 default_searcher, boyer_moore_searcher, boyer_moore_horspool_searcher 가 있다.
- 모두
<functional>
에 정의되어 있다. - 두, 세번째는 방대한 텍스트에서 부분 문자열을 검색하는 데 유용하다.
비교 알고리즘
- equal(), mismatch(), lexicographical_compare(), lexicographical_compare_three_way()(c++20) 이 있다
- 컨테이너가 달라도 적용 가능한 장점이 있다. vector와 list 비교 가능
- 순차 컨테이너에 적용할 때 성능이 가장 뛰어나며, 각 컨테이너에서 동일한 위치에 있는 값끼리 비교하는 방식으로 실행된다.
- equal()
- 주어진 원소가 모두 같으면 true
- 첫 번째 범위의 시작, 끝 반복자, 두 번째 범위의 시작, 끝 반복자 총 네개의 인수
- mismatch()
- 주어진 범위에서 일치하지 않는 범위를 가리키는 반복자를 리턴
- 네개의 인수 버전 사용하는 것이 좋음
- lexicographical_compare()
- 제공된 두 범위에서 동일한 위치에 있는 원소를 순차적으로 서로 비교한다.
- 첫 번째로 일치하지 않는 양쪽 범위의 원소 중 첫 번째 범위의 원소가 두 번쨰 범위의 원소보다 작거나,
- 첫 번째 범위의 원소 개수가 두 번째 범위의 원소 개수보다 적으면서 첫 번째 범위의 원소가 모두 두 번째 범위의 앞부분과 일치하면 true 반환
- 사전식이라 생각하면 됨
- lexicographical_compare_three_way()
- 3방향 비교 연산 사용.
- 부울 타입대신 비교 카테고리 타입 리턴
- equal()
집계 알고리즘
- 불변형 집계 알고리즘에는 all_of(), any_of(), none_of(), count(), count_if() 가 있다.
예시 코드
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include <string>
using namespace std;
int main()
{
vector<int> MyVector { 1, 2, 5, 4, 4, 3};
auto BeginIter = cbegin(MyVector);
auto EndIter = cend(MyVector);
// 람다 표현식을 만족하지 않는 첫 번째 원소를 찾음
auto Iter1 = find_if_not(BeginIter, EndIter,
[](int i){return i<5;});
if (Iter1 != EndIter) cout << "find_if_not - number < 5 : " << *Iter1 << endl;
// 같은 값이 연속된 첫 번째 원소 쌍을 찾음
auto Iter2 = adjacent_find(BeginIter, EndIter);
if (Iter2 != EndIter) cout << "not consecutive equal elements : " << *Iter2 << endl;
// 두 값 중 첫 번째 값을 찾음
vector<int> Targets {4, 5};
auto Iter3 = find_first_of(BeginIter, EndIter, cbegin(Targets), cend(Targets));
if (Iter3 != EndIter) cout << "Found number of Targets at MyVector : " << *Iter3 << endl;
// 첫 번째 부분열을 찾음
vector Sub {2, 5};
auto Iter4 = search(BeginIter, EndIter, cbegin(Sub), cend(Sub));
if (Iter4 != EndIter) cout << "Found Subsequence [2, 5]"<< endl;
// 마지막 부분열을 찾음
auto Iter5 = search(BeginIter, EndIter, cbegin(Sub), cend(Sub));
// 4가 두 번 연속된 첫 번째 부분열을 찾음
auto Iter6 = search_n(BeginIter, EndIter, 2, 4);
if (Iter6 != EndIter) cout << "Found two consecutive 4s"<< endl;
string Text = "The sky is blue.";
string ToSearchFor = "sky";
boyer_moore_searcher Searcher(cbegin(ToSearchFor), cend(ToSearchFor));
auto Result = search(cbegin(Text), cend(Text), Searcher);
if (Result != cend(Text)) cout << "Found the 'sky'" << endl;
vector<int> MyVector2 { 1, 1, 1, 3};
auto BeginIter2 = cbegin(MyVector2);
auto EndIter2 = cend(MyVector2);
if (all_of(BeginIter2, EndIter2, [](int i){return i == 1;})) cout << " ! " << endl;
else cout<<"the vector is not all of 1"<<endl;
if (any_of(BeginIter2, EndIter2, [](int i){return i == 3;}))
{
cout << "At least one element == 3"<< endl;
}
if (none_of(BeginIter2, EndIter2, [](int i){return i == 4;}))
{
cout<< "All elements are not 4" << endl;
}
int Number = 2;
int CallCounter = 0;
auto Tally = count_if(BeginIter2, EndIter2,
[Number, &CallCounter](int i) -> int {++CallCounter; return i < Number;});
cout << "Found " << Tally << " numbers that less than " << Number << endl;
cout << "The lambda expression was called " << CallCounter << " times." <<endl;
}
/* 출력 결과
find_if_not - number < 5 : 5
not consecutive equal elements : 4
Found number of Targets at MyVector : 5
Found Subsequence [2, 5]
Found two consecutive 4s
Found the 'sky'
the vector is not all of 1
At least one element == 3
All elements are not 4
Found 3 numbers that less than 2
The lambda expression was called 4 times.
*/
가변형 순차 알고리즘(modifying sequence algorithm)
- 대상 범위에 원소를 추가할 수는 없다. 수정하거나 덮어쓰기만 가능하다. 추가하려면 반복자 어댑터를 이용해야 한다.(17장)
- map, set은 키를 const로 지정하는데, 가변형 알고리즘은 키도 덮어쓰기 때문에 일반적으로 적용이 불가능하다. 적용하려면 추가 반복자(insert iteratro)로 처리해야 한다.(17장)
- 대부분 대상 범위의 마지막 바로 다음 번쨰 항목을 가리키는 반복자를 리턴한다.
generate
- 반복자 범위를 인수로 받아 그 범위에 있는 값을 세 번째 인수로 지정한 함수의 리턴값으로 교체한다.
transform
- 여러 오버로드가 있다.
- 주어진 범위에 있는 모든 원소마다 콜백을 적용해 새 원소를 생성 후, 인수로 지정한 대상 범위에 저장됨
- 주어진 원소 쌍에 대해 이항 함수를 호출해 네 번째 인수 범위에 덮어씀
copy
- 주어진 범위에 있는 원소를 다른 범위로 복제함
- 원본과 대상 범위는 반드시 달라야 한다. 일정 제약을 만족하면 중첩할 순 있다.
- ex) copy(b, e, d) 에서 d가 b 보다 앞에 나오는 경우
- 대상 컨테이너의 공간 확인 작업을 resize() 메서드 등으로 해주면 좋다.
- copy_backward() 는 마지막 원소부터 시작 원소 순으로 복제한다. 세번째 인수는 end() 로?
- copy_if() 는 predicate도 인수로 받는다.
- copy_n() 은 원본 원소 n개를 대상으로 복제함. 경곗값 검사를 하지 않으므로 검사하는 코드를 추가해 주어야 함.
move
- 이동 의미론을 사용한다.
- 원소 타입을 직접 정의하려면 원소 타입에 대한 클래스에 반드시 이동 대입 연산자를 구현해야 한다.(9장)
- 이동 연산 수행 후 원래 담긴 모든 원소를 확실한 상태로 만들지 않았다면 사용하지 말아야 한다.
- move_backward()는 마지막 원소부터 첫 번째 원소 순으로 원소를 이동시킨다.
replace
- 주어진 범위에서 값을 새 값으로 교체한다. repalce_if 의 경우 predicate 이용
- replace_copy, repalce_copy_if 는 교체한 값을 다른 대상 범위로 복제함
erase(c++20)
- std::erase, std::erase_if 는 인자로 지정한 값이나, predicate을 만족하는 원소를 모두 제거한다.
- 반복자 범위 대신 컨테이너에 대한 레퍼런스를 사용한다.
- erase() 는 정렬 연관 비정렬 연관 컨테이너에서 사용할 수 없다. 더 성능 뛰어난 erase(key) 메서드를 갖고 있다.
- erase_if() 는 다 가능.
remove
- c++20 이 아닌 경우.. 이걸 쓰자
- erase 컨테이너 메서드를 지원하는 경우 반복문을 돌며 원하는 원소들을 삭제할 수도 있지만,
- 삭제시마다 원소의 이동이 일어나 복잡도가 제곱이다. 그리고 반복자 유지도 까다롭다.
- 이럴 때는 remove를 이용한 제거 후 삭제 패턴(remove-erase idiom)으로 구현하는 것이 좋다.
- remove()는 원소를 실제로 지우는 것이 아닌 predicate을 만족하는 원소와 이동 대입 연산으로 교체한다.
- 원소의 순서는 유지되며 유지될 원소들은 모두 주어진 범위의 앞으로 이동한다.
- 그러면 범위는 유지할 원소집합과 삭제될 원소 집합으로 나뉜다.
- 삭제될 원소 집합의 시작 반복자를 리턴하는데, 이를 이용해 erase를 호출하여 삭제하면 된다.
- 이때 당연한거지만 erase 의 두번째 인수를 지정하지 않으면 원소 하나만 삭제 된다.
- remove_copy, remove_copy_if 도 있다.
unique
- 같은 원소가 연달아 나오는 부분을 모두 삭제한다.
- 비정렬 컨테이너에도 적용 가능하대
- unique_copy 도 있다.
- 얘도 유지할 원소집합과 삭제할 원소집합 나누는 모양이다.
shuffle
- 주어진 범위의 원소를 무작위 순으로 재정렬하며 시간 복잡도는 선형이다.
- 세번째 인수로 균등 분포 무작위수 생성기를 인수로 받는다.(23장)
sample
- 원본 범위의 원소를 무작위로 n개 골라 리턴하고, 대상 범위에 저장한다.
reverse
- reverse_copy 도 있다.
- 주어진 범위에 있는 원소의 순서를 반대로 바꿈
shift(c++20)
- 주어진 범위의 원소를 새로운 위치로 이동시키는 방식으로 시프트하는 shift_left(), shift_right() 가 있다.
- 주어진 범위의 끝을 넘어서는 원소는 제거된다.
- shift_left는 새로운 범위의 끝을 가리키는 반복자를 리턴하고, shift_right는 새로운 범위의 시작점을 가리키는 반복자를 리턴한다.
예시 코드
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> Values(10);
int Value = 1;
generate(begin(Values), end(Values), [&Value](){return Value *= 2;});
for (const auto& i : Values) cout << i << " ";
cout << endl;
//2 4 8 16 32 64 128 256 512 1024
vector<int> Values2(7); // 0 이면 에러.
transform(begin(Values), end(Values), begin(Values2),
[](int i){return i + 100;});
for (const auto& i : Values2) cout << i << " ";
cout << endl;
// 102 104 108 116 132 164 228
transform(begin(Values), end(Values), begin(Values2),
begin(Values), [](int a, int b) {return a + b;});
for (const auto& i : Values) cout << i << " ";
cout << endl;
// 104 108 116 132 164 228 356 612 1124 2148
for (const auto& i : Values2) cout << i << " ";
cout << endl;
// 102 104 108 116 132 164 228
Values2.resize(size(Values));
copy(cbegin(Values), cend(Values), begin(Values2));
for (const auto& i : Values2) cout << i << " ";
cout << endl;
// 104 108 116 132 164 228 356 612 1124 2148
vector<int> Values3(5);
copy_backward(cbegin(Values3), cend(Values3), end(Values2));
for (const auto& i : Values2) cout << i << " ";
cout << endl;
// 104 108 116 132 164 0 0 0 0 0
Values2.resize(10);
auto Iter = copy_if(cbegin(Values), cend(Values), begin(Values2),
[](int i) { return i < 500;});
// 뒷부분은 지워주기
Values2.erase(Iter, end(Values2));
for (const auto& i : Values2) cout << i << " ";
cout << endl;
// 104 108 116 132 164 228 356
copy_n(cbegin(Values), 3, begin(Values3));
for (const auto& i : Values3) cout << i << " ";
cout << endl;
// 104 108 116 0 0
move_backward(cbegin(Values3), cend(Values3), end(Values));
for (const auto& i : Values) cout << i << " ";
cout << endl;
// 104 108 116 132 164 104 108 116 0 0
replace_if(begin(Values3), end(Values3),
[](int i) {return i < 105; }, 3);
for (const auto& i : Values3) cout << i << " ";
cout << endl;
// 3 108 116 3 3
auto Iter2 = remove_if(begin(Values3), end(Values3),
[](int i) {return i == 3;});
Values3.erase(Iter2, end(Values3));
for (const auto& i : Values3) cout << i << " ";
cout << endl;
// 108 116
vector<int> Values4 {1, 1, 2, 2, 3, 4};
unique(begin(Values4), end(Values4));
for (const auto& i : Values4) cout << i << " ";
cout << endl;
// 1 2 3 4 3 4
reverse(begin(Values4), end(Values4));
for (const auto& i : Values4) cout << i << " ";
cout << endl;
// 4 3 4 3 2 1
}
연산 알고리즘
- for_each(), for_each_n()이 있다.
- for_each()
- 주어진 범위에 있는 원소마다 콜백 실행
- for_each_n()
- 첫 번째 부터 n번째 원소까지 콜백 실행
- 실전에서는 for문을 사용하는 것이 낫다. 구현도 쉽고 가독성도 높음
분할 알고리즘
- parition_copy()는 원본에 있는 원소를 복제해 다른 두 대상으로 분할(partition)한다.
- 둘 중 어느 대상으로 보낼지는 predicate의 true, false 에 따라 결정된다.
- 반복자 쌍을 리턴하는데, 하나는 첫 번째 대상 범위에서 마지막으로 복제한 원소의 바로 다음 지점, 다른 하나는 두 번째 대상..
- parition() 알고리즘은 predicate에서 true를 리턴하는 원소가 false 리턴하는 함수보다 앞에 나오도록 정렬한다. 원래 순서가 유지되지 않는다.
정렬 알고리즘(sorting algorithm)
- 특정 조건을 기준으로 순서에 맞게 유지하도록 재배치한다.
- 순차 컨테이너에만 정렬 알고리즘 적용이 가능하다.
- 정렬 연관 컨테이너는 항상 정렬되어 있는 상태니 적용할 수 없고, 비정렬 연관 컨테이너도 적용할 수 없다.
- list, forward_list는 자체 정렬 메서드를 이용하는 것이 낫다.
- stable_sort()는 원본에 나온 순서를 그대로 유지한다.
- 선택 알고리즘인 nth_element() 가 있다.
이진 탐색 알고리즘
- binary_search(), lower_bound(), upper_bound(), equal_range() 등이 있다.
- map, set은 자체 메서드 제공한다.
- lower_bound()
- 주어진 값보다 작지 않은 원소 중 첫 번째를 찾는다.
- binary_search()
- 탐색 범위 시작, 끝 반복자, 탐색할 값, 옵션으로 비교 콜백을 인수로 받는다.
- 찾으면 true, 없으면 false 를 반환한다.
집합 알고리즘(set algorithm)
- 모든 정렬된 범위에 적용 가능하다.
- includes()
- 부분 집합을 판별하는 표준 함수이다.
- 순서를 고려하지 않는다.
- set_union()
- 합집합
- set_intersection()
- 교집합
- set_difference()
- 차집합
- set_symmetric_difference()
- 대칭차집합
- XOR 연산을 적용한 결과로 집합을 만듬
- merge()
- 정렬된 두 범위를 하나로 합칠 수 있다.
- 정렬 순서는 그대로 유지된다.
- 선형 복잡도를 가진다.
최대/최소 알고리즘
- min(), max()
- operator< 또는 사용자가 정의한 이항 predicate으로 비교해서 각각 최소 원소와 최대 원소에 대한 const 레퍼런스를 리턴
- minmax()
- 최솟값과 최댓값을 쌍으로 묶어 리턴
- 반복자로 지정한 범위에 대해 처리하려면 min_element(), max_element()를 사용해야 한다.
- std::clamp()는 주어진 값이 최솟값과 최댓값 사이에 있는지 검사해서, 최솟값 보다 작으면 최솟값에 대한 레퍼런스를 리턴하고, 최댓값보다 크면 최댓값에 대한 레퍼런스를 리턴하며, 사이에 있다면 주어진 값에 대한 레퍼런스를 리턴한다.
Leave a comment