Питання 13 · Розділ 4

Що таке TreeSet і як він працює?

4. View = live, не копія 5. Java 21 → SequencedSet API 6. Cache miss при ітерації — враховуйте 7. Пам'ять у 3-4 рази більша за HashSet

Мовні версії: English Russian Ukrainian

🟢 Junior Level

TreeSet — відсортована колекція унікальних елементів.

Проста аналогія: Словник — слова завжди в алфавітному порядку, незалежно від того, в якому порядку ви їх додавали.

TreeSet<Integer> set = new TreeSet<>();
set.add(5);
set.add(2);
set.add(8);
set.add(1);

System.out.println(set);  // → [1, 2, 5, 8] (завжди відсортовано!)

Особливості:

  • Елементи завжди відсортовані
  • Не можна null
  • Повільніший за HashSet: O(log n) замість O(1) + кожен вузол — окремий об’єкт у купі (cache miss при обході). Але дані відсортовані.

🟡 Middle Level

Всередині: TreeMap

// TreeSet = адаптер над TreeMap
private transient TreeMap<E, Object> m;
private static final Object PRESENT = new Object();

// Всі операції делегуються TreeMap
// Складність: O(log n) для add/remove/contains

Червоно-чорне дерево

Самобалансувальне бінарне дерево:
  → Висота завжди O(log n)
  → При вставці/видаленні: rebalancing (повороти)
    Повороти = перебудова зв'язків між вузлами дерева, щоб висота залишалася O(log n). Без поворотів дерево виродиться у linked list з O(n).
  → Кольори вузлів (червоний/чорний) для балансу

    5(B)
   /   \
  2(R)  8(R)
 /  \
1(B) 3(B)

Навігація

TreeSet<Integer> set = new TreeSet<>(Set.of(1, 3, 5, 7, 9));

set.floor(6);    // → 5 (найближчий ≤ 6)
set.ceiling(6);  // → 7 (найближчий ≥ 6)
set.lower(5);    // → 3 (строго < 5)
set.higher(5);   // → 7 (строго > 5)

set.subSet(3, 8);    // → [3, 5, 7] (діапазон)
set.headSet(5);      // → [1, 3] (менше 5)
set.tailSet(5);      // → [5, 7, 9] (більше або дорівнює)

compareTo vs equals

// TreeSet використовує compareTo, НЕ equals!
class Person implements Comparable<Person> {
    String name;
    public int compareTo(Person o) {
        return name.compareTo(o.name);  // За ім'ям
    }
}

// Якщо compareTo = 0 → елемент НЕ додасться
// Навіть якщо equals повертає false!

// → equals та compareTo повинні бути узгоджені!

Коли НЕ використовувати TreeSet

  1. Потрібна тільки унікальність — HashSet швидший (O(1) vs O(log n))
  2. Елементи не імлементують Comparable та немає Comparator — TreeSet не зможе сортувати
  3. Пам’ять критична — TreeSet у 3-4 рази більший за HashSet (вузли дерева з prev/next/left/right)

🔴 Senior Level

Memory Footprint

Кожен Entry у TreeMap:
  Object Header: 12 байт
  K key: 4-8 байт
  V value: 4 байти (PRESENT)
  Entry left: 4-8 байт
  Entry right: 4-8 байт
  Entry parent: 4-8 байт
  boolean color: 1 байт
  Padding: до 8 байт

Разом: ~40-48 байт на елемент!

→ TreeSet у 3-4 рази більший за HashSet

Cache Locality проблема

Вузли дерева розкидані по Heap:
  [Entry1]...[Entry2]...[Entry3]

Кожен перехід до нащадка = cache miss!
  → Очікування з RAM: 100+ циклів

→ Ітерація TreeSet повільніша за HashSet

Java 21: SequencedSet

// TreeSet → SequencedSet (SequencedSet — вимагає Java 21+, недоступно на Java 11/17.)
set.getFirst();   // Найменший елемент
set.getLast();    // Найбільший елемент
set.reversed();   // Зворотний порядок (view)

// addFirst/addLast → UnsupportedOperationException
// → Порядок дерева фіксований!

View семантика

NavigableSet<Integer> view = set.subSet(3, true, 7, true);
// → "Живе" представлення, не копія!

view.add(5);       // Додає у ВИХІДНИЙ set
set.remove(5);     // Видаляє з view теж!

view.add(10);      // → IllegalArgumentException!
// → 10 поза діапазоном [3, 7]

Custom Comparator з null

// TreeSet не приймає null за замовчуванням
// Але можна через Comparator:
TreeSet<String> set = new TreeSet<>(
    Comparator.nullsFirst(Comparator.naturalOrder())
);
set.add(null);  // OK! null перший

Spliterator оптимізації

Spliterator<Integer> spliterator = set.spliterator();
// Характеристики:
//   SORTED   → Stream API оптимізує
//   DISTINCT → distinct() безкоштовний
//   SIZED    → відомий розмір

set.stream()
   .distinct()  // → Нічого не робить! Вже унікальні
   .count();    // → O(1), знає розмір

Production Experience

Реальний сценарій: Tax brackets

// Податкові ставки за порогом доходу
NavigableMap<Integer, Double> taxBrackets = new TreeMap<>();
taxBrackets.put(0, 0.0);      // 0% до 0
taxBrackets.put(10000, 0.1);  // 10% до 10к
taxBrackets.put(50000, 0.2);  // 20% до 50к

// Знайти ставку для доходу 35,000
double rate = taxBrackets.floorEntry(35000).getValue();
// → 0.1 (10%)

// HashMap НЕ зможе! Потрібне сортування

Best Practices

  1. TreeMap всередині → O(log n) операції
  2. compareTo = equals контракт!
  3. null заборонений (без custom Comparator)
  4. View = live, не копія
  5. Java 21 → SequencedSet API
  6. Cache miss при ітерації — враховуйте
  7. Пам’ять у 3-4 рази більша за HashSet

Резюме для Senior

  • TreeSet = TreeMap + PRESENT заглушка
  • Червоно-чорне дерево → O(log n), балансування
  • compareTo визначає унікальність, не equals
  • NavigableSet → floor/ceiling/subSet
  • View = live представлення, O(1) створення
  • Cache Locality погана → вузли розкидані
  • Memory ~40-48 байт на елемент
  • SequencedSet (Java 21) → getFirst/reversed

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • TreeSet = адаптер над TreeMap, елементи зберігаються як ключі, значення = static final Object PRESENT
  • Всередині: червоно-чорне дерево (самобалансувальне бінарне дерево), висота завжди O(log n)
  • Складність: O(log n) для add/remove/contains — повільніший за HashSet (O(1))
  • NavigableSet API: floor, ceiling, lower, higher, subSet, headSet, tailSet
  • Не приймає null (без custom Comparator) — NullPointerException
  • compareTo визначає унікальність, а не equals — якщо compareTo = 0, елемент не додасться
  • Memory: ~40-48 байт на елемент (left, right, parent, color) — у 3-4 рази більше за HashSet
  • Cache locality погана — вузли розкидані по heap, кожен перехід = cache miss
  • View (subSet і т.д.) = live представлення, не копія, O(1) створення

Часті уточнюючі запитання:

  • Чому TreeSet повільніший за HashSet? — O(log n) через дерево + cache miss при кожному переході між вузлами.
  • Що буде якщо compareTo та equals не узгоджені? — TreeSet може вважати різні елементи (за equals) однаковими (за compareTo = 0) — втрата даних.
  • Як додати null у TreeSet? — Через Comparator.nullsFirst(Comparator.naturalOrder()).
  • Навіщо потрібні View (subSet) та які у них обмеження? — Live представлення діапазону. Можна додавати тільки елементи всередині діапазону — інакше IllegalArgumentException.

Червоні прапорці (НЕ говорити):

  • ❌ «TreeSet використовує equals для порівняння елементів» — використовує compareTo!
  • ❌ «TreeSet швидший за HashSet» — у 50 разів повільніший через O(log n) та cache miss
  • ❌ «TreeSet можна використовувати з null за замовчуванням» — ні, тільки з custom Comparator
  • ❌ «subSet створює копію даних» — це live view, зміни відображаються у вихідній множині

Пов’язані теми:

  • [[11. В чому різниця між HashSet, LinkedHashSet та TreeSet]]
  • [[15. В чому різниця між HashMap, LinkedHashMap та TreeMap]]
  • [[16. Коли використовувати TreeMap]]
  • [[14. Що таке Map і які реалізації існують]]