Hello again. In the previous lesson, you used input constraints to set a performance budget: for , a quadratic design is usually ruled out before you code it. This lesson supplies the next link in that reasoning chain: once you know the operation budget, choose a Java structure whose expensive operations are not on your algorithm’s critical path.
For an interview solution, do not begin with “Which collection class do I remember?” Begin with: What information must I retrieve, update, or remove repeatedly—and must it be indexed, unique, ordered, or prioritized? By the end of this lesson, you should be able to justify choices such as HashMap, ArrayDeque, PriorityQueue, or ArrayList in one precise sentence.
Choose from operations, not names
A data structure is useful because it makes some operations cheap by making other operations more expensive or by using extra memory.
Consider three ways to answer “Have I already seen value ?” while scanning integers:
// List: contains may scan the whole list
List<Integer> seen = new ArrayList<>();
for (int x : nums) {
if (seen.contains(x)) {
// found a duplicate
}
seen.add(x);
}
ArrayList.contains(x) is . If it happens inside a loop over values, the total becomes . At , that conflicts with the performance target from the previous lesson.
The question is fundamentally membership, not positional storage. A set expresses that requirement directly:
Set<Integer> seen = new HashSet<>();
for (int x : nums) {
if (!seen.add(x)) {
// x was already present
}
}
For HashSet, add, remove, and membership testing are expected , giving expected total time. The structure did not merely make the code shorter; it removed the repeated linear search.
Use this four-part decision process before coding:
-
Name the repeated operation.
Examples: random access by index, membership test, lookup by key, remove the smallest item, or add and remove at an endpoint. -
Count how often it occurs.
An operation once may be fine. An operation in each of iterations usually is not. -
Identify semantic requirements.
Do you need duplicates? insertion order? sorted order? only the current minimum? This is as important as Big-O. -
Select the simplest Java structure that makes the repeated operation cheap.
Prefer standard interfaces such asList,Set,Map,Deque, andQueuein declarations; choose the concrete implementation for its costs.
This compact cheat sheet will be a useful visual reference during the course. Treat its complexity labels as first approximations; we will add the important qualifications below, especially “expected,” “amortized,” and “only at the ends.”

Watch the selected portions of “Java Collections Explained (with examples)” by Visual Computer Science for a fast survey of the trade-offs among the collections used most often in interview code.
Java Collections Explained (with examples)
Visual Computer Science’s “Java Collections Explained (with examples)” connects collection operations to their underlying structures. Watch it now to establish the broad map; the rest of this lesson sharpens the interview-level choices.
Watch ArrayList for dynamic arrays and the cost of shifting elements. Then watch priority and maps for heaps, hash maps, ordered maps, and sets. Focus on the question each structure answers efficiently, not just its class name.
The interview operation map
The following table is the core reference for this lesson. Complexities refer to the typical operations relevant to DSA interviews.
| Algorithm needs | Prefer in Java | Key operations | Typical cost | Important limitation |
|---|---|---|---|---|
| Fixed-size, indexed primitive data | int[], char[], etc. | read/write by index | Fixed length | |
| Dynamic sequence with indexed access | ArrayList<E> | get(i), set(i), append | , amortized | Insert/remove in middle is |
| Unique membership, no ordering needed | HashSet<E> | add, contains, remove | expected | No sorted or insertion-order guarantee |
| Key-to-value lookup or counting | HashMap<K, V> | get, put, containsKey, remove | expected | Iteration order is unspecified |
| FIFO queue, LIFO stack, both ends | ArrayDeque<E> | offer, poll, push, pop | amortized | No random access by index |
| Repeatedly retrieve min/max priority item | PriorityQueue<E> | offer, poll | Only the head is directly accessible | |
| Ordered keys plus predecessor/successor queries | TreeMap<K,V>, TreeSet<E> | lookup, insert, floor, ceiling | Slower than hashing when order is unnecessary | |
| General linked sequence | usually avoid LinkedList<E> | endpoint operations | Index access and indexed traversal are costly |
The high-value habit is to distinguish accessing a position from accessing an endpoint, and finding an arbitrary item from finding the globally smallest item.
For example, a PriorityQueue does not maintain a fully sorted array you can inspect at arbitrary indices. It guarantees that peek() returns the highest-priority element—by default, the smallest—and that poll() removes it efficiently. If all you repeatedly need is “the next smallest active item,” that is exactly the guarantee you want.
By contrast, if you must ask, “What is the largest key no greater than ?” a hash map cannot help: hashing intentionally discards ordering. A TreeMap maintains order and provides methods such as floorKey(x) and ceilingKey(x) in .
Arrays and lists: positional access versus edits
Use an array when the size is known or bounded and you need fast positional access. In interview code, primitive arrays are particularly valuable:
int[] frequency = new int[26];
frequency[ch - 'a']++;
This has access, uses compact primitive storage, and avoids hashing. It is ideal when keys come from a small known range: lowercase letters, digits, or values in a bounded interval.
Use ArrayList<E> when you need a dynamically growing sequence and positional access:
List<Integer> values = new ArrayList<>();
values.add(10); // amortized O(1)
int middle = values.get(0); // O(1)
“Amortized ” deserves a precise interpretation. Most appends write into unused space in the backing array. Occasionally the backing array fills, so Java allocates a larger one and copies existing elements, taking for that individual append. Across many appends, those rare copies average out to per append.
The costly list operations are insertions and removals away from the end:
values.add(0, 99); // O(n): shift elements right
values.remove(0); // O(n): shift elements left
That shift is not automatically a problem. If it happens once, use the clear, simple list operation. If it happens repeatedly in a large loop, redesign.
Read the selected portions of Dev.java’s “Choosing the Right Implementation Between ArrayList and LinkedList.” The article is useful because it separates theoretical operation costs from the practical cost of traversing linked nodes.
Choosing the Right Implementation Between ArrayList and LinkedList - Dev.java
Read Dev.java’s comparison to make the ArrayList versus LinkedList decision based on the operations your algorithm actually performs, rather than the oversimplification that “linked lists make insertion fast.”
In “Algorithm Complexity for Some Common List Operations,” read the comparison setup and table. Focus on the difference between indexed reads and insertion by index. Then go to “Iterating with an Index and an Iterator” under “Iterating Over the Elements of a List.” Read the index-iteration warning. The key interview lesson is that list.get(i) inside a loop is disastrous when list happens to be a LinkedList.
Why LinkedList is rarely the answer in Java interview code
A linked list can relink nodes in time only after you already have the relevant node or its predecessor. But Java’s LinkedList.get(i), add(i, x), and remove(i) first have to traverse nodes to reach index , which is .
This distinction matters:
- In a custom linked-list algorithm, if you hold a
ListNode prev, inserting afterprevis . - In
java.util.LinkedList, “insert at the middle index” is still , because locating that middle node costs . - Iterating with
for (int i = 0; i < list.size(); i++) list.get(i)is on aLinkedList.
For an ordinary resizable indexed sequence, default to ArrayList. For endpoint-only work, use ArrayDeque, not LinkedList.
Hashing: choose it for fast lookup, membership, and counts
A HashMap<K, V> stores a relationship from a key to a value. A HashSet<E> stores only membership. Internally, a set is conceptually similar to a map whose values are irrelevant.
Use a hash map when your scan repeatedly needs a fact associated with a prior value:
- value frequency
- value first index
- prefix sum number of occurrences
- identifier record or state
For frequency counting:
Map<Integer, Integer> frequency = new HashMap<>();
for (int x : nums) {
frequency.put(x, frequency.getOrDefault(x, 0) + 1);
}
Each getOrDefault and put is expected , so the complete pass is expected .
For the classic “find two values adding to target” pattern, the map stores information that lets the current value query the past in constant expected time:
Map<Integer, Integer> indexByValue = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int needed = target - nums[i];
if (indexByValue.containsKey(needed)) {
return new int[] {indexByValue.get(needed), i};
}
indexByValue.put(nums[i], i);
}
A concise interview justification is:
“For each number, I need to test whether its complement occurred earlier. A hash map gives expected constant-time lookup and stores the earlier value’s index, so the full scan is expected linear time.”
That statement identifies the operation, the required information, and the total complexity.
“Expected” matters. Hash tables rely on keys dispersing well across buckets; pathological collisions can hurt performance. For ordinary Integer, String, and well-designed custom keys, HashMap and HashSet are the standard interview choice. Also remember these common traps:
containsKey(key)is expected ;containsValue(value)is .- Iterating a
HashMapdoes not promise sorted or insertion order. - A mutable object should not be changed in a way that changes its equality or hash code while it is a map key or set element.
- If you need only “seen or not seen,” use
HashSet, not a map with dummy values.
The Java documentation’s note on capacity is a useful qualification: although lookups are expected constant time, iteration depends on both stored entries and the table’s capacity.
Read the opening of the official HashMap documentation to connect expected constant-time lookup with the implementation assumptions behind it.
At the start of the class description, read through the paragraph ending with the iteration-cost qualification. Then continue through the discussion beginning capacity, load factor, and resizing. For interview work, retain two ideas: get and put are expected O(1), and an excessively large initial capacity is not automatically better.
Endpoint processing: use ArrayDeque for stacks and queues
A stack restricts work to one end: last in, first out. A queue restricts it to opposite ends: first in, first out. If that is your algorithm’s access pattern, model it directly with Deque<E> and implement it with ArrayDeque<E>.
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10); // add at top
int top = stack.peek();
int removed = stack.pop();
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(10); // add at back
int front = queue.peek();
int removed = queue.poll();
These endpoint operations are amortized . ArrayDeque is a strong default for interview stacks and queues because it is array-backed and avoids the per-node overhead and pointer traversal of LinkedList.
Use the method names consistently:
| Role | Add | Inspect without removing | Remove |
|---|---|---|---|
| Stack | push(x) | peek() | pop() |
| Queue | offer(x) | peek() | poll() |
Avoid the legacy Stack class in new interview code. Also note that ArrayDeque does not permit null, which is usually helpful because poll() returning null clearly means the deque was empty.
This choice will recur constantly: breadth-first search uses a queue; matching brackets and next-greater-element problems use a stack; sliding-window maxima later use a deque from both ends.
Priority and ordering: do not pay for more order than you need
A PriorityQueue<E> is a heap. It is the right tool when your algorithm repeatedly needs the single best next candidate according to a priority rule.
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(7);
minHeap.offer(2);
minHeap.offer(5);
int smallest = minHeap.poll(); // 2
Its core costs are:
peek():offer(x):poll():
To make a max-heap of integers:
PriorityQueue<Integer> maxHeap =
new PriorityQueue<>(Comparator.reverseOrder());
The decision rule is simple:
- Need every insertion followed by access to the minimum or maximum? Use
PriorityQueue. - Need a structure fully sorted for iteration, or need nearest ordered keys such as floor and ceiling? Use
TreeMaporTreeSet. - Need only exact key lookup or membership, not order? Use
HashMaporHashSet.
For example, imagine processing jobs by earliest finish time. Sorting all jobs once may be best if all jobs are known upfront. But if jobs arrive incrementally and you repeatedly need the next earliest job, a heap matches the operational need.
Do not use PriorityQueue merely because a problem mentions “largest” or “smallest.” If you need the maximum of a static array once, a linear scan is and simpler than building a heap. A data structure pays off when its inexpensive operation is repeated.
Turn a problem statement into a data-structure sentence
Before you implement, force yourself to complete this template:
“During each iteration, I need to ___; I need ___ ordering or uniqueness; therefore I will use ___, whose ___ operation costs ___; the total is ___.”
Here are examples worth memorizing as reasoning patterns—not as fixed solutions.
| Problem shape | Required repeated operation | Appropriate choice |
|---|---|---|
| Detect duplicates while scanning | Test membership of current value | HashSet |
| Count occurrences | Retrieve and update count by value | HashMap |
| Maintain visited nodes in a traversal | Test and mark visited | HashSet plus ArrayDeque for BFS |
| Process tasks in arrival order | Add at back, remove at front | ArrayDeque |
| Repeatedly select least costly available task | Add candidates and remove minimum | PriorityQueue |
| Store a growing answer and later access by index | Append and indexed reads | ArrayList |
| Find nearest stored key below or above a value | Ordered predecessor/successor query | TreeMap or TreeSet |
| Count lowercase English letters | Access a small bounded key range | int[26] |
One subtle but important comparison: an int[26] is better than a HashMap<Character, Integer> for lowercase-letter frequency counting because the key domain is fixed and tiny. A map becomes necessary when the keys are sparse, large, unknown in advance, or not naturally convertible to compact indices.
For the next few array and string problems you solve, write the one-sentence justification in a comment before the data-structure declaration. This makes pattern recognition deliberate rather than automatic:
// Need expected O(1) lookup of prior values to avoid pair enumeration.
Map<Integer, Integer> indexByValue = new HashMap<>();
Key takeaways
Data-structure selection is an operation-cost decision:
- Use arrays and
ArrayListfor indexed access; choose primitive arrays when the size or value range is known. - Use
HashSetfor expected membership andHashMapfor expected key-to-value lookup or counting. - Use
ArrayDequefor stacks, queues, and work at both endpoints. - Use
PriorityQueuewhen repeatedly extracting the current minimum or maximum is the central operation. - Use
TreeMaporTreeSetonly when key order, predecessor, successor, or ordered traversal is genuinely required. - Default to
ArrayListoverLinkedListfor general list work. In Java, a linked list does not make indexed insertion cheap because reaching the target index is itself linear.
Next, you will examine a different hidden cost: recursion. You will estimate recursion depth and auxiliary call-stack usage, then decide when an iterative Java implementation is safer.
Can't find a good explanation? Sign up and we'll make it for you
Sign up