TreeSet — 정렬과 범위 쿼리를 지원하는 Set

TreeSet의 Red-Black Tree 기반 구조, O(log n) 성능, NavigableSet 메서드(floor/ceiling/headSet/tailSet/subSet), Comparable과 Comparator를 통한 커스텀 정렬, 그리고 TreeSet이 적합한 상황

· 4 min read · PALDYN Team

지난 글에서 삽입 순서를 유지하는 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)**을 보장한다.

TreeSet 내부 — Red-Black Tree 기반 정렬

기본 사용

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

TreeSetNavigableSet<E>를 구현한다. 이 인터페이스는 범위 탐색에 특화된 강력한 메서드를 제공한다.

TreeSet NavigableSet 주요 메서드

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 불허

TreeSetnull을 허용하지 않는다. 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 — 해시 맵의 내부 구조와 성능


읽어주셔서 감사합니다. 😊