How to Approach Problems
Staring at a blank screen is the most common experience beginners have with DSA problems — and it does not go away just from reading more theory. What makes experienced engineers different is not that they know more algorithms; it is that they have a repeatable process for converting any problem into a solution, even when they have never seen that exact problem before.
This page teaches you that process from multiple angles: the REACTO method, the dry-run technique, how to move from brute force to optimized, and how to handle the psychological challenges of live problem-solving.
The REACTO Method
REACTO is a six-step framework used by coding bootcamps and interview coaches worldwide. It gives you a concrete checklist to follow when you sit down with any problem:
Step | Letter | What to do |
|---|---|---|
1 | R — Repeat | Restate the problem in your own words to confirm understanding |
2 | E — Examples | Walk through the provided examples; create your own edge cases |
3 | A — Approach | Describe your strategy in plain English before writing any code |
4 | C — Code | Translate your approach into working code |
5 | T — Test | Manually trace through your code with the examples |
6 | O — Optimize | Analyze complexity; improve time/space if possible |
Step 1 — Read Carefully and Repeat
Before anything else, read the problem statement twice. Then restate it in your own words out loud (in an interview) or in a comment at the top of your code (when practicing alone). This does three things:
Confirms you understood what is being asked
Surfaces hidden assumptions you might have made
Gives the interviewer a chance to correct you before you go down the wrong path
Ask clarifying questions about constraints. These determine which approaches are valid:
What is the maximum input size? (determines acceptable complexity)
Can there be duplicates?
Are the values sorted, or in any particular order?
What should be returned if the input is empty?
Are integers always positive, or can they be negative?
Can I modify the input array, or must I preserve it?
// Problem: "Given an array of integers, return the two numbers // that add up to a target sum." // Before coding, clarify: // - Can there be duplicates? (e.g., [3, 3] with target 6) // - Will there always be exactly one solution? // - Can I use the same element twice? // - What range of values? (affects whether negative numbers matter) // - Return indices or values?
Step 2 — Work Through Examples
Examples are your most valuable tool. Work through every example in the problem statement by hand — slowly, on paper or a whiteboard, tracing each step. Then create your own examples, especially edge cases:
Empty input — what happens when the array is empty?
Single element — what happens with only one item?
All same values — does your logic still hold?
Already sorted / reverse sorted — matters for sorting algorithms
Negative numbers and zero — often breaks naive solutions
Very large or very small values — overflow issues in some languages
The answer is at the start / end / middle — tests boundary conditions
// Problem: Two Sum — find indices of two numbers that add to target
// Input: nums = [2, 7, 11, 15], target = 9
// Expected: [0, 1] (nums[0] + nums[1] = 2 + 7 = 9)
// Work through it by hand:
// i=0: nums[0]=2. Need 9-2=7. Have we seen 7? No. Record {2: 0}
// i=1: nums[1]=7. Need 9-7=2. Have we seen 2? YES at index 0. Return [0, 1]
// Edge cases to consider:
// nums = [], target = 0 -> return [] or throw?
// nums = [3], target = 6 -> no solution possible
// nums = [3, 3], target = 6 -> [0, 1] — same value, different indices
// nums = [-1, 10], target = 9 -> [0, 1] — negative numbers work fine
// nums = [0, 0], target = 0 -> [0, 1]Step 3 — Brute Force First, Then Optimize
A critical mindset shift: always start with the brute force solution. Beginners often freeze trying to find the optimal solution immediately. This is a mistake. The brute force solution:
Proves you understand the problem correctly
Gives you something to compare against when testing the optimized version
Sometimes the brute force IS the expected answer (constraints are small)
Often reveals the inefficiency that points toward the optimization
// Two Sum — Step 1: Brute Force
// Check every pair of elements
function twoSum_brute(nums, target) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) {
return [i, j]
}
}
}
return []
}
// Time: O(n^2) — two nested loops
// Space: O(1)
// Two Sum — Step 2: Identify the bottleneck
// The inner loop is asking: "have I seen (target - nums[i]) before?"
// That question can be answered in O(1) with a hash map!
// Two Sum — Step 3: Optimized
function twoSum_optimized(nums, target) {
const seen = new Map() // value -> index
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i]
if (seen.has(complement)) {
return [seen.get(complement), i]
}
seen.set(nums[i], i)
}
return []
}
// Time: O(n) — single pass
// Space: O(n) — hash map stores up to n entriesStep 4 — Write Pseudocode First
Before writing real code, write pseudocode — English-like descriptions of each step. This separates algorithm design (hard) from syntax (mechanical), letting you focus on one thing at a time:
// Problem: Find the longest consecutive sequence in an array
// Input: [100, 4, 200, 1, 3, 2] -> Output: 4 (sequence 1,2,3,4)
// PSEUDOCODE:
// 1. Put all numbers in a hash set for O(1) lookup
// 2. For each number in the set:
// a. Only start counting if it's the beginning of a sequence
// (i.e., num-1 is NOT in the set)
// b. From this starting number, count how far the sequence extends
// by checking num+1, num+2, etc.
// c. Update the global maximum length
// 3. Return the maximum length found
// Now translate to code:
function longestConsecutive(nums) {
const numSet = new Set(nums)
let maxLength = 0
for (const num of numSet) {
// Only start counting at sequence beginnings
if (!numSet.has(num - 1)) {
let current = num
let length = 1
while (numSet.has(current + 1)) {
current++
length++
}
maxLength = Math.max(maxLength, length)
}
}
return maxLength
}
// Time: O(n) — each number is visited at most twice
// Space: O(n) — the hash setStep 5 — The Dry-Run Technique
After writing your code, dry-run it — trace through the execution manually, updating a state table as you go. This catches 80% of bugs before you even run the code:
// Function to dry-run:
function longestConsecutive([100, 4, 200, 1, 3, 2]) { ... }
// Dry-run trace:
// numSet = {100, 4, 200, 1, 3, 2}
// maxLength = 0
// Iteration 1: num = 100
// numSet.has(99)? No -> start counting
// current=100, length=1
// numSet.has(101)? No -> stop
// maxLength = max(0, 1) = 1
// Iteration 2: num = 4
// numSet.has(3)? Yes -> skip (not a start)
// Iteration 3: num = 200
// numSet.has(199)? No -> start counting
// current=200, length=1
// numSet.has(201)? No -> stop
// maxLength = max(1, 1) = 1
// Iteration 4: num = 1
// numSet.has(0)? No -> start counting
// current=1, length=1
// numSet.has(2)? Yes -> current=2, length=2
// numSet.has(3)? Yes -> current=3, length=3
// numSet.has(4)? Yes -> current=4, length=4
// numSet.has(5)? No -> stop
// maxLength = max(1, 4) = 4
// Iterations 5-6: num=3 and num=2
// Both have predecessors in set -> skip
// Final: return maxLength = 4 ✓Step 6 — Test With Edge Cases
After dry-running, test your solution against:
Edge Case | Why it matters |
|---|---|
Empty array | Most functions crash or give wrong answers on empty input |
Single element | Off-by-one errors often appear with size-1 inputs |
All duplicates | Tests whether your logic handles repeated values |
Already sorted | Some algorithms have different behavior on sorted input |
Reverse sorted | Catches issues with max/min assumptions |
Negative numbers | Often breaks solutions that assume non-negative values |
Very large values | Integer overflow in languages with fixed-size integers |
Answer at first/last position | Tests boundary condition handling |
// Testing longestConsecutive with edge cases: console.log(longestConsecutive([])) // Expected: 0 console.log(longestConsecutive([1])) // Expected: 1 console.log(longestConsecutive([1, 1, 1])) // Expected: 1 (duplicates) console.log(longestConsecutive([1, 2, 3, 4])) // Expected: 4 (all consecutive) console.log(longestConsecutive([4, 3, 2, 1])) // Expected: 4 (reverse order) console.log(longestConsecutive([-3, -2, -1])) // Expected: 3 (negatives) console.log(longestConsecutive([100, 4, 200, 1, 3, 2])) // Expected: 4
0 1 1 4 4 3 4 <- all correct!
Thinking Out Loud (Crucial for Interviews)
In a live interview, silence is your enemy. Interviewers cannot give you hints or correct your direction if they do not know what you are thinking. Practice narrating your thought process:
"Let me first make sure I understand the problem... so we need to..."
"A naive approach would be to... which gives us O(n²)..."
"The bottleneck is this inner loop. If I could look up X in O(1), I could eliminate it..."
"I think a hash map would work here because..."
"Let me trace through the example to verify my logic..."
"One edge case I should handle is empty input because..."
Common Patterns to Recognize
After enough practice, you develop a catalogue of patterns. When you see the right keywords, a pattern fires instantly:
Problem keywords | Likely pattern / approach |
|---|---|
"find pair that sums to X" | Hash map (Two Sum pattern) |
"find all subsets / combinations" | Backtracking / recursion |
"longest/shortest subarray/substring" | Sliding window |
"kth largest/smallest" | Heap (priority queue) |
"validate parentheses / matching brackets" | Stack |
"find if path exists / connected components" | BFS or DFS on a graph |
"maximum profit / optimal choice at each step" | Greedy algorithm |
"count ways to do X / optimal value with constraints" | Dynamic programming |
"search in sorted array" | Binary search |
"serialize / deserialize a tree" | BFS level-order or preorder DFS |
The Problem-Solving Framework in Practice
Let us apply the full framework to a medium-difficulty problem from scratch:
// Problem: "Given a string s, find the length of the longest
// substring without repeating characters."
// Input: "abcabcbb" -> Output: 3 (substring "abc")
// Input: "bbbbb" -> Output: 1 (substring "b")
// Input: "pwwkew" -> Output: 3 (substring "wke")
// R — Restate:
// Find the maximum window size in which all characters are unique.
// E — Examples:
// "abcabcbb": a,b,c then a repeats -> window [1..3] = "abc" length 3
// "": empty -> 0
// "au": both unique -> 2
// A — Approach (brute force first):
// Check every possible substring -> O(n^3) — too slow
// Better: sliding window — maintain a window [left, right] where all chars unique
// - Expand right one char at a time
// - When a duplicate is found, shrink left until the duplicate is removed
// - Track maximum window size seen
// C — Code:
function lengthOfLongestSubstring(s) {
const seen = new Map() // char -> last index seen
let left = 0
let maxLen = 0
for (let right = 0; right < s.length; right++) {
const char = s[right]
// If we've seen this char in our current window, move left past it
if (seen.has(char) && seen.get(char) >= left) {
left = seen.get(char) + 1
}
seen.set(char, right)
maxLen = Math.max(maxLen, right - left + 1)
}
return maxLen
}
// T — Test:
// "abcabcbb":
// right=0,a: seen={a:0}, left=0, len=1
// right=1,b: seen={a:0,b:1}, left=0, len=2
// right=2,c: seen={a:0,b:1,c:2}, left=0, len=3
// right=3,a: a seen at 0 >= left(0), left=1, seen={a:3,b:1,c:2}, len=3
// right=4,b: b seen at 1 >= left(1), left=2, seen={a:3,b:4,c:2}, len=3
// right=5,c: c seen at 2 >= left(2), left=3, seen={a:3,b:4,c:5}, len=3
// right=6,b: b seen at 4 >= left(3), left=5, seen={a:3,b:6,c:5}, len=2
// right=7,b: b seen at 6 >= left(5), left=7, seen={a:3,b:7,c:5}, len=1
// Result: 3 ✓
// O — Optimize: Already O(n) time, O(min(n, charset)) space — optimal!When You Are Completely Stuck
Every engineer gets stuck. Here is a concrete sequence of prompts to unstick yourself:
Simplify the problem — can you solve it if the input is always sorted? If all values are positive? Build the simplified version first.
Draw it — visually represent the data. Many solutions become obvious on a diagram that were invisible in abstract form.
Try small examples by hand — use n=3 or n=4 and literally write out every step.
Name the operation you wish you had — "if only I had a function that finds X in O(1)..." — that function is probably a hash map, set, or tree.
Work backwards from the answer — what does the final answer look like? What information did you need to compute it?
Consider the constraints as hints — n ≤ 20 suggests exponential; n ≤ 1000 suggests O(n²); n ≤ 10^6 suggests O(n log n) or O(n).
Complexity as a Constraint Hint
Input size (n) | Target complexity | Likely approach |
|---|---|---|
n ≤ 10 | O(n!) or O(2^n) | Brute force, backtracking, all permutations |
n ≤ 20 | O(2^n) | Bitmask DP, backtracking with pruning |
n ≤ 100 | O(n^3) | Triple nested loops, Floyd-Warshall |
n ≤ 1,000 | O(n^2) | DP with 2D table, double nested loops |
n ≤ 100,000 | O(n log n) | Sorting, binary search, merge sort, heap |
n ≤ 1,000,000 | O(n) | Single pass, sliding window, two pointers |
n ≤ 10^9 | O(log n) | Binary search, math formula, fast exponentiation |
Building Your Practice Habit
Knowing the methodology is not enough — you must build the habit of applying it. Here is a concrete practice structure:
Read the problem statement (5 minutes max)
Clarify constraints and edge cases (2 minutes)
Think of an approach — brute force first (5 minutes)
Write pseudocode before actual code (5 minutes)
Code the solution (15–20 minutes)
Test with examples and edge cases (5 minutes)
Review — read the editorial even if you solved it, to learn optimal approaches
Summary
Use the REACTO method: Repeat, Examples, Approach, Code, Test, Optimize
Always start with brute force — it clarifies the problem and reveals the inefficiency
Write pseudocode before real code to separate algorithm design from syntax
Dry-run your solution on paper — trace variables step by step
Test explicitly against edge cases: empty, single, duplicates, negatives
Think out loud in interviews — silence is your worst enemy
Use constraint size as a hint for target complexity