TreeSet
TreeSet is a NavigableSet implementation backed by a red-black tree that stores elements in sorted order. It provides O(log n) time for add, remove, and contains operations. Every element must either implement Comparable or a Comparator must be supplied at construction. The sorted order enables a rich set of range and navigation operations: finding the element just greater or lesser than a target, extracting subsets over a range, and iterating in ascending or descending order — operations that HashSet and LinkedHashSet cannot support. TreeSet implements both SortedSet and NavigableSet, and understanding these interfaces is inseparable from understanding TreeSet's full capability. This entry covers the red-black tree structure, the ordering contract, all navigation and range operations, performance trade-offs, and the practical scenarios where sorted order justifies the O(log n) cost.
Red-Black Tree — Structure and Order
// ── TreeSet construction and ordering ────────────────────────────────
// Natural ordering — elements must implement Comparable
TreeSet<Integer> natural = new TreeSet<>();
natural.add(5); natural.add(3); natural.add(8); natural.add(1); natural.add(7);
System.out.println(natural); // [1, 3, 5, 7, 8] — always sorted ascending
// Custom comparator — reverse order
TreeSet<Integer> reversed = new TreeSet<>(Comparator.reverseOrder());
reversed.addAll(natural);
System.out.println(reversed); // [8, 7, 5, 3, 1] — descending
// Custom comparator for domain objects
record Person(String name, int age) {}
TreeSet<Person> byAge = new TreeSet<>(
Comparator.comparingInt(Person::age)
.thenComparing(Person::name));
byAge.add(new Person("Bob", 30));
byAge.add(new Person("Alice", 25));
byAge.add(new Person("Carol", 25));
for (Person p : byAge) System.out.println(p.name() + " " + p.age());
// Alice 25, Carol 25, Bob 30 — sorted by age then name
// ── Consistency with equals — the critical contract ───────────────────
// CORRECT: Comparator consistent with equals
TreeSet<String> correct = new TreeSet<>();
correct.add("apple");
correct.add("apple"); // duplicate — compareTo returns 0 — not added
System.out.println(correct.size()); // 1 — correct Set behaviour
// DANGEROUS: Comparator inconsistent with equals
TreeSet<String> inconsistent = new TreeSet<>(
Comparator.comparingInt(String::length)); // only compares by length
inconsistent.add("cat");
inconsistent.add("dog"); // same length as "cat" → compareTo returns 0 → NOT added!
inconsistent.add("horse");
System.out.println(inconsistent); // [cat, horse] — "dog" silently dropped!
// "cat" and "dog" are not equals() to each other, but the comparator treats them equal
// This violates Set contract: multiple non-equal objects appear as one
// ── Red-black tree structure (simplified) ─────────────────────────────
//
// TreeSet {1, 3, 5, 7, 8, 9, 11}:
//
// 7 (black)
// / // 3 9 (red)
// / / // 1 5 8 11 (black)
//
// contains(8): 7→right→9→left→8 — 3 comparisons, O(log n)
// contains(6): 7→left→3→right→5→right→null — 4 comparisons, not foundNavigation Operations — first, last, floor, ceiling, higher, lower
// ── Navigation method semantics ──────────────────────────────────────
TreeSet<Integer> set = new TreeSet<>(Set.of(1, 3, 5, 7, 9, 11));
// floor: greatest element <= target
System.out.println(set.floor(6)); // 5 (6 not in set, largest ≤ 6 is 5)
System.out.println(set.floor(5)); // 5 (5 is in set, exact match)
System.out.println(set.floor(0)); // null (nothing ≤ 0)
// ceiling: smallest element >= target
System.out.println(set.ceiling(6)); // 7 (6 not in set, smallest ≥ 6 is 7)
System.out.println(set.ceiling(7)); // 7 (exact match)
System.out.println(set.ceiling(12)); // null (nothing ≥ 12)
// lower: greatest element STRICTLY < target
System.out.println(set.lower(5)); // 3 (strictly less than 5)
System.out.println(set.lower(1)); // null (nothing < 1)
// higher: smallest element STRICTLY > target
System.out.println(set.higher(5)); // 7 (strictly greater than 5)
System.out.println(set.higher(11)); // null (nothing > 11)
// first and last
System.out.println(set.first()); // 1 — minimum element
System.out.println(set.last()); // 11 — maximum element
// pollFirst / pollLast — remove and return extremes
System.out.println(set.pollFirst()); // 1 — removed
System.out.println(set.pollLast()); // 11 — removed
System.out.println(set); // [3, 5, 7, 9]
// ── Real-world navigation patterns ───────────────────────────────────
// Find the next available appointment slot after a given time
TreeSet<LocalTime> available = new TreeSet<>(Set.of(
LocalTime.of(9, 0), LocalTime.of(10, 30),
LocalTime.of(14, 0), LocalTime.of(16, 30)));
LocalTime desired = LocalTime.of(11, 0);
LocalTime nextSlot = available.ceiling(desired); // 14:00 — next slot at or after 11:00
LocalTime prevSlot = available.floor(desired); // 10:30 — last slot at or before 11:00
// Stock price: highest price at or below $50
TreeSet<BigDecimal> prices = new TreeSet<>(Set.of(
new BigDecimal("45.00"), new BigDecimal("48.50"),
new BigDecimal("52.00"), new BigDecimal("55.00")));
BigDecimal threshold = new BigDecimal("50.00");
BigDecimal bestPrice = prices.floor(threshold); // 48.50Range Views — subSet, headSet, tailSet
// ── subSet — range view inclusive/exclusive control ──────────────────
TreeSet<Integer> set = new TreeSet<>(
Set.of(1, 3, 5, 7, 9, 11, 13, 15));
// subSet(from, to) — [from, to) left-inclusive, right-exclusive
NavigableSet<Integer> middle = set.subSet(5, 11);
System.out.println(middle); // [5, 7, 9]
// subSet with full inclusivity control
NavigableSet<Integer> closed = set.subSet(5, true, 11, true);
System.out.println(closed); // [5, 7, 9, 11]
// subSet exclusive both ends
NavigableSet<Integer> open = set.subSet(5, false, 11, false);
System.out.println(open); // [7, 9]
// ── headSet and tailSet ───────────────────────────────────────────────
NavigableSet<Integer> head = set.headSet(7); // [1, 3, 5] — strictly < 7
NavigableSet<Integer> headInc = set.headSet(7, true); // [1, 3, 5, 7] — <= 7
NavigableSet<Integer> tail = set.tailSet(7); // [7, 9, 11, 13, 15] — >= 7
NavigableSet<Integer> tailExc = set.tailSet(7, false); // [9, 11, 13, 15] — > 7
// ── Live views — modifications reflected in original ──────────────────
NavigableSet<Integer> view = set.subSet(5, true, 11, true);
view.add(6); // adds 6 to the ORIGINAL set
view.remove(7); // removes 7 from the ORIGINAL set
System.out.println(set); // [1, 3, 5, 6, 9, 11, 13, 15] — 6 added, 7 removed
// Attempting to add outside range throws IllegalArgumentException:
view.add(12); // throws IllegalArgumentException — 12 > 11 (upper bound)
view.add(4); // throws IllegalArgumentException — 4 < 5 (lower bound)
// ── Practical range queries ───────────────────────────────────────────
// Tasks scheduled between two times:
TreeSet<Task> schedule = new TreeSet<>(
Comparator.comparing(Task::scheduledTime));
LocalDateTime from = LocalDateTime.now();
LocalDateTime to = from.plusHours(2);
// Live view of tasks in the next 2 hours:
SortedSet<Task> upcoming = schedule.subSet(
new Task(from), true,
new Task(to), false);
// Count tasks in range without materialising a copy:
System.out.println("Tasks in next 2h: " + upcoming.size());
// Descending view of range:
NavigableSet<Integer> descending = set.subSet(5, true, 11, true)
.descendingSet();
System.out.println(descending); // [11, 9, 6, 5]TreeSet Performance and When to Use It
// ── TreeSet vs HashSet: choosing based on need ───────────────────────
// Need membership testing only, no ordering:
Set<String> membership = new HashSet<>(); // O(1) — correct choice
// Need sorted iteration:
Set<String> sorted = new TreeSet<>(); // O(log n) — justified
// Need range queries:
TreeSet<Integer> range = new TreeSet<>(); // only TreeSet can do this efficiently
// ── TreeSet as leaderboard ────────────────────────────────────────────
record Score(String player, int points) {}
TreeSet<Score> leaderboard = new TreeSet<>(
Comparator.comparingInt(Score::points).reversed()
.thenComparing(Score::player));
leaderboard.add(new Score("Alice", 1500));
leaderboard.add(new Score("Bob", 2300));
leaderboard.add(new Score("Carol", 1500));
leaderboard.add(new Score("Dave", 3100));
// Top 3:
leaderboard.stream().limit(3).forEach(s ->
System.out.println(s.player() + ": " + s.points()));
// Dave: 3100, Bob: 2300, Alice: 1500
// Who is just below 2000 points?
Score justBelow = leaderboard.higher(new Score("", 2000));
// Uses comparator: find score just above 2000 in descending order
// ── ConcurrentSkipListSet — thread-safe TreeSet replacement ───────────
NavigableSet<Integer> concurrent = new ConcurrentSkipListSet<>();
concurrent.add(5);
concurrent.add(3);
concurrent.add(7);
// Thread-safe: O(log n) add/remove/contains
// Non-blocking reads: multiple threads can read simultaneously
System.out.println(concurrent.floor(6)); // 5 — thread-safe navigation
System.out.println(concurrent.ceiling(4)); // 5 — thread-safe navigation
// ── Performance summary ───────────────────────────────────────────────
// Operation HashSet LinkedHashSet TreeSet ConcurrentSkipListSet
// ──────────────────────────────────────────────────────────────────────
// add O(1)* O(1)* O(log n) O(log n)
// remove O(1)* O(1)* O(log n) O(log n)
// contains O(1)* O(1)* O(log n) O(log n)
// first/last O(n) O(n) O(log n) O(log n)
// floor/ceiling O(n) O(n) O(log n) O(log n)
// subSet O(n) O(n) O(log n) O(log n)
// iteration O(cap) O(size) O(size) O(size)
// thread-safe NO NO NO YES