tolower, mismatch 를 이용해서 구현하는 방법이 있고, lexicoraphical_compare 를 이용할 수도 있다. 자세한 내용은 귀찮으므로 패스.
정렬된 범위에 대해 동작하는 알고리즘이 어떤 것들인지 파악해 두자.
특정 알고리즘들은 미리 정렬시킨 범위를 넣어주어야 한다. 만약 이러한 규칙을 지키지 않았을 경우에는 실행시 동작을 예측 할수 없게 된다.
binary_search | lower_bound | upper_bound | equal_range |
set_union | set_intersection | set_difference | set_symmetric_difference |
merge | inplace_merge | ||
include |
binary_search, lower_bound, upper_bound, equal_range 등은 내부적으로 이진 탐색을 사용해서 값을 찾기 때문에 반듯이 정렬된 데이터 범위가 필요하다.
일반적으로 위의 알고리즘들은 로그 시간의 성능을 내준다고 하지만 임의 접근 반복자를 사용할때에만 그렇다. 예를 들어 양방향 반복자등을 사용한다면 한곳에서 다른 곳으로 이동시 인접한 데이타만으로 이동이 가능하므로 선형시간 이상이 걸리게 된다.
정렬 기준에 대한 첨언
binary_search, lower_bound, upper_bound, equal_range 등의 알고리즘은 당연히 정렬된 기준과 동일한 기준으로 검색기준을 가져야 정상적으로 작동을 할것이다.vector<int> v;
…
sort(v.begin(), v.end(), greater<int>());
…
bool a5Exists = binary_search(v.begin(), v.end(), 5);
bool a5Exists = binary_search(v.begin(), v.end(), 5, greater<int>());
이와는 반대로 unique류의 알고리즘은 상등성에 의해 두 객체의 같음을 판별하게 된다.(EffectiveSTL19 참조)
remove와 비슷한 알고리즘을 포인터의 컨테이너에 적용할 때에는 각별히 조심하자.
EffectiveSTL32에서 언급한 것처럼 remove(remove_if, unique)는 지워질 데이터에 대해서 지워 지지 않는 데이타로 덮어써 버리는 동작을 취한다. 이런 동작은 컨테이너의 요소타입이 포인터일 경우 심각한 메모리 누수 현상을 일으킬 가능성이 있다.
요소를 정말로 제거하고자 한다면 remove 류의 알고리즘에는 꼭 erase를 붙여 사용하자.
remove 알고리즘의 경우 범위를 나타내는 한쌍의 반복자를 인자로 받는다. 여기서 문제가 생기는데 반복자만으로는 어떤 컨테이너 인지 알수 없을 뿐더러 해당 컨테이너를 접근할수도 없다.
그 결과 remove 알고리즘을 호출하더라 하더라도 컨테이너에 들어있는 요소의 개수가 삭제되지 않는 결과가 나타나게 된다. 이유는
romove는 어느것도 진짜로 없애지 않는다. 없앨수 없기 때문이다.
vector<int> v;
v.reserve(10);
for(int i=1; i<=10; ++i)
{
v.push_back(i);
}
v[3] = v[5] = v[9] = 99;
remove(v.begin(), v.end(), 99);
v.begin() v.end()같은 상태이다. remove를 호출한 이후는
↓ ↓
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│1 │2 │3 │99│5 │99│7 │8 │9 │99│
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
v.begin() v.end()위와 같은 상태를 갖게 된다. 즉 remove는 남아있을 값을 앞쪽으로 모아 놓고 논리적인 마지막 위치의 반복자를 리턴하게 되는 것이다. 이후 remove에서 리턴한 반복자와 원래 컨테이너의 end() 반복자 사이의 요소를 컨테이너의 erase를 호출하여 삭제해줘야 한다.
↓ ↓
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│1│2│3│5│7│8│9│?│?│?│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
↑
remove에서 리턴된 반복자위치
vector<int> v;
…
v.erase(remove(v.begin(), v.end(), 99), v.end());
remove와 유사한 알고리즘으로 remove_if 가 존재하며 비슷하게 동작하는 unique(인접한 중복갑제거)도 존재한다. 즉 remove_if와 unique등은 remove와 마찬가지로 실제로 데이타를 삭제하기 위해서는 erase를 같이 호출해 주어야 한다.
list의 경우는 예외적으로 remove 와 unique 맴버함수가 존재하며 위의 설명과는 다르게 바로 데이타를 삭제한다.
위의 remove, remove_if, unique등은 삭제 대상이 되는 데이타는 신경쓰지 않고 덮어써 버리는 구조를 취하게 되므로 포인터를 컨테이너에 넣는 경우 주의해야 한다.(EffectiveSTL33)
정렬시의 선택 사항들을 제대로 파악해보자
- vector, string, deque 혹은 C++ 배열에 대해 전체 정렬을 수행할 필요가 있을 때에는 sort 나 stable 소트를 사용한다.
- vector, string, deque 혹은 C++ 배열에 대해 상위 n개의 요소만 순서에 맞추어 뽑아내고 싶다면 partial_sort를 사용한다.
- vector, string, deque 혹은 C++ 배열에 대해 상위 n개의 요소를 뽑되 순서를 고려할 필요가 없다면 nth_element가 적합하다.
- 표준 시퀀스 컨테이너가 있고, 이 컨테이너의 요소들을 어떤 기준에 만족하는 것들과 그렇지 않은 것들을 모아 구분하고 싶다면 partition 혹시 stable_partition 을 사용한다. (등급을 구분하거나 더 나아가 몇 등급이상의 것만 얻거나)
- list의 경우 partition, stable_partition 은 직접 사용이 가능하며 sort 와 stable_sort 알고리즘 대신 list::sort 멤버 함수를 사용할수 있다. 만일 partial_sort나 nth_element의 기능이 필요하다면 백터등으로 변환하거나 반복자를 백터에 넣어 정렬하는 간접적인 방법을 사용해야 한다.
- partition
- stable_partition
- nth_element
- partial_sort
- sort
- stable_sort
nth_element 의 또 다른 사용법과 다른 알고리즘의 사용예는 책을 참고
알고리즘의 데이터 기록범위는 충분히 크게 잡자
컨테이너를 대상으로 알고리즘을 수행할때는 언제나 알고리즘의 결과가 어디에 입력이 되는지를 생각해야 한다. 예를 들어
int transmogrify(int x); // 정수 x를 받아
// 새로운 값을 생성
vector<int> values;
vector<int> result;
transform(values.begin(), values.end(),
result.end(), // 알고리즘의 결과를
transmogrify); // result 뒤에 추가 시도
int transmogrify(int x);
//…
vector<int> values;
list<int> result;
transform(values.begin(), values.end(),
front_inserter(result),
transmogrify);
list 같은 의 경우 노드 단위의 컨테이너 이므로 앞,뒤 삽입이 자유로우며 삽입에 따른 오버헤드 역시 별로 없다. 하지만 임의 위치에 삽입은 탐색에 대한 오버헤드가 존재한다.
vector 등의 front_inserter를 지원하지 않고 모든 삽입의 경우 재할당 오버헤드가 발생할 가능성이 있다. 하지만 reserve 나 resize 를 이용해 재할당 오버헤드를 방지할수 있다.(물론 reserve등을 쓴다고 해도 back_inserter 등의 함수를 사용해야 반복자 위치를 정상적으로 증가 시킬수 있다.)
즉 각 컨테이너의 구현 방식에 따라 효율적인 방법을 선택해야 하는것이다.
문자 단위의 입력에는 istreambuf_iterator의 사용도 적절하다.
어떤 텍스트 파일을 string 객체에 복사해야 한다고 할때
ifstream inputFile(“file.txt”);
string fileData((istream_iterator<char>(inputFile)),
istream_iterator<char>());
ifstream inputFile(“file.txt”);
inputFile.unsetf(ios::skipws);
string fileData((istream_iterator<char>(inputFile)),
istream_iterator<char>());
ifstream inputFile(“file.txt”);
string fileData((istreambuf_iterator<char>(inputFile)),
istreambuf_iterator<char>());
ostreambuf_iterator 역시 비슷한 속성을 가지고 있다.(문자단위의 비서식화 출력 스트림 반복자)
reverse_iterator에 대응되는 기점 반복자(base iterator)를 사용하는 방법을 정확하게 이해하자
아래는 vector에 1~5까지의 5개의 int 데이타를 넣고 reverse_iterator를 써서 3을 가리키게 한후, iterator하나를 revserse_iterator의 기점으로 세팅하는 코드이다.
vector<int> v;
v.reserve(5); // 미리 할당 EffectiveSTL14 를 참고for( int i=1; i<=5; i++) {
v.push_back(i);
}// 3을 가리킨다.
vector<int>::reverse_iterator ri = find(v.rbegin(), v.rend(), 3);// i가 ri의 기점과 같게 한다.
vector<int>::iterator i(ri.base());
위의 코드를 도식화 해서 표현하자면 이렇게 된다.
rend() rbegin()
↓ ↓
┌─┬─┬─┬─┬─┐
│1│2│3│4│5│
└─┴─┴─┴─┴─┘
↑ ↑
begin() end()
(크아 그리느라 죽는줄 알았어!)
insert나 erase는 reverse_iterator를 인자로 받아들이지 않는 경우가 있기 때문에 위의 경우와 같이 reverse_iterator를 사용하다가 insert나 erase가 필요한 경우는 iterator로 변환을 해야 한다. 그 때 사용하는 메소드가 바로 base이고 위의 그림과 같은 대응되는 위치를 갖는다.
insert를 할 경우에는 저런 관계가 아주 정확하게 성립된다. STL의 세계 에서의 요소 삽입은 반복자가 가르키는 위치의 바로 앞에서 이루어 진다는 점을 생각할때 3을 가르키는 ri의 기점 반복자가 4를 가리키고 있으므로 ri의 base() 호출에 의한 i로 insert() 를 호출시 정확히 원하는 위치인 3과 4 사이에 데이타를 삽입하게 된다.
위의 그림을 볼때 ri가 가르키는 데이타를 삭제해서 나타나는 원하는 결과는 3이 삭제되는것일 것이지만 base()함수를 이용해 기점 반복자 i를 얻어 erase()를 호출하면 4가 삭제되어 버릴것이다.
이런 erase관련 문제를 해결하기 위해서 reverse_iterator의 위치 값을 하나더 증가시킨뒤 base() 를 호출하는 방법을 생각할수 있다.
v.erase( (++ri).base() ); // ri가 가르키는 요소를 삭제한다.
const_iterator를 iterator로 바꾸는 데에는 distance와 advance를 사용하자.
EffectiveSTL26 에서 설명한 것 처럼 iterator만 받아들이는 함수가 있어서 const_iterator를 iterator로 변환해야 하는 경우가 생길수 있는데 일반적으로 생각하는 캐스팅 연산으로는 변경을 할수가 없다. iterator 와 const_iterator 는 서로 상속 관계등이 존재하는것이 아닌 전혀 다른 클래스이기 때문이다. (vector 나 string은 iterator가 포인터이기 때문에 가능할수도 있다.)
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;IntDeque d;
ConstIter ci;
// 생략
Iter i(d.begin());
advance(i, distance<ConstIter>(i,ci));
즉 distance<ConstIter>(i,ci) 에 의해서 i가 자동은 const_iterator 로 변환되후(iterator는 const_iterator로 변환가능 EffectiveSTL26 참조) ci와의 거리를 구한다. advance는 그 거리만큼 i를 이동시켜 결과적으로 ci와 같은 위치를 가르키는 iterator를 얻게 된다.
const_iterator 나 reverse_iterator, const_reverse_itertor도 좋지만 역시 쓸만한 것은 iterator 이다
- iterator는 모든 다른 반복자로 변환이 가능하다.
- reverse 반복자류는 해당하는 종류의 반복자로 base() 맴버함수를 통해 변환이 가능하다.
(reverse_iterator -> iterator, const_reverse_iterator -> const_iterator )
- reverse_iterator는 const_reverse_iterator 로 변환이 가능한다.
- 어떤 형태의 insert와 erase 멤버함수는 무조건 iterator 만을 넘겨야 한다.
- const_iterator 를 iterator로 암시적으로 변환하는 방법은 없고 굳이 바꾸자면 EffectiveSTL27의 내용을 참조한다. 하지만 일반성도 떨어지고 효율에 대한 보장도 할수 없다.
- reverse_iterator를 iterator로 변환할수 있으나, 변환후 약간의 조정이 필요하다. 자세한 내용은 EffectiveSTL28을 참고한다.