How to Approach a DSA Problem
Knowing what a hash map or a two-pointer technique does is not the same as knowing when to reach for it while staring at a blank editor. The gap between “I’ve studied algorithms” and “I can solve a problem I’ve never seen before” is a process gap, not a knowledge gap. This lesson teaches that process: a repeatable sequence of steps you can apply to any DSA problem, whether it’s a homework exercise, a take-home assignment, or a live coding interview. We’ll use the classic Two Sum problem as a running example to make each step concrete, then generalize the lessons to problems you haven’t seen yet.
Overview: A Repeatable Framework
Struggling with a DSA problem is rarely about not knowing an algorithm — it’s usually about not having a process. Experienced problem-solvers follow roughly the same steps every time, whether the problem is “reverse a linked list” or “find the shortest path in a weighted graph.” Internalizing this process matters more than memorizing any single algorithm, because it is what lets you handle something novel. The five steps are: Understand, Explore with examples, Brute force, Optimize by pattern-matching, and Verify.
Step 1: Understand the problem
Before writing a single line of code, restate the problem in your own words and pin down the constraints. For Two Sum: “Given an array of integers nums and an integer target, return the indices of two numbers that add up to target.” Then ask clarifying questions you would ask an interviewer: Is the array sorted? Can there be duplicate values? Is there guaranteed to be exactly one solution? Can the same element be used twice? These answers change which algorithms are even valid, so skipping this step is the single most common reason people write code that solves the wrong problem.
Step 2: Explore with examples
Work through one or two examples by hand, including at least one edge case (empty input, a single element, all duplicate values). For nums = [2, 7, 11, 15] and target = 9, the answer is indices [0, 1] because 2 + 7 = 9. Walking through this by hand — before any code exists — often reveals the pattern the algorithm needs to exploit (here: for each number, we’re really looking for its “complement,” target - num).
Step 3: Write a brute-force solution first
Resist the urge to jump straight to the clever answer. Write the simplest correct solution you can think of, even if it is slow, and confirm it works on your examples. For Two Sum, the obvious brute force is to check every pair of numbers with nested loops. It’s correct, easy to reason about, and gives you something to compare an optimized version against.
Step 4: Optimize by recognizing a pattern
Once you have a working brute force, ask: what is the brute force repeating unnecessarily? Here, the inner loop re-scans the array looking for a specific value (the complement) — and looking something up by value is exactly what a hash map is good at. Recognizing “I need fast lookups by value” as the bottleneck is what leads you from an O(n^2) nested loop to an O(n) single pass with a dictionary. This kind of pattern recognition — hash map for lookups, two pointers for sorted arrays, sliding window for contiguous subarrays, recursion or dynamic programming for overlapping subproblems — is the core skill this whole course is building toward.
Step 5: Verify
Trace your optimized solution against the same examples you used in Step 2, plus the edge cases. Then state its time and space complexity out loud (or in a comment) — this is expected in interviews and is good discipline even when no one is watching.
Time and Space Complexity
The whole point of optimizing is to move down this table. Which row is achievable depends on constraints you should have pinned down in Step 1 — for example, the two-pointer approach below only works if the array is already sorted (or you’re allowed to sort it).
| Approach | Time Complexity | Space Complexity | Why |
|---|---|---|---|
| Brute force (nested loops) | O(n^2) |
O(1) |
Two nested loops each scan up to n elements, so total work grows quadratically; no extra data structure is built. |
| Hash map, single pass | O(n) average |
O(n) |
Each dictionary lookup and insert is O(1) on average, and we perform one per element, so total time is linear; the dictionary can grow to hold up to n entries. |
| Two pointers (sorted array) | O(n log n) if sorting is required first, otherwise O(n) |
O(1) extra, excluding the input |
Once sorted, the two pointers move toward each other and together take at most n total steps, but sorting an unsorted array first costs O(n log n). |
Examples
The first example is the brute-force baseline: correct, but it re-checks pairs unnecessarily.
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
return []
nums = [2, 7, 11, 15]
target = 9
result = two_sum_brute_force(nums, target)
print(result)
Output:
[0, 1]
The outer loop starts at i = 0 (nums[0] = 2) and the inner loop immediately checks j = 1 (nums[1] = 7); since 2 + 7 == 9, it returns [0, 1] right away. On a larger input this pair might be found much later, after many wasted comparisons — that wasted work is exactly what the next version eliminates.
The second example applies the pattern-recognition step: since we repeatedly ask “have I seen this complement before?”, a dictionary turns that question into an O(1) average-case lookup.
def two_sum_optimized(nums: list[int], target: int) -> list[int]:
seen: dict[int, int] = {}
for index, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], index]
seen[num] = index
return []
nums = [3, 2, 4]
target = 6
result = two_sum_optimized(nums, target)
print(result)
Output:
[1, 2]
This one pass never looks at the same pair twice, which is why it’s linear instead of quadratic. The step-by-step section below traces exactly how seen fills in.
The third example shows the same pattern-recognition skill applied to a different problem, to make clear this is a general technique, not a Two Sum trick. “Does this array contain a duplicate?” is also an existence question, so it also calls for a set.
def contains_duplicate(nums: list[int]) -> bool:
seen: set[int] = set()
for num in nums:
if num in seen:
return True
seen.add(num)
return False
nums = [1, 2, 3, 1]
print(contains_duplicate(nums))
Output:
True
1, 2, and 3 are added to seen in turn; when 1 is encountered a second time, it is already in seen, so the function returns True immediately without scanning the rest of the array.
How It Works Step by Step
Tracing two_sum_optimized on nums = [3, 2, 4], target = 6, one iteration at a time:
| index | num | complement | complement in seen? | action |
|---|---|---|---|---|
| 0 | 3 | 3 | No (seen is empty) |
store seen[3] = 0 |
| 1 | 2 | 4 | No | store seen[2] = 1 |
| 2 | 4 | 2 | Yes, seen[2] = 1 |
return [1, 2] |
Notice the loop never has to look ahead or backtrack — by the time it reaches index 2, every number that came before it is already recorded in seen, so checking “has this number’s partner already appeared?” is a single dictionary lookup instead of a second loop.
Common Mistakes
Mistake 1: Skipping edge cases because you skipped Step 1
Jumping straight to code without clarifying constraints leads to code that silently assumes things that aren’t true — like assuming the input is never empty.
def find_max(nums: list[int]) -> int:
max_val = nums[0]
for num in nums:
if num > max_val:
max_val = num
return max_val
print(find_max([]))
This raises IndexError: list index out of range because nums[0] is accessed before anyone checks whether nums is empty — a question that Step 1 (“can the input be empty?”) should have surfaced before any code was written. The fix is to handle the edge case explicitly:
def find_max(nums: list[int]) -> int:
if not nums:
raise ValueError("nums must not be empty")
max_val = nums[0]
for num in nums:
if num > max_val:
max_val = num
return max_val
print(find_max([3, 1, 4, 1, 5]))
Output:
5
Mistake 2: Using a mutable default argument in a recursive helper
When a problem calls for an accumulator passed through recursive calls (common in backtracking), it is tempting to default it to an empty list. This is a classic Python trap: default argument values are created once, when the function is defined, and reused across every call that doesn’t supply that argument.
def collect_positive(nums: list[int], index: int = 0, result: list[int] = []) -> list[int]:
if index == len(nums):
return result
if nums[index] > 0:
result.append(nums[index])
return collect_positive(nums, index + 1, result)
print(collect_positive([1, -2, 3]))
print(collect_positive([4, 5]))
Output:
[1, 3]
[1, 3, 4, 5]
The second call’s output leaks values from the first call, because both calls shared the exact same list object as their default. The fix is to default to None and create a fresh list inside the function body:
def collect_positive(nums: list[int], index: int = 0, result: list[int] | None = None) -> list[int]:
if result is None:
result = []
if index == len(nums):
return result
if nums[index] > 0:
result.append(nums[index])
return collect_positive(nums, index + 1, result)
print(collect_positive([1, -2, 3]))
print(collect_positive([4, 5]))
Output:
[1, 3]
[4, 5]
Best Practices
- Restate the problem in your own words and clarify constraints (input size, duplicates allowed, sorted or not, negative numbers) before writing any code.
- Work through at least one example by hand, plus an edge case (empty input, one element, all-equal values), before you touch the keyboard.
- Always write a brute-force solution first to lock in correctness, then optimize — a working
O(n^2)solution is worth more than a brokenO(n)one. - Name the pattern before you name the code: “this needs fast existence checks” (hash set/map), “this needs to shrink a sorted range” (two pointers), “this needs a moving window” (sliding window), “this has overlapping subproblems” (recursion/DP).
- State the time and space complexity of your final solution explicitly — don’t leave it implicit.
- Prefer Python’s built-in
dict,set,collections.deque, andheapqover hand-rolled versions unless the exercise is specifically about building that structure from scratch. - Test against edge cases, not just the happy path, before declaring a solution finished.
Practice Exercises
- Given an array of integers and a target sum, return
Trueif any two distinct numbers add up to the target. Apply the full framework: restate the problem, trace an example by hand, write a brute-force nested-loop version, then optimize it with a set. State the time complexity of each version. - Given a string, determine whether it contains any repeated characters. Solve it first with a
setinO(n)time andO(n)space, then think about how you could solve it using onlyO(1)extra space if you know the string contains only lowercase English letters (hint: there are only 26 possible characters). - You are given an array of integers that is already sorted in ascending order. Find two numbers that sum to a target value using only
O(1)additional space. Why does a hash-map solution no longer need to be your default here? What property of the sorted array lets two pointers, starting at each end and moving inward, solve this inO(n)time?
Summary
- A repeatable 5-step framework — Understand, Explore with examples, Brute force, Optimize by pattern-matching, Verify — turns an unfamiliar problem into a solvable one.
- Brute-force Two Sum (nested loops) runs in
O(n^2)time andO(1)space; recognizing the “fast lookup” pattern and switching to a hash map brings it down toO(n)average time at the cost ofO(n)space. - When the input is already sorted, a two-pointer approach can solve the same class of problem in
O(n)time using onlyO(1)extra space — the right approach depends on constraints you should confirm in Step 1. - Common mistakes come from skipping steps: not clarifying edge cases before coding (crashes on empty input), and reusing a mutable default argument across recursive calls (silent, hard-to-debug data leaks between calls).
- Always state your final solution’s time and space complexity, and justify it in terms of what the code is actually doing on an input of size
n.
