Practical choices
Choosing between the standard collections: ArrayList or LinkedList, HashMap or TreeMap, HashSet or TreeSet, ArrayDeque or Stack, and a heap or an ordered map.
Learning goals
- Choose ArrayList over LinkedList in almost every case, and know the exceptions
- Pick a map or set by the operations it must support
- Replace the legacy Stack with ArrayDeque
- Decide between a PriorityQueue and a TreeMap
Prerequisites
ArrayList vs LinkedList
Default to ArrayList. It has O(1) indexed access, amortised O(1) append, 4 bytes per element reference and contiguous storage. LinkedList offers O(1) insertion at a node you already hold, but you rarely hold one; finding the position costs O(n) and each element costs a 24-byte node. In practice ArrayList wins even for many insertions in the middle, because shifting a contiguous array is very fast.
Reach for LinkedList only when you need null-friendly queue/deque methods and do not mind the cost, and even then ArrayDeque is usually better. Compare element access in the animation.
| Need | Choose |
|---|---|
| Random access, iteration, appending | ArrayList |
| Queue or deque | ArrayDeque |
| Insert/remove while iterating with an Iterator | LinkedList (rarely worth it) |
| Fixed number of primitives | int[] |
Reading element k: array vs linked list
Interactive · edit the inputarray memory (value, address)
The 7 values sit in consecutive memory cells, 4 bytes apart, starting at address 1000.
1static int getFromArray(int[] arr, int k) {2return arr[k]; // address = base + k * 4: one memory read3}45static int getFromList(Node head, int k) {6Node cur = head;7for (int i = 0; i < k; i++) {8cur = cur.next; // pointer chase: one read per hop9}10return cur.value;11}
Live state
k
5
base
1000
Execution trace (dry run table)Show 3 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | k=5 base=1000 | The 7 values sit in consecutive memory cells, 4 bytes apart, starting at address 1000. |
| 2 | L2 | k=5 address=1020 | Address = 1000 + 5 × 4 = 1020. One multiplication and one addition give the location, no matter how large k is. |
| 3 | L2 | return=8 | return 8 with a single memory read: O(1) random access. |
HashMap vs TreeMap
Use HashMap when you only look keys up: O(1) expected, smaller, faster. Use TreeMap when you need sorted iteration or order queries: firstKey, floorKey, ceilingKey, subMap, headMap. LinkedHashMap is the third choice: hash speed plus predictable insertion (or access) order.
The key requirements differ: HashMap needs hashCode and equals; TreeMap needs Comparable keys or a Comparator, and treats two keys as the same when the comparison returns 0.
| HashMap | TreeMap | |
|---|---|---|
| get / put / remove | O(1) expected | O(log n) |
| Iteration order | Unspecified | Sorted by key |
| floor / ceiling / range | Not available | O(log n) (+ k) |
| Memory per entry (measured, excluding boxed key/value) | 32 bytes + table slot | 40 bytes |
| Null key | Allowed | Not allowed |
1static void choose() {2Map<String, Integer> lookup = new HashMap<>(); // only exact lookups3TreeMap<Integer, String> schedule = new TreeMap<>(); // need "the next event at or after t"4schedule.put(900, "standup"); schedule.put(1400, "review");5System.out.println(schedule.ceilingEntry(1000).getValue()); // review6}
HashSet vs TreeSet
The same split applies to sets. HashSet for membership and duplicate removal; TreeSet when elements must come out sorted or you need first, last, floor, ceiling and range views; LinkedHashSet when insertion order must be preserved.
A TreeSet uses the comparator, not equals, to detect duplicates. With a comparator that compares only string length, "aa" and "bb" are the same element and the second add is ignored: the set printed [c, aa] after adding aa, bb and c.
1static void comparatorDecidesDuplicates() {2TreeSet<String> byLength = new TreeSet<>(Comparator.comparing(String::length));3byLength.add("aa"); byLength.add("bb"); byLength.add("c");4System.out.println(byLength); // [c, aa]: "bb" compared equal to "aa"5}
ArrayDeque vs Stack
java.util.Stack is a legacy class: it extends Vector, so every method is synchronised (needless locking), it exposes list operations such as get(i) and add(i, x) that break the stack abstraction, and its iteration order is bottom to top. Use ArrayDeque through the Deque interface instead: push, pop and peek work at the front, it is faster and smaller, and it does not pretend to be a list.
One visible difference: after pushing 1, 2, 3, Stack prints [1, 2, 3] (bottom to top) while ArrayDeque prints [3, 2, 1] (top first). Both peek() the value 3. Stack.pop() on an empty stack throws EmptyStackException; ArrayDeque.pop() throws NoSuchElementException.
1static void stacks() {2Deque<Integer> stack = new ArrayDeque<>();3stack.push(1); stack.push(2); stack.push(3);4System.out.println(stack + " " + stack.peek()); // [3, 2, 1] 35int top = stack.pop(); // 3; pop() on an empty deque throws NoSuchElementException6}
Heap vs ordered map
A PriorityQueue and a TreeMap both give you the smallest element quickly, but they are different tools. The heap does exactly one thing: peek the best element in O(1) and poll it in O(log n); it has the smallest overhead of the two (an array, 4 bytes per slot plus the element). The tree does everything the heap does and more (any element can be removed in O(log n), and floor, ceiling and ranges work), at the price of 40-byte entries and O(log n) for the minimum.
Rule of thumb: if you only ever take the minimum or maximum (top-k, scheduling, merging sorted lists, Dijkstra), use a PriorityQueue. If you also need to remove or update arbitrary elements, look up by key, or ask about neighbours and ranges, use a TreeMap/TreeSet.
| Operation | PriorityQueue | TreeMap / TreeSet |
|---|---|---|
| Peek minimum | O(1) | O(log n) |
| Remove minimum | O(log n) | O(log n) |
| Remove arbitrary element | O(n) | O(log n) |
| Insert | O(log n) | O(log n) |
| Floor / ceiling / range | Not supported | O(log n) (+ k) |
| Iterate in sorted order | Not supported (repeated poll) | O(n) |
PriorityQueue: offer, poll and peek on a binary heap
Interactive · edit the inputThe heap is empty.
int[] arr · length 0
polled so far (in order)
An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2.
1Comparator<Integer> cmp = Comparator.naturalOrder(); // Comparator.reverseOrder() makes a max-queue2PriorityQueue<Integer> pq = new PriorityQueue<>(cmp);3pq.offer(x); // append at the end, then sift up4Integer top = pq.poll(); // last element to the root, then sift down5Integer next = pq.peek(); // the root only; nothing is removed
Live state
size
0
order
min first
Execution trace (dry run table)Show 26 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | size=0 order=min first | An empty min-queue. It is a binary heap stored in an array: the children of index i are 2i+1 and 2i+2. |
| 2 | L3 | op=offer(5) size=1 | offer(5): append at the end (index 0), the only place that keeps the tree complete. |
| 3 | L3 | op=offer(3) size=2 | offer(3): append at the end (index 1), the only place that keeps the tree complete. |
| 4 | L3 | op=offer(3) child=3 parent=5 | The child has higher priority than its parent, so they swap. |
| 5 | L3 | op=offer(3) | Swapped: 3 moves up to index 0. |
| 6 | L3 | op=offer(8) size=3 | offer(8): append at the end (index 2), the only place that keeps the tree complete. |
| 7 | L3 | op=offer(8) child=8 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 8 | L3 | op=offer(1) size=4 | offer(1): append at the end (index 3), the only place that keeps the tree complete. |
| 9 | L3 | op=offer(1) child=1 parent=5 | The child has higher priority than its parent, so they swap. |
| 10 | L3 | op=offer(1) | Swapped: 1 moves up to index 1. |
| 11 | L3 | op=offer(1) child=1 parent=3 | The child has higher priority than its parent, so they swap. |
| 12 | L3 | op=offer(1) | Swapped: 1 moves up to index 0. |
| 13 | L4 | op=poll() returned=1 size=3 | poll() returns the root 1. The last element (5) moves to the root to fill the gap, then sifts down. |
| 14 | L4 | op=poll() node=5 better child=3 | The better child (3) beats the node, so they swap. |
| 15 | L4 | op=poll() | Swapped: 3 moves up to index 0. |
| 16 | L3 | op=offer(4) size=4 | offer(4): append at the end (index 3), the only place that keeps the tree complete. |
| 17 | L3 | op=offer(4) child=4 parent=5 | The child has higher priority than its parent, so they swap. |
| 18 | L3 | op=offer(4) | Swapped: 4 moves up to index 1. |
| 19 | L3 | op=offer(4) child=4 parent=3 | The parent keeps priority over the child (ties do not swap): sift up stops. |
| 20 | L4 | op=poll() returned=3 size=3 | poll() returns the root 3. The last element (5) moves to the root to fill the gap, then sifts down. |
| 21 | L4 | op=poll() node=5 better child=4 | The better child (4) beats the node, so they swap. |
| 22 | L4 | op=poll() | Swapped: 4 moves up to index 0. |
| 23 | L4 | op=poll() returned=4 size=2 | poll() returns the root 4. The last element (8) moves to the root to fill the gap, then sifts down. |
| 24 | L4 | op=poll() node=8 better child=5 | The better child (5) beats the node, so they swap. |
| 25 | L4 | op=poll() | Swapped: 5 moves up to index 0. |
| 26 | L5 | size=2 return=[5, 8] | Heap array: [5, 8]. Only the root is guaranteed to be the best value; the rest is just heap-ordered, not sorted. |
Practice ladder
Now try it yourself
Easy
- Why is ArrayList usually faster than LinkedList for inserting in the middle?
- Replace java.util.Stack with a Deque
Medium
- My Calendar I
- Kth Largest Element in a Stream
Advanced
- Design a Leaderboard with update and top-K
- Sliding Window Median
Key takeaways
- Default to ArrayList, HashMap, HashSet and ArrayDeque; switch when a requirement names order or ranges.
- TreeMap/TreeSet buy order and neighbour queries for O(log n) operations and bigger entries.
- Prefer a PriorityQueue when only the best element matters; a TreeMap when arbitrary removals or ranges matter.