TreeSet — 정렬과 범위 쿼리를 지원하는 Set
TreeSet의 Red-Black Tree 기반 구조, O(log n) 성능, NavigableSet 메서드(floor/ceiling/headSet/tailSet/subSet), Comparable과 Comparator를 통한 커스텀 정렬, 그리고 TreeSet이 적합한 상황
지난 글에서 삽입 순서를 유지하는 LinkedHashSet을 살펴봤다. 이번에는 **TreeSet**을 다룬다. TreeSet은 원소를 항상 정렬된 순서로 유지하고, 범위 탐색과 인접 원소 검색 같은 풍부한 쿼리 메서드를 제공한다.
내부 구조: Red-Black Tree
TreeSet은 내부적으로 TreeMap을 사용하며, TreeMap은 Red-Black Tree(자기 균형 이진 탐색 트리)로 구현되어 있다.
// java.util.TreeSet 내부 (간략화)
private transient NavigableMap<E, Object> m;
public TreeSet() {
this(new TreeMap<>());
}
Red-Black Tree는 삽입·삭제 후 트리를 자동으로 균형 잡아 높이를 O(log n)으로 유지한다. 따라서 모든 기본 연산(add, remove, contains)이 **O(log n)**을 보장한다.
기본 사용
import java.util.TreeSet;
TreeSet<Integer> nums = new TreeSet<>();
nums.add(5);
nums.add(3);
nums.add(8);
nums.add(1);
nums.add(4);
System.out.println(nums); // [1, 3, 4, 5, 8] — 정렬 순서
System.out.println(nums.first()); // 1
System.out.println(nums.last()); // 8
NavigableSet 메서드
TreeSet은 NavigableSet<E>를 구현한다. 이 인터페이스는 범위 탐색에 특화된 강력한 메서드를 제공한다.
headSet / tailSet / subSet
이 뷰 메서드들은 원본 TreeSet의 뷰를 반환한다. 뷰에 대한 변경은 원본에 반영되고, 원본의 변경도 뷰에 반영된다.
TreeSet<String> words = new TreeSet<>(
Set.of("apple", "banana", "cherry", "date", "elderberry")
);
// b 이상 d 미만 (알파벳 범위)
NavigableSet<String> sub = words.subSet("b", true, "d", false);
System.out.println(sub); // [banana, cherry]
// d 미만 모두
System.out.println(words.headSet("d")); // [apple, banana, cherry]
// c 이상 모두 (내림차순 뷰)
NavigableSet<String> desc = words.tailSet("c").descendingSet();
System.out.println(desc); // [elderberry, date, cherry]
Comparator를 통한 커스텀 정렬
기본 정렬은 Comparable(자연 순서)을 따른다. 커스텀 순서가 필요하면 생성자에 Comparator를 전달한다.
import java.util.Comparator;
import java.util.TreeSet;
// 문자열 길이 역순으로 정렬
TreeSet<String> byLength = new TreeSet<>(
Comparator.comparingInt(String::length).reversed()
.thenComparing(Comparator.naturalOrder())
);
byLength.add("apple");
byLength.add("fig");
byLength.add("banana");
byLength.add("kiwi");
System.out.println(byLength); // [banana, apple, kiwi, fig]
null 불허
TreeSet은 null을 허용하지 않는다. null을 삽입하거나 비교할 때 NullPointerException이 발생한다.
TreeSet<String> s = new TreeSet<>();
s.add(null); // NullPointerException!
HashSet/LinkedHashSet과 달리 TreeSet의 정렬 연산이 compareTo(또는 Comparator)를 호출하는데, null과의 비교가 정의되어 있지 않기 때문이다.
TreeSet vs HashSet 선택 기준
| 상황 | 권장 |
|---|---|
| 중복 제거만 필요 | HashSet |
| 삽입 순서 재현 필요 | LinkedHashSet |
| 원소를 항상 정렬된 상태로 유지 | TreeSet |
first() / last() / floor() / ceiling() 범위 쿼리 | TreeSet |
| 알파벳·숫자 순서 최솟값/최댓값 | TreeSet |
TreeSet은 HashSet보다 O(1) vs O(log n)으로 느리지만, 범위 쿼리와 정렬 유지가 필요할 때는 별도의 정렬 비용 없이 이를 자동으로 처리해준다.
// 슬라이딩 윈도우 최솟값 — TreeSet 활용
TreeSet<int[]> window = new TreeSet<>(Comparator.comparingInt(a -> a[0]));
window.add(new int[]{3, 0});
window.add(new int[]{1, 1});
window.add(new int[]{5, 2});
int[] min = window.first(); // [1, 1] — 첫 번째 원소가 최솟값
window.pollFirst(); // 최솟값 제거
지난 글: LinkedHashSet — 삽입 순서를 유지하는 Set
다음 글: HashMap — 해시 맵의 내부 구조와 성능
읽어주셔서 감사합니다. 😊