Lesson 17 of 71
Sorted Maps, Sets, and Priority Queues
A HashMap finds a key in O(1) but keeps no order. A sorted collection keeps its elements ordered by a comparison, which costs O(log n) per operation and adds range queries: "the largest key at or below x", "all keys between a and b". This lesson covers TreeMap and TreeSet, the NavigableMap queries, the rule that a comparator must agree with equals, PriorityQueue, and SequencedMap. Terms such as bucket and load factor come from HashMap Internals.
| Class | Structure | Ordered by |
|---|---|---|
TreeMap, TreeSet |
red-black tree | key comparison, full order on iteration |
PriorityQueue |
binary heap in an array | comparison, only the head is ordered |
LinkedHashMap |
hash table plus linked list | insertion or access order |
ConcurrentSkipListMap |
skip list | key comparison, thread safe |
Example 1: TreeMap keeps keys sorted
TreeMap is a red-black tree: a binary search tree that colors nodes red or black and rebalances on insert and delete so the height stays at most about 2 log2(n). get, put, and remove are O(log n). Iteration visits keys in ascending order. Keys are compared with compareTo (natural ordering) or with a Comparator passed to the constructor.
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
public class TreeMapBasics {
public static void main(String[] args) {
Map<String, Integer> natural = new TreeMap<>();
for (String s : new String[]{"pear", "apple", "fig", "banana"}) natural.put(s, s.length());
System.out.println("natural: " + natural);
Map<String, Integer> byLength = new TreeMap<>(
Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
byLength.putAll(natural);
System.out.println("byLength: " + byLength);
TreeMap<String, Integer> reversed = new TreeMap<>(Comparator.reverseOrder());
reversed.putAll(natural);
System.out.println("reversed: " + reversed);
System.out.println("first and last: " + reversed.firstKey() + " " + reversed.lastKey());
}
}natural: {apple=5, banana=6, fig=3, pear=4}
byLength: {fig=3, pear=4, apple=5, banana=6}
reversed: {pear=4, fig=3, banana=6, apple=5}
first and last: pear appleTreeMap rejects a null key with natural ordering, because compareTo cannot be called on null. A TreeMap created without a comparator also requires keys to implement Comparable, and the failure shows up as a ClassCastException on the first put.
Example 2: NavigableMap queries
NavigableMap (implemented by TreeMap) adds nearest-key lookups and views over key ranges. All of them run in O(log n).
| Method | Returns |
|---|---|
floorKey(k) |
greatest key <= k, or null |
lowerKey(k) |
greatest key < k |
ceilingEntry(k) |
entry with least key >= k |
higherKey(k) |
least key > k |
headMap(k) |
keys < k (headMap(k, true) includes k) |
tailMap(k) |
keys >= k |
subMap(a, b) |
keys in [a, b) |
descendingMap() |
the same map in reverse order |
import java.util.NavigableMap;
import java.util.TreeMap;
public class Navigable {
public static void main(String[] args) {
NavigableMap<Integer, String> m = new TreeMap<>();
for (int k : new int[]{10, 20, 30, 40, 50}) m.put(k, "v" + k);
System.out.println("floorKey(25) = " + m.floorKey(25));
System.out.println("lowerKey(20) = " + m.lowerKey(20));
System.out.println("ceilingEntry(25)= " + m.ceilingEntry(25));
System.out.println("higherKey(50) = " + m.higherKey(50));
System.out.println("headMap(30) = " + m.headMap(30));
System.out.println("headMap(30,true)= " + m.headMap(30, true));
System.out.println("tailMap(40) = " + m.tailMap(40));
System.out.println("subMap(20,40) = " + m.subMap(20, 40));
System.out.println("descendingMap = " + m.descendingMap());
NavigableMap<Integer, String> view = m.subMap(20, true, 40, true);
m.put(35, "v35"); // views are live
view.remove(20); // and write through
System.out.println("view after edits= " + view);
System.out.println("map after edits = " + m);
}
}floorKey(25) = 20
lowerKey(20) = 10
ceilingEntry(25)= 30=v30
higherKey(50) = null
headMap(30) = {10=v10, 20=v20}
headMap(30,true)= {10=v10, 20=v20, 30=v30}
tailMap(40) = {40=v40, 50=v50}
subMap(20,40) = {20=v20, 30=v30}
descendingMap = {50=v50, 40=v40, 30=v30, 20=v20, 10=v10}
view after edits= {30=v30, 35=v35, 40=v40}
map after edits = {10=v10, 30=v30, 35=v35, 40=v40, 50=v50}A range view is backed by the original tree. Putting a key outside the view's range through the view throws IllegalArgumentException.
Example 3: TreeSet and a comparator that disagrees with equals
TreeSet wraps a TreeMap, the same way HashSet wraps a HashMap. A sorted collection decides that two elements are the same when compare returns 0; it never calls equals. A comparator is consistent with equals when compare(a, b) == 0 exactly when a.equals(b). If it is not, the set silently drops elements that are not equal.
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class Consistency {
record Order(String id, long price) {}
public static void main(String[] args) {
List<Order> orders = List.of(new Order("A", 100), new Order("B", 100), new Order("C", 99));
Set<Order> hash = new HashSet<>(orders);
Set<Order> byPriceOnly = new TreeSet<>(Comparator.comparingLong(Order::price));
byPriceOnly.addAll(orders);
Set<Order> byPriceThenId = new TreeSet<>(
Comparator.comparingLong(Order::price).thenComparing(Order::id));
byPriceThenId.addAll(orders);
System.out.println("HashSet size: " + hash.size());
System.out.println("TreeSet price only: " + byPriceOnly.size() + " " + byPriceOnly);
System.out.println("TreeSet price then id: " + byPriceThenId.size());
System.out.println("contains B (price only): "
+ byPriceOnly.contains(new Order("B", 100)));
System.out.println("BigDecimal 1.0 vs 1.00 in TreeSet: "
+ new TreeSet<>(List.of(new java.math.BigDecimal("1.0"),
new java.math.BigDecimal("1.00"))).size()
+ ", in HashSet: "
+ new HashSet<>(List.of(new java.math.BigDecimal("1.0"),
new java.math.BigDecimal("1.00"))).size());
}
}HashSet size: 3
TreeSet price only: 2 [Order[id=C, price=99], Order[id=A, price=100]]
TreeSet price then id: 3
contains B (price only): true
BigDecimal 1.0 vs 1.00 in TreeSet: 1, in HashSet: 2Order B was dropped because its price compares equal to order A, and contains(B) answers true because it finds A. The fix is a tie-breaker on a unique field. BigDecimal is a JDK class whose natural ordering is not consistent with equals: 1.0 and 1.00 compare as 0 but are not equals, so the two sets disagree on size.
Example 4: PriorityQueue as a binary heap
PriorityQueue stores a binary heap in an array: the element at index i has children at 2i + 1 and 2i + 2, and every parent is less than or equal to its children. The smallest element is at index 0. offer places the element at the end and sifts up (swaps with its parent while smaller). poll moves the last element to the root and sifts down (swaps with the smaller child while larger). Both are O(log n); peek is O(1).
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;
public class HeapOrder {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int x : new int[]{5, 3, 8, 1, 9, 2}) pq.offer(x);
System.out.println("iterator order (array): " + pq);
System.out.println("toArray: " + Arrays.toString(pq.toArray()));
System.out.println("peek: " + pq.peek());
List<Integer> polled = new ArrayList<>();
while (!pq.isEmpty()) polled.add(pq.poll());
System.out.println("poll order: " + polled);
}
}iterator order (array): [1, 3, 2, 5, 9, 8]
toArray: [1, 3, 2, 5, 9, 8]
peek: 1
poll order: [1, 2, 3, 5, 8, 9]The iterator, toString, and toArray walk the array, which satisfies the heap property but is not sorted. Only repeated poll yields sorted order. remove(Object) and contains are O(n) because they scan the array.
Example 5: a max-heap and top-k
A Comparator turns the min-heap into a max-heap. A common pattern keeps the k largest values of a stream in a min-heap of size k: when the heap is full and a new value beats the smallest kept value, the smallest is replaced. That costs O(n log k) instead of sorting all n values.
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
public class TopK {
static List<Integer> topK(int[] values, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);
for (int v : values) {
if (minHeap.size() < k) minHeap.offer(v);
else if (v > minHeap.peek()) { minHeap.poll(); minHeap.offer(v); }
}
List<Integer> result = new ArrayList<>(minHeap);
result.sort(Comparator.reverseOrder());
return result;
}
public static void main(String[] args) {
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
for (int x : new int[]{5, 3, 8, 1}) maxHeap.offer(x);
System.out.println("max-heap head: " + maxHeap.peek());
int[] latenciesMicros = {120, 45, 980, 33, 410, 77, 650, 12, 300};
System.out.println("top 3: " + topK(latenciesMicros, 3));
}
}max-heap head: 8
top 3: [980, 650, 410]Example 6: SequencedMap (Java 21)
Java 21 added SequencedMap: a map with a defined encounter order and methods firstEntry, lastEntry, pollFirstEntry, pollLastEntry, putFirst, putLast, and reversed. SortedMap (and so TreeMap) and LinkedHashMap implement it. A sorted map decides positions by comparison, so putFirst and putLast throw UnsupportedOperationException on TreeMap.
import java.util.LinkedHashMap;
import java.util.SequencedMap;
import java.util.TreeMap;
public class Sequenced {
public static void main(String[] args) {
SequencedMap<String, Integer> linked = new LinkedHashMap<>();
linked.put("b", 2);
linked.put("c", 3);
linked.putFirst("a", 1);
System.out.println("linked: " + linked);
System.out.println("reversed view: " + linked.reversed());
System.out.println("pollLastEntry: " + linked.pollLastEntry() + " -> " + linked);
SequencedMap<String, Integer> tree = new TreeMap<>(linked);
System.out.println("tree firstEntry: " + tree.firstEntry());
try {
tree.putFirst("z", 26);
} catch (UnsupportedOperationException e) {
System.out.println("TreeMap.putFirst: UnsupportedOperationException");
}
}
}linked: {a=1, b=2, c=3}
reversed view: {c=3, b=2, a=1}
pollLastEntry: c=3 -> {a=1, b=2}
tree firstEntry: a=1
TreeMap.putFirst: UnsupportedOperationExceptionTreeMap, TreeSet, and PriorityQueue are not thread safe. ConcurrentSkipListMap and ConcurrentSkipListSet are the sorted concurrent versions; they are covered in Concurrent Collections.
Example 7: an order book side with TreeMap
A price level book keeps, for each price, the total quantity resting there. Bids are sorted highest first, asks lowest first, so firstEntry is the best price on each side. floorKey and ceilingKey answer "the next level a marketable order reaches".
import java.util.Comparator;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
public class PriceLevels {
private final NavigableMap<Long, Long> bids = new TreeMap<>(Comparator.reverseOrder());
private final NavigableMap<Long, Long> asks = new TreeMap<>();
void add(boolean buy, long price, long qty) {
(buy ? bids : asks).merge(price, qty, Long::sum);
}
void reduce(boolean buy, long price, long qty) {
NavigableMap<Long, Long> side = buy ? bids : asks;
side.computeIfPresent(price, (p, q) -> q - qty > 0 ? q - qty : null); // null removes
}
long askQuantityUpTo(long limit) {
long total = 0;
for (long q : asks.headMap(limit, true).values()) total += q;
return total;
}
public static void main(String[] args) {
PriceLevels book = new PriceLevels();
book.add(true, 9_990, 300);
book.add(true, 9_995, 100);
book.add(true, 9_990, 200);
book.add(false, 10_005, 150);
book.add(false, 10_010, 400);
book.add(false, 10_020, 50);
Map.Entry<Long, Long> bestBid = book.bids.firstEntry();
Map.Entry<Long, Long> bestAsk = book.asks.firstEntry();
System.out.println("bids " + book.bids + " asks " + book.asks);
System.out.println("best bid " + bestBid + " best ask " + bestAsk
+ " spread " + (bestAsk.getKey() - bestBid.getKey()));
System.out.println("ask qty up to 10010: " + book.askQuantityUpTo(10_010));
book.reduce(false, 10_005, 150); // level fully filled
System.out.println("best ask after fill: " + book.asks.firstEntry());
System.out.println("first bid level <= 9993: " + book.bids.ceilingKey(9_993L));
}
}bids {9995=100, 9990=500} asks {10005=150, 10010=400, 10020=50}
best bid 9995=100 best ask 10005=150 spread 10
ask qty up to 10010: 550
best ask after fill: 10010=400
first bid level <= 9993: 9990With the reversed comparator, "next" means lower price, so ceilingKey(9993) on the bid side returns 9990, the highest bid at or below 9993. Each update allocates tree nodes and boxed Long keys; Primitive Collections and Memory Cost measures that cost.
Complexity of common collections
| Collection | get or contains | add or put | remove | first or min | Iteration order |
|---|---|---|---|---|---|
ArrayList |
O(1) by index, O(n) by value | O(1) amortized at end | O(n) | O(1) | insertion |
ArrayDeque |
O(n) by value | O(1) amortized at ends | O(1) at ends | O(1) | insertion |
LinkedList |
O(n) | O(1) at ends | O(1) at ends | O(1) | insertion |
HashMap, HashSet |
O(1) average, O(log n) worst in tree bins | O(1) average | O(1) average | not supported | unspecified |
LinkedHashMap |
O(1) average | O(1) average | O(1) average | O(1) | insertion or access |
TreeMap, TreeSet |
O(log n) | O(log n) | O(log n) | O(log n) | sorted |
PriorityQueue |
O(n) | O(log n) | O(log n) poll, O(n) remove(o) |
O(1) peek |
heap array, not sorted |
ConcurrentSkipListMap |
O(log n) expected | O(log n) expected | O(log n) expected | O(1) | sorted |
Interview points
TreeMapis a red-black tree; operations are O(log n), iteration is sorted.floorKey,ceilingKey,lowerKey,higherKeyanswer nearest-key questions in O(log n);headMap,tailMap,subMapreturn live views.- Sorted collections use
compare, notequals. An inconsistent comparator drops distinct elements; add a tie-breaker on a unique field. BigDecimal("1.0")andBigDecimal("1.00")are equal to aTreeSetand different to aHashSet.PriorityQueueis a binary heap in an array;offerandpollare O(log n),peekO(1),remove(Object)O(n). Its iterator is not in priority order.- Top-k of n values: a min-heap of size k, O(n log k).
SequencedMap(Java 21) addsfirstEntry,pollLastEntry,reversed;TreeMap.putFirstthrowsUnsupportedOperationException.- None of these are thread safe;
ConcurrentSkipListMapis the sorted concurrent map.
Summary
- Sorted maps and sets trade O(1) hashing for O(log n) operations plus order and range queries.
TreeSetwrapsTreeMap; equality in both is defined by the comparator.- A heap orders only its head; sorted output needs repeated
poll. SortedMapandLinkedHashMapimplementSequencedMapsince Java 21.
