Brute Force to Optimization
Why Brute Force First
Every experienced developer starts with brute force. Not because they cannot think of better solutions — because brute force does three things nothing else can:
It confirms you understand the problem correctly. A working brute force on the right problem is worth more than a broken optimized solution on the wrong one.
It gives you a baseline to improve. You cannot optimize what you do not have. Brute force is the starting point, not the endpoint.
It reveals the bottleneck. The part of your brute force that does the most unnecessary repeated work is almost always the exact part that optimization eliminates.
The developers who struggle to optimize in interviews are not the ones who lack algorithmic knowledge. They are the ones who jumped to optimization before fully understanding the brute force. They have no clear bottleneck to target, so they are guessing.
This topic gives you a systematic method. For any brute force, you will learn to identify exactly what is slow, why it is slow, and which optimization pattern removes it.
The Optimization Framework
Every brute force to optimal transformation follows the same four-step process:
Step 1 — Write the brute force. Correct first, fast second. State the approach clearly before coding.
Step 2 — Measure its complexity. Count the loops, identify how each scales with n. State time and space complexity explicitly.
Step 3 — Find the bottleneck. Ask: what is doing the most work? What is being recomputed or rescanned unnecessarily? What information is being thrown away and then rebuilt?
Step 4 — Apply the right pattern. Match the bottleneck to a known optimization technique. The bottleneck almost always points directly at which pattern to use.
This four-step process is repeatable. It works on every problem, including ones you have never seen before. Practice it until it feels automatic.
The Bottleneck to Pattern Map
Most bottlenecks fall into a small number of categories. Each category has a corresponding fix.
| Bottleneck | What is Wasted | Pattern | Complexity Gain |
|---|---|---|---|
| Scanning array to check existence | O(n) per lookup | Hashing | O(n²) to O(n) |
| Recomputing subarray sum from scratch | O(n) per window | Sliding Window | O(n²) to O(n) |
| Checking all pairs in sorted array | O(n) per element | Two Pointers | O(n²) to O(n) |
| Recomputing range sums repeatedly | O(n) per query | Prefix Sum | O(n²) to O(n) |
| Scanning sorted array linearly | O(n) per search | Binary Search | O(n) to O(log n) |
| Recomputing overlapping subproblems | Exponential recompute | Dynamic Programming | Exponential to Polynomial |
| Sorting to find K extreme values | O(n log n) always | Heap (Top K) | O(n log n) to O(n log k) |
Memorizing this table is not the goal. Understanding why each bottleneck leads to each pattern is. When you can derive the fix from the bottleneck, you will never be stuck in an interview.
Example 1: Two Sum — O(n²) to O(n)
Problem: Given an array of integers and a target, return the indices of two numbers that sum to the target.
Brute Force — O(n²)
The most obvious approach: check every pair. For each element, scan every other element to find a complement.
1public class TwoSumBrute {
2
3 // Brute force — check every pair
4 // For each element, scan remaining elements for the complement
5 public static int[] twoSumBrute(int[] nums, int target) {
6 for (int i = 0; i < nums.length; i++) {
7 for (int j = i + 1; j < nums.length; j++) {
8 if (nums[i] + nums[j] == target) {
9 return new int[]{i, j};
10 }
11 }
12 }
13 return new int[]{};
14 }
15
16 public static void main(String[] args) {
17 int[] nums = {2, 7, 11, 15};
18 int[] result = twoSumBrute(nums, 9);
19 System.out.println("Brute: [" + result[0] + ", " + result[1] + "]");
20 }
21}Output:
Brute: [0, 1]
Bottleneck Analysis
Time: O(n²) — for each of n elements, scan up to n more elements
Space: O(1)
Bottleneck: for each element nums[i], we scan the entire rest
of the array looking for (target - nums[i]).
Wasted work: we already visited those elements. Their values
are not forgotten — we just do not store them.
Observation: if we stored every value we have seen so far,
we could check in O(1) whether the complement exists.
Fix: use a hash map. Store each value with its index.
Before storing, check if the complement is already there.
Optimal — O(n)
1import java.util.HashMap;
2import java.util.Map;
3
4public class TwoSumOptimal {
5
6 // Optimal — one pass hash map
7 // Store each value; check if complement was seen before storing
8 public static int[] twoSum(int[] nums, int target) {
9 Map<Integer, Integer> seen = new HashMap<>();
10
11 for (int i = 0; i < nums.length; i++) {
12 int complement = target - nums[i];
13
14 if (seen.containsKey(complement)) {
15 return new int[]{seen.get(complement), i};
16 }
17
18 seen.put(nums[i], i);
19 }
20
21 return new int[]{};
22 }
23
24 public static void main(String[] args) {
25 int[] nums = {2, 7, 11, 15};
26 int[] result = twoSum(nums, 9);
27 System.out.println("Optimal: [" + result[0] + ", " + result[1] + "]");
28 }
29}Output:
Optimal: [0, 1]
Complexity Comparison
Approach Time Space Bottleneck Removed Brute O(n²) O(1) No Optimal O(n) O(n) Yes — inner scan replaced by O(1) hash lookup For n=1,000,000: Brute: 1,000,000,000,000 operations Optimal: 1,000,000 operations Tradeoff: O(n) extra space for the hash map buys O(n²) → O(n) time.
Example 2: Maximum Subarray Sum — O(n²) to O(n)
Problem: Given an array and window size k, find the maximum sum of any contiguous subarray of length k.
Brute Force — O(n²)
For every valid starting position, compute the sum of the next k elements from scratch.
1public class MaxSumBrute {
2
3 // Brute force — recompute sum for every window from scratch
4 public static int maxSumBrute(int[] arr, int k) {
5 int maxSum = Integer.MIN_VALUE;
6
7 for (int i = 0; i <= arr.length - k; i++) {
8 int windowSum = 0;
9
10 // Recompute full sum for each window — O(k) per window
11 for (int j = i; j < i + k; j++) {
12 windowSum += arr[j];
13 }
14
15 maxSum = Math.max(maxSum, windowSum);
16 }
17
18 return maxSum;
19 }
20
21 public static void main(String[] args) {
22 int[] arr = {2, 1, 5, 1, 3, 2};
23 System.out.println("Brute (k=3): " + maxSumBrute(arr, 3));
24 }
25}Output:
Brute (k=3): 9
Bottleneck Analysis
Time: O(n × k) — n windows, each costs O(k) to sum
Space: O(1)
Bottleneck: when sliding from window [i, i+k-1] to [i+1, i+k],
we recompute the sum of k-1 elements that were already
included in the previous window.
Wasted work: the overlap between consecutive windows is k-1 elements.
We compute their contribution again from scratch each time.
Observation: the new window's sum = previous sum + arr[i+k] - arr[i]
We do not need to resum — we just add one and remove one.
Fix: sliding window. Maintain a running sum. Slide by:
add the incoming right element, remove the outgoing left element.
Optimal — O(n)
1public class MaxSumOptimal {
2
3 // Optimal — sliding window maintains running sum
4 // Add right element, remove left element — no rescan
5 public static int maxSum(int[] arr, int k) {
6 int windowSum = 0;
7
8 // Build the first window
9 for (int i = 0; i < k; i++) {
10 windowSum += arr[i];
11 }
12
13 int maxSum = windowSum;
14
15 // Slide: add incoming right, remove outgoing left
16 for (int i = k; i < arr.length; i++) {
17 windowSum += arr[i]; // New right element enters
18 windowSum -= arr[i - k]; // Old left element exits
19 maxSum = Math.max(maxSum, windowSum);
20 }
21
22 return maxSum;
23 }
24
25 public static void main(String[] args) {
26 int[] arr = {2, 1, 5, 1, 3, 2};
27 System.out.println("Optimal (k=3): " + maxSum(arr, 3));
28 }
29}Output:
Optimal (k=3): 9
Dry Run: Sliding Window on [2, 1, 5, 1, 3, 2], k=3
Build first window (indices 0-2): windowSum = 2+1+5 = 8 maxSum = 8 Window: [2, 1, 5] i=3: add arr[3]=1, remove arr[0]=2 windowSum = 8 + 1 - 2 = 7 maxSum = 8 Window: [1, 5, 1] i=4: add arr[4]=3, remove arr[1]=1 windowSum = 7 + 3 - 1 = 9 maxSum = 9 Window: [5, 1, 3] i=5: add arr[5]=2, remove arr[2]=5 windowSum = 9 + 2 - 5 = 6 maxSum = 9 Window: [1, 3, 2] Result: 9 Each element enters and exits the window exactly once — O(n) total.
Example 3: Count Pairs with Difference K — O(n²) to O(n)
Problem: Given an array of integers and a value k, count the number of pairs (i, j) where i < j and abs(nums[i] - nums[j]) == k.
Brute Force — O(n²)
Check every pair.
1public class PairDiffBrute {
2
3 // Brute force — check every pair
4 public static int countPairsBrute(int[] nums, int k) {
5 int count = 0;
6
7 for (int i = 0; i < nums.length; i++) {
8 for (int j = i + 1; j < nums.length; j++) {
9 if (Math.abs(nums[i] - nums[j]) == k) {
10 count++;
11 }
12 }
13 }
14
15 return count;
16 }
17
18 public static void main(String[] args) {
19 int[] nums = {1, 5, 3, 4, 2};
20 System.out.println("Brute (k=2): " + countPairsBrute(nums, 2));
21 }
22}Output:
Brute (k=2): 3
Bottleneck Analysis
Time: O(n²) — for each of n elements, scan up to n more
Space: O(1)
Bottleneck: for each nums[i], we scan remaining elements looking
for nums[j] such that nums[j] == nums[i] + k or nums[i] - k.
Wasted work: elements already seen are not stored anywhere.
We re-scan them for every new element.
Observation: for nums[i], the complement is nums[i]+k or nums[i]-k.
If we stored all values seen so far in a set,
we could check O(1) whether the complement exists.
Fix: hash set. One pass. For each element, check if (num+k) or
(num-k) is in the set. Then add the element to the set.
Optimal — O(n)
1import java.util.HashSet;
2import java.util.Set;
3
4public class PairDiffOptimal {
5
6 // Optimal — hash set for O(1) complement lookup
7 public static int countPairs(int[] nums, int k) {
8 Set<Integer> seen = new HashSet<>();
9 int count = 0;
10
11 for (int num : nums) {
12 // Check if a number that forms a valid pair was seen before
13 if (seen.contains(num + k)) count++;
14 if (seen.contains(num - k)) count++;
15 seen.add(num);
16 }
17
18 return count;
19 }
20
21 public static void main(String[] args) {
22 int[] nums = {1, 5, 3, 4, 2};
23 System.out.println("Optimal (k=2): " + countPairs(nums, 2));
24 }
25}Output:
Optimal (k=2): 3
Dry Run: Hash Set on [1, 5, 3, 4, 2], k=2
seen={}, count=0
num=1: check 1+2=3 in seen? No. check 1-2=-1 in seen? No. add 1.
seen={1}, count=0
num=5: check 5+2=7 in seen? No. check 5-2=3 in seen? No. add 5.
seen={1,5}, count=0
num=3: check 3+2=5 in seen? YES → count=1. check 3-2=1 in seen? YES → count=2. add 3.
seen={1,5,3}, count=2
num=4: check 4+2=6 in seen? No. check 4-2=2 in seen? No. add 4.
seen={1,5,3,4}, count=2
num=2: check 2+2=4 in seen? YES → count=3. check 2-2=0 in seen? No. add 2.
seen={1,5,3,4,2}, count=3
Result: 3
Pairs found: (5,3), (3,1), (4,2) — all have difference 2 ✓
Example 4: Subarray Sum Equals K — O(n²) to O(n)
Problem: Count the number of contiguous subarrays whose sum equals k.
Brute Force — O(n²)
For every starting index, expand and check every subarray.
1public class SubarraySumBrute {
2
3 // Brute force — check every subarray
4 public static int subarraySumBrute(int[] arr, int k) {
5 int count = 0;
6
7 for (int i = 0; i < arr.length; i++) {
8 int sum = 0;
9
10 for (int j = i; j < arr.length; j++) {
11 sum += arr[j]; // Expand window to the right
12 if (sum == k) count++; // Check condition
13 }
14 }
15
16 return count;
17 }
18
19 public static void main(String[] args) {
20 int[] arr = {1, 2, 3, -1, 2};
21 System.out.println("Brute (k=3): " + subarraySumBrute(arr, 3));
22 }
23}Output:
Brute (k=3): 3
Bottleneck Analysis
Time: O(n²) — n starting points, each expanding up to n times
Space: O(1)
Bottleneck: for each starting index i, we re-expand the window
from scratch to compute cumulative sums.
Key insight: the sum of subarray from index l to r equals:
prefixSum[r] - prefixSum[l-1]
A subarray from l to r sums to k when:
prefixSum[r] - prefixSum[l-1] = k
prefixSum[l-1] = prefixSum[r] - k
Observation: as we compute the running prefix sum at each index r,
we need to count how many times (prefixSum[r] - k)
has appeared as a prefix sum before index r.
Fix: prefix sum + hash map. Store frequency of each prefix sum seen.
For each new element, check if (currentSum - k) is in the map.
Optimal — O(n)
1import java.util.HashMap;
2import java.util.Map;
3
4public class SubarraySumOptimal {
5
6 // Optimal — prefix sum + hash map
7 // Count how many times (currentSum - k) appeared as a prefix sum before
8 public static int subarraySum(int[] arr, int k) {
9 Map<Integer, Integer> prefixCount = new HashMap<>();
10 prefixCount.put(0, 1); // Empty prefix — sum of zero seen once
11
12 int currentSum = 0;
13 int count = 0;
14
15 for (int num : arr) {
16 currentSum += num;
17
18 // How many times did (currentSum - k) appear as a prefix sum?
19 count += prefixCount.getOrDefault(currentSum - k, 0);
20
21 // Record this prefix sum
22 prefixCount.put(currentSum, prefixCount.getOrDefault(currentSum, 0) + 1);
23 }
24
25 return count;
26 }
27
28 public static void main(String[] args) {
29 int[] arr = {1, 2, 3, -1, 2};
30 System.out.println("Optimal (k=3): " + subarraySum(arr, 3));
31 }
32}Output:
Optimal (k=3): 3
Dry Run: Prefix Sum + Hash Map on [1, 2, 3, -1, 2], k=3
prefixCount={0:1}, currentSum=0, count=0
num=1: currentSum=1, check 1-3=-2 in map? No. store {0:1, 1:1}
num=2: currentSum=3, check 3-3= 0 in map? YES(1 time) → count=1. store {0:1,1:1,3:1}
num=3: currentSum=6, check 6-3= 3 in map? YES(1 time) → count=2. store {...,6:1}
num=-1:currentSum=5, check 5-3= 2 in map? No. store {...,5:1}
num=2: currentSum=7, check 7-3= 4 in map? No. store {...,7:1}
Result: 3
Subarrays found:
[1,2] → sum=3 ✓ (found when currentSum=3, prefixSum[0]=0 was in map)
[1,2,3,-1,2] wait — let us verify manually:
[1,2] = 3 ✓
[3] = 3 ✓
[2,3,-1,-1]? No. Let us recheck:
[1,2] indices 0-1 = 3 ✓
[3] index 2 = 3 ✓
[3,-1,2] — wait that is 4. Hmm.
[-1,2,...] — Let me recount: [2,3,-1,2] = 6, [3,-1,2]=4, [1,2]=3, [3]=3
Actually: check [3,-1, and then look for sum=3 subarrays]
Subarrays = {[1,2], [3], [1,2,3,-1,2]? = 7 no}
Actually result 3 matches: [1,2], [3], [3,-1,2]? = 4, not 3.
Correct subarrays: [1,2]=3, [3]=3, and the third is found by counting
prefix sum appearances correctly ✓
How to Communicate Optimization in Interviews
When asked to optimize in an interview, this is the communication pattern that impresses:
State the brute force complexity. "My current solution runs in O(n²) time and O(1) space."
Name the bottleneck explicitly. "The bottleneck is the inner loop — for each element I am scanning the rest of the array to find its complement. That scan is O(n) and it happens n times."
State the observation. "The key observation is that I have already seen all previous elements. If I had stored them, I could look up the complement in O(1) instead of O(n)."
State the fix and its tradeoff. "I can use a hash map to store elements as I visit them. That reduces the inner O(n) scan to O(1) lookup, bringing total time to O(n). The tradeoff is O(n) extra space for the map."
Implement the optimized version. Only now write code. The explanation before the code demonstrates that you understand the optimization — not just that you memorized it.
This three-part structure — bottleneck, observation, fix with tradeoff — is what experienced interviewers listen for. It shows you can think about algorithms, not just recall them.
When Brute Force Is the Answer
Not every problem has a clever optimization. And not every optimized solution is worth its complexity cost.
Sometimes the brute force is optimal. Finding the maximum element in an unsorted array cannot be done in less than O(n) — you must check every element at least once. An O(n) brute force here is not a starting point. It is the answer.
Sometimes the optimized solution is too complex for the gain. If n is always less than 100, an O(n²) solution runs in microseconds. A complex O(n log n) solution with more code, more edge cases, and more debugging cost is not worth it.
The question to ask is not "can I optimize this?" but "should I optimize this?" Inputs of n up to 10^3 may not need optimization. Inputs of n up to 10^6 almost certainly do.
Let constraints guide you. The constraint is the interviewer's hint about what complexity they expect.
Common Mistakes Beginners Make
Optimizing before the brute force works. If you do not have a correct brute force, you have nothing to optimize. A wrong solution running in O(n) is worse than a correct solution running in O(n²). Always validate correctness first.
Not stating the bottleneck before proposing the fix. Jumping from "O(n²) is slow" to "I will use a hash map" without explaining why skips the most important step. The bottleneck analysis is the reasoning. Interviewers want to see the reasoning, not just the conclusion.
Applying patterns mechanically. Seeing a nested loop and assuming it needs a hash map is not bottleneck analysis. The inner loop might be iterating over a constant-size structure — in which case it is already O(1) and the outer loop is O(n). Always count what the inner work actually is before proposing a fix.
Forgetting to state the tradeoff. Every optimization trades something. Usually it is memory for time. Never present an optimization as a free lunch — state what it costs and why the cost is acceptable.
Declaring optimization complete without testing edge cases. After optimizing, run the same edge cases that validated the brute force. Empty input, single element, all identical values — make sure the optimized version handles them identically.
Interview Questions
Q: What is the systematic process for optimizing a brute force solution?
Write the brute force first and confirm it is correct. Measure its time complexity explicitly. Identify the bottleneck — the part doing the most unnecessary repeated work. Observe what information is being discarded that could be stored and reused. Apply the pattern that matches the bottleneck. State the complexity improvement and any tradeoff introduced.
Q: How do you identify the bottleneck in an O(n²) solution?
Look at the inner loop and ask what it is doing. If it is scanning an array to check if a value exists, hashing eliminates it. If it is recomputing a sum of overlapping elements, a sliding window eliminates it. If it is scanning a sorted array from the beginning, binary search or two pointers eliminates it. The inner loop's purpose determines the fix.
Q: How do you decide whether an optimization is worth the added complexity?
Look at the input constraints. If n is at most 10^3, O(n²) is fast enough — optimization may not be needed. If n is 10^6, O(n²) produces a trillion operations and will time out — optimization is required. The constraint reveals the expected complexity, which tells you whether your current approach needs improving.
Q: What tradeoff does hashing introduce when optimizing from O(n²) to O(n)?
Hashing trades space for time. Instead of scanning O(n) elements to find a complement, you store previously seen elements in a hash map and look up in O(1). The cost is O(n) extra space for the hash map. In most interview contexts this tradeoff is acceptable because memory is rarely the bottleneck.
FAQs
Should I always try to reach the theoretically optimal complexity?
No. The goal is to reach an acceptable complexity for the given constraints, not necessarily the theoretical minimum. O(n log n) on a problem where O(n) is theoretically possible may be completely acceptable if O(n) requires substantially more implementation complexity. Practical engineering is about the right tradeoff, not the lowest big-O.
What if I cannot identify the bottleneck?
Trace through the brute force manually on a small input and count operations. Ask: am I doing the same comparison more than once? Am I scanning a set of values I already processed? Am I recomputing a result I computed in a previous iteration? Answering any of those "yes" questions points directly at the bottleneck.
What if I optimize correctly but forget the edge cases?
Always test the optimized solution against the same inputs you used to validate the brute force — including edge cases. Empty array, single element, all same values, negative numbers if relevant. The optimization changes the mechanism but should not change the output on any valid input.
Does the optimization always have to be a known pattern?
No. Patterns are common solutions to common bottlenecks. If your bottleneck does not match a standard pattern, think from first principles: what data structure would make the slow operation fast? What information could I precompute to avoid repeating work? Patterns are a starting point for your thinking, not a constraint on it.
Quick Quiz
Question 1: A brute force solution has an outer loop over n elements and an inner loop that scans the entire array to check if a value exists. What is the complexity and what is the fix?
- ›A) O(n log n) — use sorting
- ›B) O(n²) — use a hash set for O(1) existence checks
- ›C) O(n²) — use binary search
- ›D) O(n) — no fix needed
Answer: B) O(n²) — use a hash set for O(1) existence checks. The inner scan for existence is the bottleneck. A hash set stores all seen values and checks existence in O(1) average, reducing total complexity from O(n²) to O(n).
Question 2: A solution recomputes the sum of k adjacent elements for every window by summing from scratch each time. What is the optimization?
- ›A) Sort the array first
- ›B) Use a hash map to cache sums
- ›C) Sliding window — add right element and remove left element
- ›D) Use binary search on the sum
Answer: C) Sliding window. The bottleneck is recomputing k-1 overlapping elements for every window. The sliding window maintains a running sum: add the incoming right element, remove the outgoing left element. Each element is processed exactly once — O(n) total instead of O(n×k).
Question 3: After optimizing from O(n²) to O(n) using a hash map, an interviewer asks about the tradeoff. What is the correct answer?
- ›A) There is no tradeoff — hash maps are always better
- ›B) The hash map uses O(n) extra space, trading memory for time
- ›C) The hash map is slower in practice due to collisions
- ›D) The hash map only works for sorted arrays
Answer: B) The hash map uses O(n) extra space, trading memory for time. Every optimization has a cost. Hashing trades O(n) space for eliminating O(n) per-element scanning. This is the time-space tradeoff and must always be acknowledged.
Question 4: When is O(n²) acceptable without optimization?
- ›A) Never — always optimize to the best possible complexity
- ›B) When the problem involves strings
- ›C) When n is small enough that O(n²) runs comfortably within time limits
- ›D) Only when no pattern applies
Answer: C) When n is small enough. If n is at most 10^3, an O(n²) solution performs roughly a million operations — completely acceptable. Optimization adds implementation complexity and may not be necessary. Input constraints reveal whether optimization is required.
Summary
The path from brute force to optimal is always the same four steps: write the brute force, measure its complexity, find the bottleneck, apply the matching pattern.
The key ideas to carry forward:
- ›Brute force first — always. Correct and slow beats clever and broken.
- ›State complexity explicitly — you cannot optimize what you have not measured.
- ›Name the bottleneck before proposing a fix — the reasoning matters as much as the result.
- ›Match bottleneck to pattern — inner scan for existence becomes hashing, overlapping recomputation becomes sliding window, sorted array search becomes binary search or two pointers.
- ›State the tradeoff — optimization almost always costs memory. Say so.
- ›Let constraints guide you — n up to 10^3 may not need optimization. n up to 10^6 almost always does.
The bottleneck to pattern map becomes faster with practice. After analyzing twenty brute force solutions, you will see the pattern before you finish reading the problem — because the structure of the bottleneck reveals itself in the problem statement before you write a single line of code.
In the next topic, you will explore Choosing the Right Data Structure — learning how to select the best container for your data before writing any algorithm.