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를 변경할 수 있지만, 보통 이를 변경하는 일은 잘 없다.

  1. 기본 생성자
  2. bucket_count를 전달하는 생성자
  3. initializer_list를 전달하는 생성자
  4. 시작과 끝의 이터레이터를 전달하는 생성자
  5. 복사 생성자
  6. 이동 생성자
// 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