template<
class Key,
class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>,
class Allocator = std::allocator<Key>
>
class unordered_set;
unordered_set
은 순서가 존재하지 않는 해시에 의해서 값이 저장되는 구현을 가졌다. 기존 set
이 균형 이진 탐색 트리를 이용한 것에 비해 hash table로 구현하면서 기존 O(lgN)이었던 연산을 O(1)로 더 효율적으로 연산할 수 있다. 기본적인 사용법은 기존 set과 유사하다. <unordered_set>
헤더에 구현되어 있고, 중복을 허용하는 unordered_multiset
도 존재한다.
컨테이너의 내부는 체이닝을 구조의 해시 테이블이다. 테이블은 vector<list<key>> buckets
형태로 이루어져 있는데, 여기서 vector는 전용으로 구현되었고, list는 std::list를 그대로 사용한다. key
를 해싱한 값을 h라고 하면, h의 값은 0 ~ buckets.size의 범위를 갖는다. 삽입이 이루어지는 것은 buckets[h].insert(key)
와 같은 형태로 저장한다. 자세한 설명은 해시 컨테이너 내부 구조에 정리했다.
1. 사용
사용법은 대부분 set과 동일하다. 차이점은 기존 O(lgN)의 연산들을 O(1)로 더 효율적으로 처리할 수 있다는 것이다.
생성
기본 생성자, 복사 생성자 이동 생성자 및 initializer_list와 이터레이터를 입력으로 하는 기본적인 5개의 생성자가 존재한다. 각 생성자들은 bucket_count
라는 입력을 추가로 가질 수 있다. bucket_count
는 내부적으로 할당될 buckets의 개수이다. 그 외에 template으로 hash나 == 연산인 equal_to를 변경할 수 있지만, 보통 이를 변경하는 일은 잘 없다.
- 기본 생성자
- bucket_count를 전달하는 생성자
- initializer_list를 전달하는 생성자
- 시작과 끝의 이터레이터를 전달하는 생성자
- 복사 생성자
- 이동 생성자
// 1. 기본 생성자
// 아무것도 할당되지 않은 상태로 초기화.
unordered_set<int> s1;
// 2. bucket_count
// bucket은 항상 N>2인 보다 2^N 이고, 요소의 수보다 크다.
unordered_set<int> s2(16); // 16개의 요소를 저장 가능한 공간 할당
// 3. initializer_list
unordered_set<int> s3 = { 1,2,3 }; // 1 2 3
unordered_set<int> s3_b = { { 1,2,3 }, 16 }; // bucket_count
unordered_set<int> s3_2{ 1,2,3 };
unordered_set<int> s3_2b{ { 1,2,3}, 16 }; // bucket_count
unordered_set<int> s3_3({ 1,2,3 });
unordered_set<int> s3_3b({ 1,2,3 }, 16); // bucket_count
// 4. iterator
unordered_set<int> s4(s3.begin(), s3.end()); // 1 2 3
unordered_set<int> s4_2(s3.begin(), s3.end(), 16); // bucket_count
// 5. copy
unordered_set<int> s5(s4); // 1 2 3
// 6. move
unordered_set<int> s6(move(s5)); // 1 2 3
접근
set형 컨테이너는 기본적으로 접근 연산을 지원하지 않는다. 요소에 접근하기 위해서는 이터레이터를 사용하거나, 조회(Lookup) 메서드를 사용해야 한다.
이터레이터
++
와 --
를 사용할 수 있다. *
를 통해서 해당 이터레이터의 Key값에 접근할 수 있지만, 수정할 수는 없다.
용량
size()
, empty()
, max_size()
만을 지원한다.
수정
insert()
와 emplace()
로 요소를 삽입하고, erase()
로 요소를 삭제한다.
unordered_set<int> s;
s.insert(5); // 5
s.emplace(3); // 5 3
s.emplace(3); // 이미 존재하기 때문에 아무 일도 일어나지 않는다.
s.erase(3); // 5
s.erase(3); // 3은 존재하지 않기 때문에 아무 일도 일어나지 않는다.
merge()
를 이용해 두 컨테이너를 하나로 합칠 수 있다. 매개변수로 들어온 컨테이너는 복사가 아니라 이동된다. clear()
메서드로 컨테이너를 비울 수 있다.
unordered_set<int> s1{ 1,3,5,7 };
unordered_set<int> s2{ 2,4,6,8 };
s1.merge(s2); // 1 3 5 7 2 4 6 8
s1.clear(); // empty
extract()
는 unordered_set의 요소를 제거하면서 제거된 요소의 node_handle을 반환한다.
unordered_set<int> s{ 1,2,3,4,5 };
auto v1 = s.extract(3);
v1.value(); // 3
조회
unordered_set
핵심은 값의 존재 여부를 검색하고, 중복을 제거하는 것이다. 이러한 연산을 조회(Lookup)이라고 한다.
count(v)
- 보관된 v의 개수를 반환한다.find(v)
- v가 존재하면 v의 이터레이터를, 아니면 past-the-end 이터레이터를 반환한다.contains(v)
- v가 존재하면 true를 반환한다.equal_range(v)
- v값이 존재하는 범위를 반환한다. (범위는 시작 이터레이터와과 끝의 다음 이터레이터로 표현된다.)
unordered_set<int> s{ 1,2,3, 3,7,9 }; // 1 2 3 7 9
cout << s.count(3) << endl; // 1
if (auto iter = s.find(3); iter != s.end()) {
cout << *iter << endl; // 3
}
if (auto iter = s.find(5); iter != s.end()) {
cout << *iter << endl; // iter == past-the-end
}
cout << (s.contains(1)) << endl; // 1
cout << (s.contains(5)) << endl; // 0
auto [begin, end] = s.equal_range(3); // 3~7
for (auto iter = begin; iter != end; iter++) {
cout << *iter << ' '; // 3
}
1
3
1
0
3
bucket 인터페이스
unordered_set
은 내부에 존재하는 hash table과 관련된 연산을 지원한다.
begin(n)
: n번 bucket이 가리키고 있는 리스트의 이터레이터를 반환한다.end(n)
: n번 bucket이 가리키고 있는 리스트의 past-the-end를 반환한다.bucket(key)
: key가 저장된 bucket의 번호를 반환한다.bucket_count()
: 총 bucket의 수를 반환한다.max_bucket_count()
: 할당 가능한 버킷의 총량을 반환한다.bucket_size(n)
: n번 bucket에 저장된 요소의 수를 반환한다.
#include<iostream>
#include<unordered_set>
using namespace std;
void print_buckets(unordered_set<int> s) {
for (int i = 0; i < s.bucket_count(); i++) {
cout << i << '\t';
for (auto iter = s.begin(i); iter != s.end(i); iter++) {
cout << " -> " << *iter;
}
cout << endl;
}
cout << endl;
}
int main() {
cout << endl << "## bucket interface" << endl;
unordered_set<int> s{ 1,5,8,10,13,15,17 };
print_buckets(s);
cout << "8은 " << s.bucket(8) << "번 버킷에 저장되어 있다." << endl;
cout << "3은 " << s.bucket(3) << "번 버킷에 저장되어 있다." << endl;
cout << s.bucket_count() << endl; // 8
cout << s.bucket_size(0) << endl; // 2
cout << s.bucket_size(1) << endl; // 0
cout << s.bucket_size(2) << endl; // 1
}
bucket list
0 -> 13 -> 5
1
2 -> 15
3
4 -> 17 -> 1
5 -> 8
6
7 -> 10
8은 5번 버킷에 저장되어 있다.
3은 6번 버킷에 저장되어 있다.
8
2
0
1
hash 관련
hash table 관련 정보를 얻거나, 재배열할 수 있다.
load_factor
: 현재 테이블의 적재율을 반환한다. (요소의 수 / buckets의 크기)max_load_factor()
: 적재율의 최대 값을 반환한다. (기본 1.0)max_load_factor(float)
: 최대 적재율을 float으로 변경한다. 만약 적재율이 최대 적재율 보다 큰 경우 buckets을 재 설정해서 적재율을 최대 적재율 밑으로 낮춘다.rehash(n)
: 적재율 조건을 만족하면서, n보다 큰 수로 bucket_count를 할당하고, 기존 요소들을 재배열한다. (최악의 경우 O(N^2))reserve(n)
: 요소가 최대 n이 입력된다고 가정하고 buckets의 크기를 할당한다. n일 때 적재율이 최대 적재율이 되지 않는 크기를 계산해 할당한다. (내부적으로 rehash를 사용)
unordered_set<int> s{ 1,5,8,10,13,15,17 };
cout << "적재율 :" << s.load_factor() << endl;
cout << "최대 적재율 : " << s.max_load_factor() << endl;
cout << "\n현재 구조" << endl;
print_buckets(s);
// 최대 적재율 변경
s.max_load_factor(0.5f);
cout << "\n 변경된 구조" << endl;
print_buckets(s);
cout << "\n rehash 구조 (8->16)" << endl;
s.rehash(0); //
print_buckets(s);
## hash
적재율 :0.875
최대 적재율 : 1
현재 구조
0 -> 5 -> 13
1
2 -> 15
3
4 -> 1 -> 17
5 -> 8
6
7 -> 10
변경된 구조
10 -> 15
31 -> 10
32 -> 5
36 -> 1
40 -> 13
52 -> 17
61 -> 8
rehash 구조 (64->16)
0 -> 5
1
2
3
4 -> 17 -> 1
5
6
7
8 -> 13
9
10 -> 15
11
12
13 -> 8
14
15 -> 10