Skip to content
All essays
ArchitectureFebruary 22, 202520 min

LeetCode Problem Solving: Strategies, Patterns, and Practice Guide

Master LeetCode problem solving with proven strategies, common patterns, and effective practice techniques for interview success.

Ü
Ümit Uz
Mobile & Full Stack Developer

LeetCode has become the gold standard for technical interview preparation. This guide will teach you systematic approaches to tackle any problem, recognize common patterns, and develop effective practice habits that actually work.

The Problem-Solving Framework

Step 1: Understand the Problem (5-10 minutes)

Before writing any code, ensure you fully understand what's being asked.

javascript
// Problem: Two Sum
// Given an array of integers nums and an integer target,
// return indices of the two numbers such that they add up to target.

// Questions to ask yourself:
// 1. What is the input size?
// 2. Are there duplicates?
// 3. What if no solution exists?
// 4. Can we use the same element twice?
// 5. What about negative numbers?

Clarification Checklist:

  • Input constraints and edge cases
  • Output format (return value vs modify in-place)
  • Time and space complexity requirements
  • Can we assume valid input?

Step 2: Think of Examples (2-3 minutes)

Create test cases that cover different scenarios.

javascript
// Example cases for Two Sum:

// Case 1: Normal case
Input: nums = [2, 7, 11, 15], target = 9
Output: [0, 1]
Explanation: nums[0] + nums[1] = 2 + 7 = 9

// Case 2: Negative numbers
Input: nums = [-1, -2, -3, -4, -5], target = -8
Output: [2, 4]
Explanation: nums[2] + nums[4] = -3 + -5 = -8

// Case 3: Same element twice
Input: nums = [3, 3], target = 6
Output: [0, 1]

// Case 4: No solution
Input: nums = [1, 2, 3], target = 10
Output: [] or some indicator

Step 3: Brute Force Solution (5-10 minutes)

Start with the simplest approach, even if inefficient.

javascript
// Brute Force Two Sum - O(n²) time, O(1) space
function twoSumBrute(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 [];
}

Step 4: Optimize (10-15 minutes)

Identify inefficiencies and improve.

javascript
// Optimized Two Sum - O(n) time, O(n) space
function twoSum(nums, target) {
  const map = new Map();

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];

    if (map.has(complement)) {
      return [map.get(complement), i];
    }

    map.set(nums[i], i);
  }

  return [];
}

Step 5: Implement Clean Code (5-10 minutes)

Write production-quality code with proper variable names and comments.

javascript
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
function twoSum(nums, target) {
  const numToIndex = new Map();

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];

    if (numToIndex.has(complement)) {
      return [numToIndex.get(complement), i];
    }

    numToIndex.set(nums[i], i);
  }

  return []; // No solution found
}

Common Problem Patterns

Pattern 1: Sliding Window

Used for problems involving subarrays or substrings.

Fixed Size Window

javascript
// Maximum average subarray I
function findMaxAverage(nums, k) {
  let sum = 0;

  // Calculate first window
  for (let i = 0; i < k; i++) {
    sum += nums[i];
  }

  let maxSum = sum;

  // Slide window
  for (let i = k; i < nums.length; i++) {
    sum += nums[i] - nums[i - k];
    maxSum = Math.max(maxSum, sum);
  }

  return maxSum / k;
}

Variable Size Window

javascript
// Longest substring without repeating characters
function lengthOfLongestSubstring(s) {
  const charSet = new Set();
  let left = 0;
  let maxLength = 0;

  for (let right = 0; right < s.length; right++) {
    while (charSet.has(s[right])) {
      charSet.delete(s[left]);
      left++;
    }

    charSet.add(s[right]);
    maxLength = Math.max(maxLength, right - left + 1);
  }

  return maxLength;
}

// Minimum size subarray sum
function minSubArrayLen(target, nums) {
  let left = 0;
  let sum = 0;
  let minLength = Infinity;

  for (let right = 0; right < nums.length; right++) {
    sum += nums[right];

    while (sum >= target) {
      minLength = Math.min(minLength, right - left + 1);
      sum -= nums[left];
      left++;
    }
  }

  return minLength === Infinity ? 0 : minLength;
}

Pattern 2: Two Pointers

Used for sorted arrays or linked lists.

javascript
// Container with most water
function maxArea(height) {
  let left = 0;
  let right = height.length - 1;
  let maxArea = 0;

  while (left < right) {
    const width = right - left;
    const h = Math.min(height[left], height[right]);
    maxArea = Math.max(maxArea, width * h);

    if (height[left] < height[right]) {
      left++;
    } else {
      right--;
    }
  }

  return maxArea;
}

// Three sum
function threeSum(nums) {
  nums.sort((a, b) => a - b);
  const result = [];

  for (let i = 0; i < nums.length - 2; i++) {
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    let left = i + 1;
    let right = nums.length - 1;

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]]);

        while (left < right && nums[left] === nums[left + 1]) left++;
        while (left < right && nums[right] === nums[right - 1]) right--;

        left++;
        right--;
      } else if (sum < 0) {
        left++;
      } else {
        right--;
      }
    }
  }

  return result;
}

Pattern 3: Fast & Slow Pointers

Used for cycle detection in linked lists.

javascript
// Linked list cycle detection
function hasCycle(head) {
  let slow = head;
  let fast = head;

  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;

    if (slow === fast) {
      return true;
    }
  }

  return false;
}

// Find cycle start node
function detectCycle(head) {
  let slow = head;
  let fast = head;

  // Detect cycle
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;

    if (slow === fast) {
      // Find start of cycle
      slow = head;
      while (slow !== fast) {
        slow = slow.next;
        fast = fast.next;
      }
      return slow;
    }
  }

  return null;
}

Pattern 4: Merge Intervals

javascript
// Merge intervals
function merge(intervals) {
  if (intervals.length <= 1) return intervals;

  // Sort by start time
  intervals.sort((a, b) => a[0] - b[0]);

  const result = [intervals[0]];

  for (let i = 1; i < intervals.length; i++) {
    const last = result[result.length - 1];
    const current = intervals[i];

    if (current[0] <= last[1]) {
      // Overlapping intervals, merge them
      last[1] = Math.max(last[1], current[1]);
    } else {
      result.push(current);
    }
  }

  return result;
}

// Insert interval
function insert(intervals, newInterval) {
  const result = [];
  let i = 0;
  const n = intervals.length;

  // Add all intervals before newInterval
  while (i < n && intervals[i][1] < newInterval[0]) {
    result.push(intervals[i]);
    i++;
  }

  // Merge overlapping intervals
  while (i < n && intervals[i][0] <= newInterval[1]) {
    newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
    newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
    i++;
  }
  result.push(newInterval);

  // Add remaining intervals
  while (i < n) {
    result.push(intervals[i]);
    i++;
  }

  return result;
}

Pattern 5: Cyclic Sort

javascript
// Find missing number
function missingNumber(nums) {
  let i = 0;

  while (i < nums.length) {
    const correct = nums[i];

    if (correct < nums.length && nums[i] !== nums[correct]) {
      [nums[i], nums[correct]] = [nums[correct], nums[i]];
    } else {
      i++;
    }
  }

  for (let i = 0; i < nums.length; i++) {
    if (nums[i] !== i) {
      return i;
    }
  }

  return nums.length;
}

// Find all missing numbers
function findDisappearedNumbers(nums) {
  const result = [];

  for (let i = 0; i < nums.length; i++) {
    const index = Math.abs(nums[i]) - 1;

    if (nums[index] > 0) {
      nums[index] = -nums[index];
    }
  }

  for (let i = 0; i < nums.length; i++) {
    if (nums[i] > 0) {
      result.push(i + 1);
    }
  }

  return result;
}
javascript
// Search in rotated sorted array
function search(nums, target) {
  let left = 0;
  let right = nums.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (nums[mid] === target) {
      return mid;
    }

    // Left half is sorted
    if (nums[left] <= nums[mid]) {
      if (nums[left] <= target && target < nums[mid]) {
        right = mid - 1;
      } else {
        left = mid + 1;
      }
    }
    // Right half is sorted
    else {
      if (nums[mid] < target && target <= nums[right]) {
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }
  }

  return -1;
}

// Find minimum in rotated sorted array
function findMin(nums) {
  let left = 0;
  let right = nums.length - 1;

  while (left < right) {
    const mid = Math.floor((left + right) / 2);

    if (nums[mid] > nums[right]) {
      left = mid + 1;
    } else {
      right = mid;
    }
  }

  return nums[left];
}

Pattern 7: Top K Elements

javascript
// Kth largest element in an array
function findKthLargest(nums, k) {
  // Using min-heap approach
  const minHeap = [];

  for (const num of nums) {
    minHeap.push(num);
    minHeap.sort((a, b) => a - b);

    if (minHeap.length > k) {
      minHeap.shift();
    }
  }

  return minHeap[0];
}

// Top K frequent elements
function topKFrequent(nums, k) {
  const frequency = {};

  for (const num of nums) {
    frequency[num] = (frequency[num] || 0) + 1;
  }

  const buckets = Array(nums.length + 1).fill(null).map(() => []);

  for (const [num, freq] of Object.entries(frequency)) {
    buckets[freq].push(parseInt(num));
  }

  const result = [];

  for (let i = buckets.length - 1; i >= 0 && result.length < k; i--) {
    result.push(...buckets[i]);
  }

  return result.slice(0, k);
}

Pattern 8: K-way Merge

javascript
// Merge k sorted lists
function mergeKLists(lists) {
  const minHeap = [];

  // Add first node of each list to heap
  for (const list of lists) {
    if (list) {
      minHeap.push(list);
    }
  }

  // Create min-heap based on node values
  minHeap.sort((a, b) => a.val - b.val);

  const dummy = new ListNode(0);
  let current = dummy;

  while (minHeap.length > 0) {
    // Extract minimum
    const node = minHeap.shift();
    current.next = node;
    current = current.next;

    // Add next node from same list
    if (node.next) {
      minHeap.push(node.next);
      minHeap.sort((a, b) => a.val - b.val);
    }
  }

  return dummy.next;
}

Problem Categories and Strategies

Array Problems

Key Techniques:

  • Hash maps for frequency counting
  • Two pointers for sorted arrays
  • Sliding window for subarray problems
  • Prefix sums for range queries
javascript
// Product of array except self
function productExceptSelf(nums) {
  const n = nums.length;
  const result = new Array(n).fill(1);

  // Calculate left products
  let leftProduct = 1;
  for (let i = 0; i < n; i++) {
    result[i] = leftProduct;
    leftProduct *= nums[i];
  }

  // Calculate right products and final result
  let rightProduct = 1;
  for (let i = n - 1; i >= 0; i--) {
    result[i] *= rightProduct;
    rightProduct *= nums[i];
  }

  return result;
}

String Problems

Key Techniques:

  • Two pointers for palindrome/checking
  • Sliding window for substring problems
  • Hash maps for character counting
  • Regular expressions for pattern matching
javascript
// Longest palindromic substring
function longestPalindrome(s) {
  let start = 0;
  let maxLength = 1;

  function expandAroundCenter(left, right) {
    while (left >= 0 && right < s.length && s[left] === s[right]) {
      left--;
      right++;
    }

    return right - left - 1;
  }

  for (let i = 0; i < s.length; i++) {
    const len1 = expandAroundCenter(i, i);     // Odd length
    const len2 = expandAroundCenter(i, i + 1); // Even length

    const len = Math.max(len1, len2);

    if (len > maxLength) {
      maxLength = len;
      start = i - Math.floor((len - 1) / 2);
    }
  }

  return s.substring(start, start + maxLength);
}

Tree Problems

Key Techniques:

  • Recursive DFS for most problems
  • BFS for level-order traversal
  • Divide and conquer for BST problems
  • Morris traversal for O(1) space
javascript
// Binary tree level order traversal
function levelOrder(root) {
  if (!root) return [];

  const result = [];
  const queue = [root];

  while (queue.length > 0) {
    const level = [];
    const size = queue.length;

    for (let i = 0; i < size; i++) {
      const node = queue.shift();
      level.push(node.val);

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    result.push(level);
  }

  return result;
}

// Lowest common ancestor in BST
function lowestCommonAncestor(root, p, q) {
  if (!root) return null;

  if (root.val > p.val && root.val > q.val) {
    return lowestCommonAncestor(root.left, p, q);
  }

  if (root.val < p.val && root.val < q.val) {
    return lowestCommonAncestor(root.right, p, q);
  }

  return root;
}

Graph Problems

Key Techniques:

  • BFS for shortest path
  • DFS for connectivity and cycles
  • Topological sort for dependencies
  • Union-Find for connected components
javascript
// Course schedule II (topological sort)
function findOrder(numCourses, prerequisites) {
  const graph = new Map();
  const inDegree = new Array(numCourses).fill(0);

  // Build graph
  for (let i = 0; i < numCourses; i++) {
    graph.set(i, []);
  }

  for (const [course, prereq] of prerequisites) {
    graph.get(course).push(prereq);
    inDegree[prereq]++;
  }

  // Start with courses having no prerequisites
  const queue = [];
  for (let i = 0; i < numCourses; i++) {
    if (inDegree[i] === 0) {
      queue.push(i);
    }
  }

  const result = [];

  while (queue.length > 0) {
    const course = queue.shift();
    result.push(course);

    for (const prereq of graph.get(course)) {
      inDegree[prereq]--;

      if (inDegree[prereq] === 0) {
        queue.push(prereq);
      }
    }
  }

  return result.length === numCourses ? result : [];
}

Practice Strategy

Phase 1: Fundamentals (Weeks 1-4)

Focus: Basic patterns and data structures

Easy Problems:

  • Array: Two Sum, Contains Duplicate, Valid Anagram
  • String: Valid Palindrome, Reverse String, First Unique Character
  • Tree: Maximum Depth, Invert Binary Tree, Same Tree
  • Graph: Number of Islands, Clone Graph

Goal: Solve 50-75 easy problems

Phase 2: Intermediate Patterns (Weeks 5-8)

Focus: Recognizing and applying patterns

Medium Problems:

  • Array: 3Sum, Container With Most Water, Product of Array Except Self
  • String: Longest Substring Without Repeating Characters, Valid Parentheses
  • Tree: Binary Tree Level Order Traversal, Validate BST, LCA
  • Graph: Course Schedule, Clone Graph, Reconstruct Itinerary

Goal: Solve 75-100 medium problems

Phase 3: Advanced Topics (Weeks 9-12)

Focus: Complex patterns and optimization

Hard Problems:

  • DP: Median of Two Sorted Arrays, Trapping Rain Water
  • Tree: Binary Tree Maximum Path Sum, Serialize and Deserialize BST
  • Graph: Word Ladder, Alien Dictionary

Goal: Solve 25-50 hard problems

Daily Practice Schedule

Weekdays (1-2 hours):

  • 3-5 new problems (mix of easy/medium)
  • Review previous day's problems
  • Document patterns learned

Weekends (2-3 hours):

  • Mock interviews (time yourself)
  • Revise weak areas
  • Participate in weekly contests

Time Management During Practice

First Attempt

  • Easy: 10-15 minutes
  • Medium: 20-30 minutes
  • Hard: 30-45 minutes

If Stuck

  1. 15 minutes: Re-read problem, check edge cases
  2. 210 minutes: Try different approach
  3. 315 minutes: Look at hint or discussion
  4. 420 minutes: Check solution approach (not code)
  5. 530+ minutes: Study solution thoroughly

After Solving

  1. 1Analyze: Time and space complexity
  2. 2Optimize: Can you improve it?
  3. 3Document: Note the pattern
  4. 4Revisit: Solve again in 1 week

Common Mistakes to Avoid

1. Jumping to Coding Too Fast

Wrong: Start coding immediately after reading problem Right: Understand, plan, then code

2. Ignoring Edge Cases

Wrong: Only testing with given examples Right: Test with empty arrays, single elements, duplicates

3. Not Optimizing After Brute Force

Wrong: Stopping at first solution Right: Always think about optimization

4. Memorizing Solutions

Wrong: Memorizing code without understanding Right: Understanding patterns and approaches

5. Not Revisiting Problems

Wrong: Solving once and moving on Right: Revisit after 1 week, 1 month, 3 months

Interview Simulation

Mock Interview Format

javascript
// Problem: Implement LRU Cache

// Step 1: Clarify requirements
// - What's the capacity? Assume positive integer
// - What operations needed? get and put
// - Time complexity? O(1) for both operations

// Step 2: Design approach
// - Use hash map for O(1) access
// - Use doubly linked list for O(1) insertion/deletion
// - Maintain order of usage

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) {
      return -1;
    }

    // Move to end (most recently used)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);

    return value;
  }

  put(key, value) {
    // Update if key exists
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }

    // Remove least recently used if at capacity
    if (this.cache.size >= this.capacity) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }

    this.cache.set(key, value);
  }
}

Tracking Progress

Problem Log Template

javascript
{
  date: "2025-02-20",
  problem: "Two Sum",
  difficulty: "Easy",
  category: "Array",
  pattern: "Hash Map",
  timeToSolve: "8 minutes",
  attempts: 1,
  notes: "Remember to check complement before adding current element",
  revisitDate: "2025-02-27"
}

Metrics to Track

  1. 1Problems solved per week
  2. 2Average time to solution
  3. 3Pattern recognition rate
  4. 4First-try success rate
  5. 5Topics mastered vs. learning

Final Tips

  1. 1Consistency over intensity: 30 minutes daily > 5 hours weekly
  2. 2Quality over quantity: Understand deeply, don't rush
  3. 3Pattern recognition: Focus on recognizing when to use which approach
  4. 4Teach others: Explaining reinforces understanding
  5. 5Stay positive: Struggling is part of learning

Remember: The goal isn't just to solve problems, but to develop problem-solving thinking that transfers to any technical challenge. Good luck with your preparation!

Related essays

Next essay
Penetration Testing: Finding Security Vulnerabilities