Choosing the Right Data Structure
Why the Right Data Structure Matters
Writing correct code is table stakes. Writing code that scales is the actual engineering skill.
Two solutions to the same problem can be completely correct and produce identical output. One runs in milliseconds. The other times out. The difference is almost always the data structure.
A hash map where you used an array. A heap where you used sorting. A deque where you used an array with shifting. These are not minor implementation details ā they are the decisions that determine whether your solution is usable at scale.
The right data structure does not just speed up code. It often makes the code simpler. When you choose the structure that fits the problem's natural shape, the algorithm follows cleanly. When you force the wrong structure, you fight the problem the entire way through.
This topic gives you a repeatable decision process: read the problem, identify what operations matter, match those operations to the structure that makes them fast.
The Decision Framework
Before choosing a data structure, answer four questions about the problem:
What operations are needed? Access by index, search by value, insert at a position, delete by value, track order, find minimum or maximum ā the answer determines everything.
How often does each operation happen? An operation that happens once can be O(n). An operation that happens n times must be O(1) or O(log n) or the total becomes O(n²).
Does order matter? Insertion order, sorted order, priority order ā each leads to a different structure.
What are the constraints? Input size, duplicate values, negative numbers ā these rule out certain structures and confirm others.
Once you know the answers, the structure choice is usually obvious. The table below is your starting reference.
The Quick Reference Table
| Need | Best Structure | Why |
|---|---|---|
| Access by index | Array | O(1) direct address computation |
| Search by value (unsorted) | Hash Map or Hash Set | O(1) average lookup |
| Search by value (sorted) | Sorted Array + Binary Search | O(log n) search |
| Insert and delete at both ends | Deque | O(1) at both ends |
| Insert and delete anywhere | Linked List | O(1) with pointer, O(n) to find |
| Last-in first-out | Stack | O(1) push and pop |
| First-in first-out | Queue | O(1) enqueue and dequeue |
| Always access minimum | Min-Heap | O(1) peek min, O(log n) insert/delete |
| Always access maximum | Max-Heap | O(1) peek max, O(log n) insert/delete |
| Sorted insertion and lookup | BST or Sorted Set | O(log n) all operations |
| Count frequencies | Hash Map | O(1) per update and lookup |
| Track unique elements | Hash Set | O(1) per insert and check |
| Hierarchical data | Tree | Natural recursive structure |
| Connections and paths | Graph | Models arbitrary relationships |
| Prefix-based search | Trie | O(length) per word operation |
Array: When Position Is Everything
Use an array when you know the index of what you want. Arrays give O(1) access by index ā nothing else does.
Arrays are also the right choice when you need to iterate over all elements in order, when the data has a natural sequence, or when you need cache-friendly traversal (arrays store elements in contiguous memory, which is faster to traverse than linked structures).
Arrays become the wrong choice when you frequently insert or delete from the middle, or when you need to search by value rather than by position.
1public class ArrayUsage {
2
3 // Array is right here: access by index is the core operation
4 // Finding the second largest element ā O(n) single pass
5 public static int secondLargest(int[] arr) {
6 int first = Integer.MIN_VALUE;
7 int second = Integer.MIN_VALUE;
8
9 for (int num : arr) {
10 if (num > first) {
11 second = first; // Previous first drops to second
12 first = num;
13 } else if (num > second && num != first) {
14 second = num;
15 }
16 }
17
18 return second;
19 }
20
21 public static void main(String[] args) {
22 int[] arr = {3, 1, 7, 5, 2, 9, 4};
23
24 System.out.println("Array: " + java.util.Arrays.toString(arr));
25 System.out.println("Second largest: " + secondLargest(arr));
26
27 // Index access ā O(1)
28 System.out.println("Element at index 3: " + arr[3]);
29 }
30}Output:
Array: [3, 1, 7, 5, 2, 9, 4]
Second largest: 7
Element at index 3: 5
Hash Map: When You Need Fast Lookup by Value
Use a hash map when you need to find, count, or group elements by a key ā not by position. Hash maps give O(1) average for insert, lookup, and delete regardless of size.
Any time your brute force has an inner loop searching for a value, a hash map eliminates it. Any time you need to count frequencies, a hash map is the natural fit.
1import java.util.HashMap;
2import java.util.Map;
3
4public class HashMapUsage {
5
6 // Hash map is right here: need to count frequencies and find by value
7 // Find the first character that appears exactly once
8 public static char firstUniqueChar(String s) {
9 Map<Character, Integer> freq = new HashMap<>();
10
11 // Count frequency of each character ā O(n)
12 for (char c : s.toCharArray()) {
13 freq.put(c, freq.getOrDefault(c, 0) + 1);
14 }
15
16 // Find first character with frequency 1 ā O(n)
17 for (char c : s.toCharArray()) {
18 if (freq.get(c) == 1) return c;
19 }
20
21 return '\0'; // No unique character found
22 }
23
24 public static void main(String[] args) {
25 String s1 = "leetcode";
26 String s2 = "aabbc";
27
28 System.out.println("First unique in '" + s1 + "': " + firstUniqueChar(s1));
29 System.out.println("First unique in '" + s2 + "': " + firstUniqueChar(s2));
30 }
31}Output:
First unique in 'leetcode': l
First unique in 'aabbc': c
Why Not an Array Here
A brute force approach would scan the string for every character to count occurrences ā O(n²). A hash map reduces frequency counting and lookup both to O(1) per character, giving O(n) total. The lookup is by character value, not by index ā that is the signal to use a hash map.
Stack: When Last In Must Come Out First
Use a stack when the most recently added element is always the next one needed. Stacks model any problem with nested structure: brackets that must close in reverse order of opening, function call chains, undo history, DFS traversal.
The core insight: when you encounter an element that must be "matched" against the most recent unmatched element, that is a stack.
1import java.util.Stack;
2
3public class StackUsage {
4
5 // Stack is right here: closing bracket must match the most recent opening bracket
6 // Valid parentheses ā check if brackets are correctly nested and closed
7 public static boolean isValid(String s) {
8 Stack<Character> stack = new Stack<>();
9
10 for (char c : s.toCharArray()) {
11 // Push opening brackets onto the stack
12 if (c == '(' || c == '[' || c == '{') {
13 stack.push(c);
14 } else {
15 // Closing bracket ā must match the most recent opening bracket
16 if (stack.isEmpty()) return false;
17
18 char top = stack.pop();
19
20 if (c == ')' && top != '(') return false;
21 if (c == ']' && top != '[') return false;
22 if (c == '}' && top != '{') return false;
23 }
24 }
25
26 return stack.isEmpty(); // All brackets were matched
27 }
28
29 public static void main(String[] args) {
30 System.out.println("'()[]{}' valid: " + isValid("()[]{}"));
31 System.out.println("'([)]' valid: " + isValid("([)]"));
32 System.out.println("'{[]}' valid: " + isValid("{[]}"));
33 }
34}Output:
'()[]{}' valid: true
'([)]' valid: false
'{[]}' valid: true
Dry Run: Stack on "{[]}"
Input: { [ ] }
c='{': opening ā push '{' ā stack=[{]
c='[': opening ā push '[' ā stack=[{, []
c=']': closing ā top='[', matches ']' ā pop ā stack=[{]
c='}': closing ā top='{', matches '}' ā pop ā stack=[]
stack is empty ā return true ā
Queue: When First In Must Come Out First
Use a queue when elements must be processed in the order they arrived. Queues model any level-by-level or sequential processing: BFS traversal, task scheduling, message delivery, sliding window with order tracking.
The core signal: if the order of processing must match the order of arrival, use a queue.
1import java.util.LinkedList;
2import java.util.Queue;
3
4public class QueueUsage {
5
6 // Queue is right here: BFS explores nodes level by level
7 // Find shortest path length in an unweighted graph
8 public static int shortestPath(int[][] graph, int src, int dst, int n) {
9 boolean[] visited = new boolean[n];
10 Queue<Integer> queue = new LinkedList<>();
11
12 queue.offer(src);
13 visited[src] = true;
14 int steps = 0;
15
16 while (!queue.isEmpty()) {
17 int size = queue.size(); // Process one level at a time
18
19 for (int i = 0; i < size; i++) {
20 int node = queue.poll();
21
22 if (node == dst) return steps;
23
24 for (int neighbor = 0; neighbor < n; neighbor++) {
25 if (graph[node][neighbor] == 1 && !visited[neighbor]) {
26 visited[neighbor] = true;
27 queue.offer(neighbor);
28 }
29 }
30 }
31
32 steps++;
33 }
34
35 return -1; // No path found
36 }
37
38 public static void main(String[] args) {
39 // Graph: 0-1, 0-2, 1-3, 2-3, 3-4
40 int[][] graph = {
41 {0,1,1,0,0},
42 {1,0,0,1,0},
43 {1,0,0,1,0},
44 {0,1,1,0,1},
45 {0,0,0,1,0}
46 };
47
48 System.out.println("Shortest path 0 to 4: " + shortestPath(graph, 0, 4, 5) + " steps");
49 System.out.println("Shortest path 0 to 1: " + shortestPath(graph, 0, 1, 5) + " steps");
50 }
51}Output:
Shortest path 0 to 4: 2 steps
Shortest path 0 to 1: 1 steps
Heap: When You Always Need the Minimum or Maximum
Use a heap (priority queue) when you repeatedly need the smallest or largest element from a dynamic collection. Heap gives O(1) access to the extreme element and O(log n) insert and delete.
The key signal: if you are sorting an entire collection just to repeatedly access one end of it, a heap eliminates the full sort.
1import java.util.PriorityQueue;
2
3public class HeapUsage {
4
5 // Heap is right here: repeatedly need the k-th largest as elements arrive
6 // Find the K largest elements from a stream ā O(n log k)
7 public static int[] kLargest(int[] stream, int k) {
8 // Min-heap of size k ā smallest of the k largest sits at top
9 PriorityQueue<int[]> minHeap = new PriorityQueue<>(
10 (a, b) -> a[0] - b[0]
11 );
12
13 for (int num : stream) {
14 minHeap.offer(new int[]{num});
15
16 // Maintain only k elements ā evict smallest if over capacity
17 if (minHeap.size() > k) {
18 minHeap.poll();
19 }
20 }
21
22 int[] result = new int[k];
23 for (int i = k - 1; i >= 0; i--) {
24 result[i] = minHeap.poll()[0];
25 }
26
27 return result;
28 }
29
30 public static void main(String[] args) {
31 int[] stream = {3, 1, 5, 12, 2, 11, 7, 8};
32 int k = 3;
33 int[] result = kLargest(stream, k);
34
35 System.out.print("Stream: ");
36 for (int x : stream) System.out.print(x + " ");
37 System.out.println();
38
39 System.out.print("Top " + k + " largest: ");
40 for (int x : result) System.out.print(x + " ");
41 System.out.println();
42 }
43}Output:
Stream: 3 1 5 12 2 11 7 8
Top 3 largest: 12 11 8
Why Not Sort Here
Sorting the entire array costs O(n log n). A min-heap of size k costs O(n log k). When k is small (find the top 10 from ten million elements), the heap approach is dramatically more efficient. More importantly, heaps handle streaming data ā you can process elements one at a time as they arrive, which sorting cannot do without reprocessing everything.
Hash Set: When You Need Uniqueness or Membership Tests
Use a hash set when you need to track which elements have been seen, detect duplicates, or check membership ā and you do not need to store any value associated with each element.
A hash set gives O(1) average insert, lookup, and delete. It is the right choice any time the question is "have I seen this before?"
1import java.util.HashSet;
2import java.util.Set;
3
4public class HashSetUsage {
5
6 // Hash set is right here: need to detect if a value was seen before
7 // Find the longest consecutive sequence ā O(n)
8 public static int longestConsecutive(int[] nums) {
9 Set<Integer> numSet = new HashSet<>();
10
11 for (int num : nums) numSet.add(num);
12
13 int longest = 0;
14
15 for (int num : numSet) {
16 // Only start counting from the beginning of a sequence
17 if (!numSet.contains(num - 1)) {
18 int current = num;
19 int length = 1;
20
21 // Extend the sequence as long as next element exists
22 while (numSet.contains(current + 1)) {
23 current++;
24 length++;
25 }
26
27 longest = Math.max(longest, length);
28 }
29 }
30
31 return longest;
32 }
33
34 public static void main(String[] args) {
35 int[] nums = {100, 4, 200, 1, 3, 2};
36 System.out.println("Array: " + java.util.Arrays.toString(nums));
37 System.out.println("Longest consecutive seq: " + longestConsecutive(nums));
38
39 int[] nums2 = {0, 3, 7, 2, 5, 8, 4, 6, 0, 1};
40 System.out.println("Longest consecutive seq: " + longestConsecutive(nums2));
41 }
42}Output:
Array: [100, 4, 200, 1, 3, 2]
Longest consecutive seq: 4
Longest consecutive seq: 9
Dry Run: Longest Consecutive on [100, 4, 200, 1, 3, 2]
numSet = {100, 4, 200, 1, 3, 2}
num=100: 99 not in set ā start of sequence
current=100, 101 not in set ā length=1
longest=1
num=4: 3 IS in set ā skip (not start of sequence)
num=200: 199 not in set ā start of sequence
current=200, 201 not in set ā length=1
longest=1
num=1: 0 not in set ā start of sequence
current=1, 2 in set ā current=2, length=2
current=2, 3 in set ā current=3, length=3
current=3, 4 in set ā current=4, length=4
current=4, 5 not in set ā stop
longest=4
Result: 4 (sequence 1,2,3,4)
Time: O(n) ā each element is visited at most twice across all sequences
How the Same Problem Changes With the Right Structure
The best illustration of data structure choice is showing the same problem solved with the wrong structure and then the right one.
Problem: Given a stream of integers, after each new integer, find the median of all integers seen so far.
With an array, each query requires re-sorting ā O(n log n) per insertion. With two heaps (a max-heap for the lower half and a min-heap for the upper half), insertion is O(log n) and median retrieval is O(1).
1import java.util.Collections;
2import java.util.PriorityQueue;
3
4public class MedianFinder {
5
6 // Two heaps is right here: need median after each insertion
7 // Lower half in max-heap, upper half in min-heap
8 // Median is at the tops of the heaps
9 private PriorityQueue<Integer> lowerHalf; // Max-heap ā largest of lower half at top
10 private PriorityQueue<Integer> upperHalf; // Min-heap ā smallest of upper half at top
11
12 public MedianFinder() {
13 lowerHalf = new PriorityQueue<>(Collections.reverseOrder());
14 upperHalf = new PriorityQueue<>();
15 }
16
17 public void addNum(int num) {
18 lowerHalf.offer(num); // Always add to lower half first
19
20 // Balance: move largest of lower to upper if needed
21 if (!upperHalf.isEmpty() && lowerHalf.peek() > upperHalf.peek()) {
22 upperHalf.offer(lowerHalf.poll());
23 }
24
25 // Balance sizes: lower can have at most one more element than upper
26 if (lowerHalf.size() > upperHalf.size() + 1) {
27 upperHalf.offer(lowerHalf.poll());
28 } else if (upperHalf.size() > lowerHalf.size()) {
29 lowerHalf.offer(upperHalf.poll());
30 }
31 }
32
33 public double findMedian() {
34 if (lowerHalf.size() > upperHalf.size()) {
35 return lowerHalf.peek(); // Odd count ā lower has the middle element
36 }
37 return (lowerHalf.peek() + upperHalf.peek()) / 2.0; // Even ā average of two middles
38 }
39
40 public static void main(String[] args) {
41 MedianFinder mf = new MedianFinder();
42 int[] stream = {5, 15, 1, 3, 8};
43
44 for (int num : stream) {
45 mf.addNum(num);
46 System.out.printf("Added %2d ā median = %.1f%n", num, mf.findMedian());
47 }
48 }
49}Output:
Added 5 ā median = 5.0
Added 15 ā median = 10.0
Added 1 ā median = 5.0
Added 3 ā median = 4.0
Added 8 ā median = 5.0
Why Two Heaps
The median is always either the middle element (odd count) or the average of two middle elements (even count). Two heaps maintain this middle boundary dynamically:
- āŗThe max-heap holds the lower half ā its top is the largest of the smaller elements.
- āŗThe min-heap holds the upper half ā its top is the smallest of the larger elements.
- āŗKeeping them balanced by at most one element ensures the median is always at one or both tops in O(1).
Single array: O(n log n) per insertion to re-sort. Two heaps: O(log n) per insertion, O(1) median retrieval.
The Data Structure Decision Guide
When you face a new problem, run through this checklist:
Do I need to access elements by position? Yes ā Array Do I need to find/check elements by value, not position? Existence only (seen or not) ā Hash Set Value associated with key ā Hash Map Do I need LIFO ā process most recent first? Yes ā Stack Do I need FIFO ā process oldest first? Yes ā Queue Do I need to repeatedly find the minimum or maximum? Min repeatedly ā Min-Heap Max repeatedly ā Max-Heap Both min and max (e.g. median) ā Two Heaps Do I need sorted order and fast insert/delete/search? Yes ā Balanced BST (TreeMap/TreeSet in Java, SortedList in Python) Do I need prefix-based search or autocomplete? Yes ā Trie Do I need to model hierarchical relationships? Yes ā Tree Do I need to model arbitrary connections? Yes ā Graph No clear answer? ā Start with Array or Hash Map. Array: if you iterate in order Hash Map: if you look up by value
Common Mistakes Beginners Make
Defaulting to arrays for everything. Arrays are the most familiar structure, so beginners reach for them first regardless of fit. If you are searching an array by value more than once, you should be using a hash map or hash set.
Using a list when a set is needed. Checking if x in list is O(n). Checking if x in set is O(1). Any time you need existence checks on a growing collection, convert to a set.
Sorting when a heap would do. Sorting an array to find the minimum or maximum repeatedly costs O(n log n) every time the collection changes. A heap maintains the extreme element dynamically in O(log n) per change.
Using a queue when a deque is needed. A standard queue only removes from the front. If you need to efficiently add or remove from both ends (sliding window maximum, palindrome checking), use a deque.
Choosing by familiarity instead of by operations. The right question is not "what structure do I know how to use?" but "what operations does this problem actually need, and what structure makes those operations fast?" Let the operations decide.
Interview Questions
Q: How do you decide between a hash map and an array for frequency counting?
If the values are bounded integers in a small range (like characters 'a' to 'z', which is only 26 values), an array of size 26 indexed by value is simpler and faster than a hash map. If the values are arbitrary integers or strings with no known bound, a hash map is required. Arrays win on simplicity and cache performance when the value range is small and known.
Q: When would you use a min-heap instead of sorting?
When the collection changes over time or when you only need the minimum repeatedly rather than all elements in sorted order. Sorting is a one-time O(n log n) operation on a static collection. A heap is O(log n) per insert or delete and O(1) to access the minimum ā it stays efficient as elements are added and removed dynamically.
Q: What is the difference between a stack and a queue, and when do you use each?
A stack is LIFO ā the most recently added element is removed first. Use it for nested structures, undo operations, DFS traversal, and expression evaluation. A queue is FIFO ā the oldest element is removed first. Use it for BFS traversal, level-by-level processing, task scheduling, and any situation where order of arrival must be preserved.
Q: Why would you use a hash set instead of a sorted array for membership testing?
A sorted array supports O(log n) binary search. A hash set supports O(1) average membership testing. For large collections with many membership queries and no need for sorted order, the hash set is faster. The sorted array is better when you also need predecessor/successor queries or range queries ā operations a hash set cannot support.
FAQs
Can I always just use a hash map and be safe?
Hash maps handle many problems well, but they have costs: O(n) space, no ordering guarantees, and worst-case O(n) operations due to hash collisions (rare in practice). For small fixed-size ranges, arrays are faster. For problems needing sorted order or range queries, a sorted structure is necessary. Use hash maps when you need fast key-value access on arbitrary keys ā not as a universal default.
What is the difference between a TreeMap and a HashMap in Java?
A HashMap stores key-value pairs with O(1) average operations but no ordering guarantee. A TreeMap maintains keys in sorted order with O(log n) operations ā it is backed by a Red-Black Tree. Use HashMap when you need fast access. Use TreeMap when you need the keys in sorted order, need to find the smallest or largest key, or need range queries like "all keys between 5 and 10."
When should I use a linked list over an array?
Linked lists give O(1) insert and delete at a known position (with a pointer to that position) and unlimited dynamic growth. Arrays give O(1) index access but O(n) insert and delete in the middle. In practice, arrays are preferable for most problems because index access is far more common than arbitrary insertion, and arrays have better cache performance. Linked lists appear in specific problems: LRU cache, reversing sequences without extra memory, and problems where you have a pointer to the node you need to modify.
Is there a data structure that handles everything well?
No ā every structure optimizes for some operations at the cost of others. This is the fundamental reason choosing the right structure matters. A hash map is O(1) for lookup but cannot answer "what is the minimum element?" A sorted array supports binary search but is O(n) for insertion. A heap answers "what is the minimum?" in O(1) but cannot look up arbitrary elements. Understanding these tradeoffs is the entire skill.
Quick Quiz
Question 1: You need to check whether each word in a document has been seen before as you read it. Which structure is most appropriate?
- āŗA) Array
- āŗB) Stack
- āŗC) Hash Set
- āŗD) Min-Heap
Answer: C) Hash Set. You need membership testing (have I seen this word?) with O(1) lookup as new words arrive. A hash set provides O(1) average insert and membership test. An array would require O(n) scanning per word.
Question 2: You are implementing a browser's back button. Pressing back should return to the most recently visited page. Which structure fits?
- āŗA) Queue
- āŗB) Stack
- āŗC) Hash Map
- āŗD) Array
Answer: B) Stack. The back button needs LIFO behavior ā the most recently visited page is the first one returned. Each visited page is pushed onto the stack. Pressing back pops the most recent one.
Question 3: You receive a stream of integers one at a time and must output the current median after each new integer. Which structure gives the best performance?
- āŗA) Sorted Array ā re-sort after each insertion
- āŗB) Single Max-Heap
- āŗC) Two Heaps ā max-heap for lower half, min-heap for upper half
- āŗD) Hash Map with counts
Answer: C) Two Heaps. Two balanced heaps give O(log n) insertion and O(1) median retrieval. Re-sorting an array after each insertion is O(n log n) per insertion. A single heap cannot efficiently find the median. A hash map with counts can work but requires O(n) median retrieval.
Question 4: You need to find whether any two numbers in an unsorted array sum to a target. Which structure gives O(n) overall time?
- āŗA) Sort the array, then use binary search ā O(n log n)
- āŗB) Use two nested loops ā O(n²)
- āŗC) Use a hash set to store seen values and check complement in O(1)
- āŗD) Use a min-heap and pop elements one by one
Answer: C) Use a hash set. For each element x, check if (target - x) is in the set in O(1), then add x to the set. One pass gives O(n) total. Sorting plus binary search is O(n log n). Nested loops are O(n²). The heap approach has no natural fit here.
Summary
Choosing the right data structure is not about memorizing a table. It is about understanding what operations your problem needs and which structure makes those operations fast.
The decision process to carry forward:
- āŗNeed access by position ā Array
- āŗNeed lookup, counting, or grouping by value ā Hash Map
- āŗNeed membership or duplicate detection ā Hash Set
- āŗNeed last-in-first-out ā Stack
- āŗNeed first-in-first-out ā Queue
- āŗNeed repeated minimum or maximum access ā Heap
- āŗNeed sorted order with fast insert and search ā Balanced BST
- āŗNeed prefix-based string search ā Trie
The data structure choice precedes the algorithm. Before thinking about how to solve a problem, think about what shape the data takes and what the problem needs to do with it. When the structure fits the problem's natural shape, the algorithm is almost always straightforward.
In the next topic, you will explore Dry Run and Debug Approach ā learning how to trace through code systematically to find bugs and validate correctness before submitting.