Ordered maps and sets
Java's ordered collections: TreeMap, TreeSet, the NavigableMap and NavigableSet interfaces, sub-map views and how comparators define equality.
Learning goals
- Choose between HashMap and TreeMap and state their costs
- Use floor, ceiling, lower, higher, first, last and poll methods
- Take head, tail and sub views of a map or set
- Write a comparator that is consistent, overflow-safe and defines the right notion of equality
Prerequisites
You rarely write a BST by hand in Java: TreeMap and TreeSet already provide a balanced one with the whole ordered-query vocabulary of this chapter. Knowing the API, and its comparator rules, is what you use in practice.
TreeMap
TreeMap<K, V> is a map whose keys are kept in sorted order. In the JDK it is a red-black tree, a self-balancing BST (the topic of the next chapter), so get, put and remove cost O(log n) even for sorted input. Iteration visits keys in increasing order. A HashMap is O(1) on average but has no order and no neighbour queries.
Use TreeMap when you need sorted iteration, nearest-key lookups, or range views. Keys must be comparable (or a Comparator must be given), and a null key throws NullPointerException with natural ordering.
| HashMap | TreeMap | |
|---|---|---|
| get / put / remove | O(1) average | O(log n) worst case |
| Iteration order | unspecified | sorted by key |
| floor / ceiling / range views | not available | O(log n) per query |
| null key | allowed (one) | NullPointerException (natural order) |
1static void treeMapBasics() {2TreeMap<Integer, String> m = new TreeMap<>();3m.put(50, "fifty"); m.put(30, "thirty"); m.put(70, "seventy"); m.put(20, "twenty");4System.out.println(m); // {20=twenty, 30=thirty, 50=fifty, 70=seventy}: sorted by key5System.out.println(m.firstKey() + " " + m.lastKey()); // 20 706for (Map.Entry<Integer, String> e : m.entrySet()) System.out.print(e.getKey() + " "); // 20 30 50 707}
TreeSet
TreeSet<E> is a sorted set backed by a TreeMap: no duplicates (by the comparison), sorted iteration, O(log n) add, remove and contains. It is the structure to reach for with 'keep the elements ordered while inserting and removing', such as a sweep line's active set or a leaderboard.
first() and last() return the extremes, pollFirst() and pollLast() remove them, and the neighbour methods below find the closest elements to a value. Rule of thumb: a PriorityQueue if you only ever need the extreme; a TreeSet if you also need to delete arbitrary elements or find neighbours.
1static void treeSetBasics() {2TreeSet<Integer> s = new TreeSet<>(List.of(40, 10, 30, 20, 30));3System.out.println(s); // [10, 20, 30, 40]: sorted, the duplicate 30 is dropped4System.out.println(s.first() + " " + s.last()); // 10 405System.out.println(s.pollFirst() + " " + s); // 10 [20, 30, 40]: removes and returns the smallest6}
Submaps
Range queries come as **views** of the original collection, not copies, in O(log n) to create. headMap(to) holds keys below to (exclusive by default), tailMap(from) keys from from upward (inclusive by default), and subMap(from, to) the **half-open** range [from, to). Overloads take explicit inclusive flags. TreeSet has headSet, tailSet and subSet with the same conventions.
Views are **live**: writes to the view change the original, and the reverse. A view only accepts keys inside its range: put of a key outside throws IllegalArgumentException. Counting the elements in a view with size() costs O(k), not O(log n), because it iterates.
1static void views() {2TreeMap<Integer, String> m = new TreeMap<>();3for (int k = 10; k <= 50; k += 10) m.put(k, "v" + k); // 10 20 30 40 504System.out.println(m.subMap(20, 40).keySet()); // [20, 30]: [20, 40) is half-open5System.out.println(m.subMap(20, true, 40, true).keySet()); // [20, 30, 40]6System.out.println(m.headMap(30).keySet() + " " + m.tailMap(30).keySet()); // [10, 20] [30, 40, 50]7m.subMap(20, 40).clear(); // removes 20 and 30 from the original map8System.out.println(m.keySet()); // [10, 40, 50]9try { m.subMap(20, 40).put(99, "x"); } catch (IllegalArgumentException e) { System.out.println("key out of range"); }10}
Comparator semantics
A TreeMap or TreeSet decides everything by the comparator: two keys are **equal if compare returns 0**, whatever equals says. A TreeSet<String> with String.CASE_INSENSITIVE_ORDER treats "a" and "A" as the same element and keeps only the first; a comparator that orders strings by length would reject every second string of the same length.
A comparator must be consistent: antisymmetric, transitive and stable over time, otherwise the tree misbehaves silently. Avoid the subtraction trick (a, b) -> a - b for ints: it overflows, so compare(Integer.MAX_VALUE, -1) returns a negative number. Use Integer.compare or Comparator.comparingInt. For several fields, chain them with thenComparing, and make sure the comparator distinguishes elements that you want to keep apart.
1static void comparatorPitfalls() {2TreeSet<String> ci = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);3ci.add("a"); ci.add("A"); ci.add("b");4System.out.println(ci); // [a, b]: "A" compared equal to "a" and was dropped56TreeSet<String> byLength = new TreeSet<>(Comparator.comparingInt(String::length));7byLength.add("aa"); byLength.add("bb"); byLength.add("c");8System.out.println(byLength); // [c, aa]: "bb" has the same length as "aa", so it is "equal"910Comparator<Integer> broken = (a, b) -> a - b; // overflows near the limits11System.out.println(broken.compare(Integer.MAX_VALUE, -1) < 0); // true: MAX_VALUE sorts BEFORE -1 (wrong)12System.out.println(Integer.compare(Integer.MAX_VALUE, -1) > 0); // true: the correct answer13}
Practice ladder
Now try it yourself
Easy
- Design HashSet with a TreeSet (concept)
- Find Closest Number to Zero
Medium
- My Calendar I
- Contains Duplicate III
- Online Stock Span with TreeMap
Advanced
- Sliding Window Median
- Data Stream as Disjoint Intervals
Key takeaways
- TreeMap and TreeSet are balanced BSTs: O(log n) operations, sorted iteration, neighbour and range queries.
- floor, ceiling, lower and higher return null when nothing qualifies.
- headMap, tailMap and subMap are live views; subMap(from, to) is half-open.
- The comparator defines equality: compare == 0 means the same key.
- Never write a comparator as a - b for ints; use Integer.compare.