A Framework for Solving Any DSA Problem
A problem-solving framework is a fixed sequence of steps you run on every DSA problem so you never stare at a blank screen. The reliable order is: understand → examples → brute force → optimize → code → test. Following it turns “I have no idea” into steady, visible progress — which is exactly what interviewers want to see.
The goal is not to be clever immediately. It’s to always have a next step. A working slow solution beats a half-imagined fast one.
The six steps
- Understand. Restate the problem in your own words. What’s the input, the output, the constraints? Ask about edge cases: empty input, duplicates, negatives, one element.
- Examples. Hand-trace 2–3 small inputs, including a tricky one. This is where hidden requirements surface.
- Brute force. Find any correct solution, even O(n²) or worse. State its complexity out loud.
- Optimize. Look for wasted work. Can sorting, a hash map, two pointers, or precomputation remove a loop? Match the problem to a known pattern.
- Code. Translate the chosen idea cleanly. Small functions, clear names.
- Test. Run your examples from step 2, plus the edge cases. Fix and re-trace.
For beginners: Spend most of your time on steps 1–4 (on paper) and very little on step 5. Beginners rush to code; strong problem solvers think first, then type once.
Worked example: two-sum
Problem. Given an array nums and a target, return the indices of the two values that add up to target. Assume exactly one answer.
Step 1 — Understand. Input: an integer array and an integer target. Output: two indices i, j with nums[i] + nums[j] == target. Edge cases: could include negatives; the same value may appear twice (but we can’t reuse one index).
Step 2 — Examples. nums = [2, 7, 11, 15], target = 9 → [0, 1] because 2 + 7 = 9. A tricky one: nums = [3, 3], target = 6 → [0, 1] (two equal values, different indices).
Step 3 — Brute force. Check every pair. Correct, simple, O(n²) time and O(1) space.
#include <vector>
using namespace std;
vector<int> twoSum(const vector<int>& nums, int target) {
int n = nums.size();
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (nums[i] + nums[j] == target)
return {i, j};
return {};
}
import java.util.*;
int[] twoSum(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (nums[i] + nums[j] == target)
return new int[]{i, j};
return new int[]{};
}
function twoSum(nums, target) {
const n = nums.length;
for (let i = 0; i < n; i++)
for (let j = i + 1; j < n; j++)
if (nums[i] + nums[j] === target)
return [i, j];
return [];
}
def two_sum(nums, target):
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]
return []
Step 4 — Optimize. The inner loop just searches for target - nums[i]. A hash map makes that search O(1). Walk once, and for each x ask: have I already seen target - x? This trades O(n) memory for an O(n) time solution.
Step 5 — Code the optimized version.
#include <vector>
#include <unordered_map>
using namespace std;
vector<int> twoSum(const vector<int>& nums, int target) {
unordered_map<int, int> seen; // value -> index
for (int i = 0; i < (int)nums.size(); i++) {
int need = target - nums[i];
if (seen.count(need)) return {seen[need], i};
seen[nums[i]] = i;
}
return {};
}
import java.util.*;
int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>(); // value -> index
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
if (seen.containsKey(need)) return new int[]{seen.get(need), i};
seen.put(nums[i], i);
}
return new int[]{};
}
function twoSum(nums, target) {
const seen = new Map(); // value -> index
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if (seen.has(need)) return [seen.get(need), i];
seen.set(nums[i], i);
}
return [];
}
def two_sum(nums, target):
seen = {} # value -> index
for i, x in enumerate(nums):
need = target - x
if need in seen:
return [seen[need], i]
seen[x] = i
return []
Step 6 — Test. [2,7,11,15], 9 → [0,1]. The tricky [3,3], 6: at i=0 we store 3→0; at i=1, need = 3 is already in the map → [0,1]. Correct.
Pitfall: Insert
nums[i]after checking forneed, not before — otherwise a single element can pair with itself.
Going from brute force to optimal
The optimize step has a small toolbox you’ll reuse constantly:
- Sort first — unlocks the two-pointers technique and binary search.
- Hash map / set — turns “have I seen this?” into O(1) and removes a nested loop.
- Precompute — running totals (prefix sums) answer range questions in O(1).
- Pick the right structure — a heap for “top-k”, a stack for “most recent”.
Going deeper: Before optimizing, compute the target complexity from the constraints. If
n ≤ 10^5, an O(n²) ≈ 10^10 solution is too slow — you need O(n log n) or better. Constraints tell you which complexity to aim for, which often reveals the intended technique.
Practice the loop
The framework only sticks through repetition. Try the complexity exercises to sharpen step 4, then apply the full loop to the array exercises. Over time, steps 1–4 become near-automatic.