DSAHow to Approach Problems

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

Note
In a real interview, spend roughly 5–10 minutes on steps R, E, and A before writing a single line of code. Interviewers are explicitly evaluating your thought process, not just the final answer. Jumping straight to code is one of the most common interview mistakes.
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?

JS
// 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

JS
// 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

JS
// 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 entries
Tip
The optimization insight almost always comes from asking: "What redundant work am I doing?" In Two Sum, the inner loop was re-scanning elements we had already examined. Caching past results in a hash map eliminates redundant work.
Step 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:

JS
// 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 set
Step 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:

JS
// 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 [1]

Off-by-one errors often appear with size-1 inputs

All duplicates [5,5,5]

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

JS
// 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..."

Tip
Even if you are stuck, narrating what you DO know ("I can see this needs some kind of linear scan...") keeps the interviewer engaged and often prompts them to give you a useful hint. Silence with a blank screen is the worst outcome.
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:

JS
// 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:

  1. Simplify the problem — can you solve it if the input is always sorted? If all values are positive? Build the simplified version first.

  2. Draw it — visually represent the data. Many solutions become obvious on a diagram that were invisible in abstract form.

  3. Try small examples by hand — use n=3 or n=4 and literally write out every step.

  4. 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.

  5. Work backwards from the answer — what does the final answer look like? What information did you need to compute it?

  6. 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

Warning
Do not try to jump to the optimal solution immediately. Many interviewers care more about seeing a clear thought process than seeing the perfect answer instantly. Brute force first, then optimize systematically.
Building Your Practice Habit

Knowing the methodology is not enough — you must build the habit of applying it. Here is a concrete practice structure:

  1. Read the problem statement (5 minutes max)

  2. Clarify constraints and edge cases (2 minutes)

  3. Think of an approach — brute force first (5 minutes)

  4. Write pseudocode before actual code (5 minutes)

  5. Code the solution (15–20 minutes)

  6. Test with examples and edge cases (5 minutes)

  7. Review — read the editorial even if you solved it, to learn optimal approaches

After each problem
Write a one-sentence note: "This was a sliding window problem because the problem asked for a contiguous subarray/substring with a condition." Build your own pattern catalogue. After 50 problems, you will have a reference guide built from your own experience.
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

Up next
The next page presents the full **DSA Roadmap** — a structured learning path from beginner to interview-ready, with time estimates for each phase.