일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 비교 함수 객체
- 반복자
- 언리얼
- operator new
- Effective c++
- 참조자
- 메타테이블
- 영화 리뷰
- implicit conversion
- 상속
- lua
- 예외
- 티스토리챌린지
- more effective c++
- 루아
- 다형성
- reference
- virtual function
- resource management class
- UE4
- 영화
- Vector
- Smart Pointer
- exception
- 게임
- 스마트 포인터
- 암시적 변환
- c++
- effective stl
- 오블완
Archives
- Today
- Total
스토리텔링 개발자
[Effective STL] 23. 이진 탐색 트리 vs 정렬된 vector 본문
728x90
항목 23. 연관 컨테이너 대신에 정렬된 vector를 쓰는 것이 좋을 때가 있다
빠른 데이터 검색을 하고 싶다
- 보통은 연관 컨테이너를 생각하게 된다.
- 그 중 해시 컨테이너를 선택하는 것이 좋다.(항목 25 참조)
- map(로그 시간), hashmap(상수 시간)
- 탐색이 로그 시간 정도가 적당하다고 판단했더라도, 연관 컨테이너보단 벡터가 나을 수 있다.
균형 이진 탐색 트리(balanced binary search tree)
- 데이터 노드의 삽입, 삭제, 탐색이 언제 이루어질지 예측할 수 없는 상황에 적합한 자료구조.
- 어떤 식으로 사용해도 안정적인 성능을 보여준다.
- 즉, 삽입, 삭제, 탐색의 수행이 일정한 순서를 따르지 않으며, 다음에 어떤 동작이 이루어질 지 예측할 수 없더라도 안정적인 성능을 보장한다.
- 하지만 대부분은 그렇게 혼란스럽게 사용하지 않는다.
- 보통은 아래 세 단계 순서로 사용하게 될 것이다.
- 데이터 셋업(setup) : 요소 삽입
- 데이터 탐색(lookup) : 요소 탐색
- 데이터 재구성(reorganize) : 요소 변경
- 이런 순으로 자료 구조를 사용하는 상황이라면
- 정렬되었다는 가정 하에, 벡터가 연관 컨테이너보다 나은 수행성능(수행 시간, 메모리 공간 모두)을 제공할 가능성이 높다.
- 정렬되어 있어야 탐색 알고리즘(binary_search, lower_bound, equal_range 등)을 제대로 적용할 수 있다.(항목 34 참조)
벡터가 이진 탐색보다 나은 이유
1. 메모리 크기
- 이진 탐색 트리의 각 노드는 기본적으로 아래를 포함하게 된다.
- 노드 왼쪽 자식, 오른쪽 자식 노드의 포인터
- 대개 부모 노드에 대한 포인터까지
- 즉, 요소별 최소 세 개의 포인터를 담는 메모리가 오버헤드로 걸린다.
- 벡터 역시 메모리 오버헤드는 있다.
- 메모리의 여유 분이 예약되기 때문이다.(항목 14 참조)
- 하지만 보통 세 개의 포인터 혹은 두 개의 포인터와 int 하나 정도 분일 뿐이다.
- 더군다나 swap trick으로 잘라낼 수도 있다.(항목 17 참조)
- 탐색시 그 부분을 건드리지도 않으므로 수행 성능에 영향도 없다.
벡터가 이진 탐색보다 나은 이유
2. 메모리 참조 위치의 근접성(locality of reference)
- 만약 데이터 크기가 무지하게 크다고 가정해보면, 요소들은 여러 메모리 페이지에 걸쳐서 저장될 것이다.
- 페이지 사용량을 볼 때, 벡터가 이진 탐색 트리보다 훨씬 적은 양을 소비한다.
- 벡터는 요소 하나에 별도의 오버헤드가 걸리지 않으므로 당연한 이야기이다.
- 예컨대, 요소의 메모리 크기가 12 바이트, 작업하는 시스템의 포인터의 크기가 4 바이트, 메모리 페이지 하나의 크기가 4096(4K)라고 가정해보자.
- 컨테이너 자체의 오버헤드를 무시하고 계산할 때,
- vector의 경우 한 페이지에 341개의 요소가 들어간다.
- 연관 컨테이너(이진 탐색 트리)의 경우 170개 가량이 들어간다.
- 약 두 배의 차이가 난다!
- 사용 메모리가 많아지만 페이지 오류(page fault)도 그만큼 많아지며, 그만큼 속도에서 손해를 보게 된다.
- 벡터처럼 연속 메모리 구조를 취하지 않는 노드 기반 컨테이너는, 이동 경로가 가까운 노드 사이라도 반드시 물리 메모리 위치도 가까울 거리는 보장을 할 수 없다.
- 이진 탐색 시의 페이지 오류를 최소화하려면 가까운 도드들은 인접한 물리 메모리에 저장하도록 구조를 만드는 것이 정석이다.
벡터의 단점
- 늘 정렬된 상태로 있어야 한다.
- 요소가 삽입, 삭제될 때마다 위치 뒷쪽 요소는 모두 한 칸씩 움직여야 한다.
- 여유 메모리가 없다면 재할당까지 발생한다.(항목 14 참조)
- 하지만, 삽입, 삭제 동작과 탐색 동작이 거의 섞이지 않는 경우에는 정렬된 벡터가 훨씬 낫다.
set 대신 vector 사용하기
vector<Widget> vw; // set<Widget> 대신 쓰인 벡터
... // 데이터 셋업
sort(vw.begin(), vw.end());
Widget w;
{
... // 객체 탐색 시작
// binary_search로 탐색
if(binary_search(vw.begin(), vw.end(), w)) ...
// lower_bound로 탐색
vector<Widget>::iterator i =lower_bound(vw.begin(), vw.end(), w);
if(i != vw.end() && !(*i < w)) ...
// equal_range로 탐색
// range의 타입은 pair<vector<Widget>::iterator, vector<Widget>::iterator>
auto range = equal_range(vw.begin(), vw.end(), w);
if(range.first != range.second) ...
... // 객체 탐색 끝
}
sort(vw.begin(), vw.end()); // 데이터 재구성
- 각 탐색 알고리즘(binary_search, lower_bound 등)은... (항목 45 참조)
map 대신 vector 사용하기
- 만약 vector로 map을 대체하려고 한다면?
- 벡터에는 pair<K, V> 를 넣어야 한다.
- map<K, V>는 사실 pair<const K, V>가 저장되지만...
- 하지만 벡터는 정렬할 때 각 요소의 값이 대입(assignment) 연산을 통해 이동하므로 const를 제거해야 한다.
- 즉, 두 데이터는 '수정 가능'해야 한다.
- 벡터 정렬 시, pair의 기본 버전 operator<는 K, V를 모두 고려하므로
- K, V 중 V만 비교하도록 비교 함수를 커스텀해야 한다.
- 또한 정렬용 비교 함수 외에 탐색(lookup)용 비교 함수를 따로 두어야 한다.
- 정렬에 사용하는 비교함수는 두 개의 pair를 매개 변수로 받지만,
- 탐색은 키 값만 가지고 이루어지기 때문이다.
- 즉, 탐색용 비교 함수는 키 타입의 객체와 페어 객체를 매개변수로 받는 형태가 되어야 한다.
- 근데 첫 매개변수가 키 타입인지 페어 타입인지 미리 알 수 없으므로 양쪽 경우를 모두 고려하여 함수를 만들어야 한다..
using Data = pair<string, int>;
class DataCompare
{
public:
// 정렬용 비교 함수
bool operator()(const Data& lhs, const Data& rhs) const
{
return keyLess(lhs.first, rhs.first);
}
// 탐색용 비교 함수 1
bool operator()(const Data& lhs, const Data::first_type& k) const
{
return keyLess(lhs.first, k);
}
// 탐색용 비교 함수 2
bool operator()(const Data::first_type& k, const Data& rhs) const
{
return keyLess(k, rhs.first);
}
private:
bool keyLess(const Data::first_type& k1, const Data::first_type& k2) const
{
return k1 < k2;
}
};
vector<Data> vd; // map<string, int> 대신 쓰인 벡터
... // 데이터 셋업
sort(vd.begin(), vd.end(), DataCompare());
string s;
{
... // 객체 탐색 시작
// binary_search로 탐색
if(binary_search(vd.begin(), vd.end(), s, DataCompare())) ...
// lower_bound로 탐색
vector<Data>::iterator i =lower_bound(vd.begin(), vd.end(), s, DataCompare());
if(i != vd.end() && !(*i->first < s)) ...
// equal_range로 탐색
// range의 타입은 pair<vector<Data>::iterator, vector<Data>::iterator>
auto range = equal_range(vd.begin(), vd.end(), s, DataCompare());
if(range.first != range.second) ...
... // 객체 탐색 끝
}
sort(vd.begin(), vd.end(), DataCompare()); // 데이터 재구성
728x90
'개발 > Effective STL' 카테고리의 다른 글
[Effective STL] 25. hash 컨테이너 (0) | 2024.12.10 |
---|---|
[Effective STL] 24. map::operator[] vs map::insert (0) | 2024.12.09 |
[Effective STL] 22. set 요소의 key 바꾸기 금지 (0) | 2024.12.05 |
[Effective STL] 21. 연관 컨테이너 비교 함수 객체의 동일값 비교 (0) | 2024.12.04 |
[Effective STL] 20. 컨테이너 비교 함수 객체 (0) | 2024.12.03 |
Comments