Set set = new HashSet(); 으로 그냥 공식처럼 사용하던 것을 발견하고 HashSet ?!?! Hash써서 단순히 중복만 방지가 아니겠는데? 생각이 들어 정리한 내용을 포스팅하고자 합니다.Set<String> names = new HashSet<>();
names.add("철수");
names.add("영희");
names.add("철수"); // 중복!
System.out.println(names); // [철수, 영희]
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private transient HashMap<E,Object> map;
// map의 value로 사용하는 더미 객체
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}
key →
hashCode()→ 버킷 인덱스 계산 → 해당 칸(버킷)에 저장
hashCode() 호출hashCode() 메서드를 가지고 있음철수.hashCode() -> 123456HashMap은 Java 8 이후 단순한 hashCode를 그대로 쓰지 않고 충돌을 줄이기 위해 상위 비트와 하위 비트를 XOR 연산으로 섞음static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Hash 값 / Hash Table의 크기% 대신 빠른 비트 연산(&) 사용int index = (table.length - 1) & hash;| key | hashCode() | hash() | table.length | bucket index |
|---|---|---|---|---|
| "철수" | 123456 | 123463 | 16 | 7 |
| "영희" | 654321 | 654328 | 16 | 8 |
| 연산 | 평균 | 최악 | 설명 |
|---|---|---|---|
add() | O(1) | O(log n) 또는 O(n) | 해시 충돌 적으면 빠름 |
contains() | O(1) | O(log n) 또는 O(n) | 버킷 인덱스 직접 접근 |
remove() | O(1) | O(log n) 또는 O(n) | 동일 원리 |
size() | O(1) | O(1) | 단순 필드 값 반환 |
table = [ Entry[0], Entry[1], Entry[2], ... Entry[15] ] // 길이 16
위에서 언급한 버킷 인덱스가 결국엔 Java에서 배열의 인덱스로 쓰이니까 바로 O(1)로 가져올 수 있습니다.
최악 복잡도 설명
버킷 인덱스 가 같을 경우 내부적으로 리스트 혹은 트리 구조로 저장하고 있어 최악의 경우 가장 마지막 요소를 탐색할 경우 O(n) (Java 8 이후 8개 미만에 적용) 혹은 O(log n) (Java 8이후 8개 이상에 적용)table[7] → Entry("철수") → Entry("지영") ...
| 구분 | 내부 구조 | 중복 판단 방식 | 정렬 / 순서 | 성능 특징 | 주요 특징 / 사용 시점 |
|---|---|---|---|---|---|
| HashSet | HashTable (HashMap 기반) | hashCode() → 동일 시 equals() 비교 | ❌ 순서 없음 | 평균 O(1) (삽입/검색 빠름) | 가장 기본적인 Set, 중복 없이 빠른 접근이 필요할 때 |
| TreeSet | Red-Black Tree (이진 탐색 트리) | compareTo() 결과로 중복 여부 판단 | ✅ 자동 정렬 (오름차순 기본) | 검색 O(log n), 삽입/삭제 느림 | 정렬된 Set 필요할 때 (Comparable 구현 필수) |
| LinkedHashSet | HashTable + LinkedList | hashCode() + equals() | ✅ 입력 순서 유지 | O(1) (HashSet과 유사) | 입력 순서를 유지하면서 중복 방지 필요할 때 |
hashCode() ↓
┌───────────────────────┐
│ (table.length - 1) & hash │ → 버킷 인덱스 계산
└───────────────────────┘
↓
[7번 버킷]
↓
┌──────────────────────────────────┐
│ Entry1(hash=123, key="철수") → Entry2(hash=123, key="민수") │
└──────────────────────────────────┘
↓
equals() 비교로 중복 여부 판단
// HashMap.java 내부 클래스
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // key의 해시값 (미리 계산해 저장)
final K key; // key 객체
V value; // value 객체 (HashSet에서는 더미 객체 PRESENT)
Node<K,V> next; // 다음 Entry (같은 버킷 내 연결 리스트)
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
}
== 로 해결public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
public LinkedHashSet() {
super(new LinkedHashMap<>());
}
}
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 이전, 다음 Entry를 가리킴 (이중 연결 리스트)
}
HashTable (버킷)
├─ [1] → A → (충돌 시) …
├─ [4] → B → …
├─ [7] → C → …
전역 순서 리스트 (이중 연결)
head ⇄ A ⇄ B ⇄ C ⇄ tail
💡 정리하자면,
TreeSet은 compareTo()로 정렬과 중복 판단을 동시에 수행하는 정렬형 Set