Problem-solving workflow
A repeatable ten-step workflow for any algorithm problem: understand, make examples, write a brute-force baseline, find the pattern, pick the structure, prove, analyse, implement, test and optimise. The animations show a brute-force solution and its optimised replacement side by side.
Learning goals
- Follow a fixed workflow instead of guessing
- Use a brute-force solution as a baseline and test oracle
- Justify a design with a short proof and a complexity analysis
- Test and optimise methodically
Prerequisites
Understand requirements
Restate the problem in your own words. Identify the input, the output, the constraints and anything ambiguous: are duplicates allowed, is the answer unique, what if there is no answer? Ask or decide, and write the decision down.
Most wasted time comes from solving a slightly different problem than the one asked.
Build examples
Create small examples by hand, including the typical case, the smallest case and an awkward case. Work out the expected answers yourself. These become your tests and often reveal the pattern.
For two-sum with target 13: [7, 11, 2, 15, 4, 9] returns indexes of 11 and 2; [1, 2] with target 10 has no answer; [3, 3] with target 6 uses two equal values at different indexes.
Brute-force baseline
Write the simplest correct solution first, whatever its speed. It proves you understand the problem, gives a correct answer for small inputs, and later serves as an oracle to test the fast solution against.
Check every pair for two-sum: obviously correct, O(n²). Watch how many pairs it examines, including the worst case where no pair works.
Brute force: check every pair
Interactive · edit the inputint[] arr · length 6
Try every pair (i, j) with i < j. There are n(n-1)/2 = 15 of them in the worst case.
1public static int[] twoSum(int[] nums, int target) {2for (int i = 0; i < nums.length; i++) {3for (int j = i + 1; j < nums.length; j++) {4if (nums[i] + nums[j] == target) {5return new int[] { i, j };6}7}8}9return new int[] { -1, -1 };10}
Live state
target
13
n
6
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=13 n=6 | Try every pair (i, j) with i < j. There are n(n-1)/2 = 15 of them in the worst case. |
| 2 | L2 | target=13 i=0 | Fix i = 0 (value 7) and pair it with every later element. |
| 3 | L4 | target=13 i=0 j=1 sum=18 | Not the target; try the next j. |
| 4 | L4 | target=13 i=0 j=2 sum=9 | Not the target; try the next j. |
| 5 | L4 | target=13 i=0 j=3 sum=22 | Not the target; try the next j. |
| 6 | L4 | target=13 i=0 j=4 sum=11 | Not the target; try the next j. |
| 7 | L4 | target=13 i=0 j=5 sum=16 | Not the target; try the next j. |
| 8 | L2 | target=13 i=1 | Fix i = 1 (value 11) and pair it with every later element. |
| 9 | L4 | target=13 i=1 j=2 sum=13 | This pair adds up to the target. |
| 10 | L5 | return=[1, 2] | return [1, 2] after 6 pair checks. Simple and obviously correct, but O(n²) in the worst case. |
Identify patterns
Ask which known pattern the problem resembles: searching a sorted range (binary search), a contiguous range (prefix sums or sliding window), pairs (two pointers or hashing), nearest larger value (monotonic stack), choices with overlap (dynamic programming), connections (graph).
In the brute force, the inner loop repeatedly asks “is there a value equal to target - nums[i] later in the array?”. A question about membership is exactly what a hash set or map answers in O(1).
Choose structure
Choose the structure that makes the repeated question cheap. Membership by value: hash set or map. Smallest next: heap. Order and ranges: balanced tree. Position: array.
For two-sum, a HashMap<value, index> turns the inner loop into a single lookup.
Prove
Convince yourself the idea is right before coding. A short argument is enough: state an invariant or explain why no answer is missed. For the hash map version: when we reach index i, the map holds every earlier value, so if a partner exists earlier we will find it; if the partner comes later, it will find i when it is processed.
Proving catches ideas that work on the examples but fail in general.
Analyze
State time and space complexity and check them against the constraints. Hash map two-sum: one pass, O(1) expected per lookup and insert, so O(n) time and O(n) space. For n = 10⁵ this is easily fast enough, while the O(n²) baseline needs about 5 × 10⁹ pair checks.
Mention the assumption: hashing is O(1) on average; adversarial keys can degrade it.
Implement
Write the code from the design, keeping names meaningful and each step small. Implement the contract exactly, including what to return when there is no answer.
The animation shows the optimised solution: each iteration computes the partner value, looks it up, and stores the current value.
1static int[] twoSum(int[] nums, int target) {2Map<Integer, Integer> seen = new HashMap<>(); // value -> index3for (int i = 0; i < nums.length; i++) {4Integer j = seen.get(target - nums[i]);5if (j != null) return new int[] { j, i };6seen.put(nums[i], i);7}8return new int[] { -1, -1 };9}
Optimised: remember what you have seen
Interactive · edit the inputint[] arr · length 6
seen (value:index)
A hash map answers “have I seen this value?” in O(1) on average, so one pass is enough.
1public static int[] twoSum(int[] nums, int target) {2Map<Integer, Integer> seen = new HashMap<>(); // value -> index3for (int i = 0; i < nums.length; i++) {4int need = target - nums[i];5if (seen.containsKey(need)) {6return new int[] { seen.get(need), i };7}8seen.put(nums[i], i);9}10return new int[] { -1, -1 };11}
Live state
target
13
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | target=13 | A hash map answers “have I seen this value?” in O(1) on average, so one pass is enough. |
| 2 | L5 | i=0 nums[i]=7 need=6 | 13 - 7 = 6, which has not appeared yet. |
| 3 | L8 | i=0 size=1 | Store 7 → 0 for future lookups. |
| 4 | L5 | i=1 nums[i]=11 need=2 | 13 - 11 = 2, which has not appeared yet. |
| 5 | L8 | i=1 size=2 | Store 11 → 1 for future lookups. |
| 6 | L5 | i=2 nums[i]=2 need=11 | 11 is in the map: it was stored at index 1, so that value plus 2 makes 13. |
| 7 | L6 | return=[1, 2] | return [1, 2] after 3 lookups: O(n) time and O(n) extra memory. Brute force needed O(1) memory but O(n²) time. |
Test
Run your hand-made examples, then compare the fast solution with the brute force on thousands of small random inputs. Mismatches show real bugs, and the failing input is already small. Use a fixed random seed so a failure is reproducible.
Include edge cases: empty, one element, duplicates, negatives and large values.
1Random rnd = new Random(1);2for (int t = 0; t < 10_000; t++) {3int[] a = rnd.ints(1 + rnd.nextInt(8), -5, 6).toArray();4int target = rnd.nextInt(11) - 5;5boolean fast = twoSum(a, target)[0] != -1;6boolean slow = false;7for (int i = 0; i < a.length; i++) for (int j = i + 1; j < a.length; j++) if (a[i] + a[j] == target) slow = true;8if (fast != slow) throw new AssertionError(Arrays.toString(a) + " target " + target);9}
Optimize
Optimise only after the solution is correct and you know where the time goes. First improve the algorithm (better complexity), then the constants (primitive arrays instead of boxed collections, avoiding repeated allocation).
Measure with realistic input. Optimising code that is not the bottleneck wastes effort and makes it harder to read.
Practice ladder
Now try it yourself
Easy
- Apply the ten steps to “find the maximum of an array” and write one example per step
- Write a brute-force solution for “does the array contain a duplicate?”
Medium
- Apply the workflow to “longest substring without repeating characters”
- Write a random tester that compares two implementations
Advanced
- Take a problem you solved earlier, identify the pattern and replace its brute force with the optimal solution, proving the new idea in two sentences
Key takeaways
- Follow the workflow: understand, examples, brute force, pattern, structure, prove, analyse, implement, test, optimise.
- A brute-force baseline is both a first answer and a test oracle.
- Make a repeated question cheap by choosing the right structure.