Lesson 12-Algorithm Design and Analysis

How to Approach Algorithm Design

understanding the problem (the most important step)

Before writing a single line of code, you must deeply understand the problem. I’ve seen engineers waste days implementing solutions to the wrong problem.

The problem understanding checklist:

  1. What are the inputs? (Type, format, constraints, edge cases)
  2. What are the outputs? (Type, format, constraints)
  3. What are the constraints? (Time limit, memory limit, input size)
  4. What are the edge cases? (Empty input, single element, duplicates, negative numbers, etc.)
  5. Can I restate the problem in my own words?

Example: Understanding the “Two Sum” problem

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

Let's understand:
- Input: nums = [2,7,11,15], target = 9
- Output: [0,1] (because nums[0] + nums[1] = 2 + 7 = 9)
- Constraints: Exactly one solution, can't use same element twice
- Edge cases: What if no solution? (Problem says exactly one solution)
- Can I restate? "Find two numbers that sum to target, return their indices"

The brute force approach (always start here):

// BRUTE FORCE: Try every pair
function twoSumBruteForce(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 null; // No solution found
}

// Analysis:
// Time complexity: O(n²) - nested loops
// Space complexity: O(1) - only using a few variables

Optimizing from brute force:

// OPTIMIZED: Use a hash map to find complement in O(1)
function twoSumOptimized(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 null;
}

// Analysis:
// Time complexity: O(n) - single pass
// Space complexity: O(n) - hash map stores n elements

The thought process:

  1. Start with brute force – Get a working solution
  2. Identify the bottleneck – The inner loop in brute force is O(n)
  3. Ask: Can I do better? – Can I find the complement in less than O(n)?
  4. Data structure choice – Hash map provides O(1) lookup
  5. Trade-off analysis – We traded space (O(n)) for time (O(n²) → O(n))

The algorithm design process

Algorithm design is iterative. Here’s a systematic process:

Step 1: Understand the problem (covered above)

Step 2: Work through examples by hand

Before coding, work through concrete examples. This helps you:

  • Understand the flow of the algorithm
  • Identify edge cases
  • Verify your understanding
// Problem: Reverse a linked list
// Example: 1 → 2 → 3 → null
// Expected output: 3 → 2 → 1 → null

// Working by hand:
// Initial: prev = null, curr = 1
// Iteration 1: next = 2, 1.next = null, prev = 1, curr = 2
// Iteration 2: next = 3, 2.next = 1, prev = 2, curr = 3
// Iteration 3: next = null, 3.next = 2, prev = 3, curr = null
// Return prev (3 → 2 → 1 → null)

function reverseLinkedList(head) {
  let prev = null;
  let curr = head;
  
  while (curr !== null) {
    const next = curr.next; // Store next
    curr.next = prev;      // Reverse pointer
    prev = curr;           // Move prev forward
    curr = next;           // Move curr forward
  }
  
  return prev;
}

Step 3: Write pseudocode

Before writing actual code, write pseudocode to clarify the algorithm.

Algorithm: Find Maximum Subarray (Kadane's Algorithm)
Input: Array of integers
Output: Maximum sum of contiguous subarray

Initialize:
  maxSoFar = nums[0]
  maxEndingHere = nums[0]

For each element nums[i] from index 1 to n-1:
  maxEndingHere = max(nums[i], maxEndingHere + nums[i])
  maxSoFar = max(maxSoFar, maxEndingHere)

Return maxSoFar

Step 4: Implement and test

function maxSubArray(nums) {
  if (nums.length === 0) return 0;
  
  let maxSoFar = nums[0];
  let maxEndingHere = nums[0];
  
  for (let i = 1; i < nums.length; i++) {
    maxEndingHere = Math.max(nums[i], maxEndingHere + nums[i]);
    maxSoFar = Math.max(maxSoFar, maxEndingHere);
  }
  
  return maxSoFar;
}

// Test cases:
console.log(maxSubArray([-2,1,-3,4,-1,2,1,-5,4])); // 6 (subarray [4,-1,2,1])
console.log(maxSubArray([1]));                            // 1
console.log(maxSubArray([5,4,-1,7,8]));                // 23
console.log(maxSubArray([-1]));                           // -1

Step 5: Analyze and optimize

After implementing, analyze:

  • Time complexity
  • Space complexity
  • Edge cases
  • Possible optimizations

Pattern recognition in algorithm problems

Many algorithm problems follow common patterns. Recognizing these patterns helps you solve problems faster.

Pattern 1: Sliding Window

Used when: Problem involves contiguous subarray or substring.

// PROBLEM: Maximum sum subarray of size k
// Pattern: Sliding window

function maxSumSubarray(arr, k) {
  if (arr.length < k) return null;
  
  // Compute sum of first window
  let windowSum = 0;
  for (let i = 0; i < k; i++) {
    windowSum += arr[i];
  }
  
  let maxSum = windowSum;
  
  // Slide window
  for (let i = k; i < arr.length; i++) {
    windowSum = windowSum - arr[i - k] + arr[i];
    maxSum = Math.max(maxSum, windowSum);
  }
  
  return maxSum;
}

// Time: O(n), Space: O(1)

Pattern 2: Two Pointers

Used when: Array is sorted, or problem involves pairs.

// PROBLEM: Remove duplicates from sorted array
// Pattern: Two pointers (slow/fast)

function removeDuplicates(nums) {
  if (nums.length === 0) return 0;
  
  let slow = 0;
  
  for (let fast = 1; fast < nums.length; fast++) {
    if (nums[fast] !== nums[slow]) {
      slow++;
      nums[slow] = nums[fast];
    }
  }
  
  return slow + 1;
}

// Time: O(n), Space: O(1)

Pattern 3: Fast and Slow Pointers

Used when: Detecting cycles in linked lists.

// PROBLEM: Detect cycle in linked list
// Pattern: Fast and slow pointers

function hasCycle(head) {
  if (!head || !head.next) return false;
  
  let slow = head;
  let fast = head;
  
  while (fast && fast.next) {
    slow = slow.next;          // Move 1 step
    fast = fast.next.next;     // Move 2 steps
    
    if (slow === fast) {
      return true; // Cycle detected!
    }
  }
  
  return false; // No cycle
}

Pattern 4: Divide and Conquer

Used when: Problem can be broken into smaller subproblems.

// PROBLEM: Merge sort
// Pattern: Divide and conquer

function mergeSort(arr) {
  // Base case
  if (arr.length <= 1) return arr;
  
  // Divide
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  
  // Conquer (merge)
  return merge(left, right);
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
  
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) {
      result.push(left[i]);
      i++;
    } else {
      result.push(right[j]);
      j++;
    }
  }
  
  return result.concat(left.slice(i)).concat(right.slice(j));
}

// Time: O(n log n), Space: O(n)
flowchart TD
    A[Problem] --> B{Recognize Pattern?}
    
    B -->|Yes| C[Apply Known Pattern]
    B -->|No| D[Try Brute Force]
    
    C --> E[Implement Solution]
    D --> F[Analyze Bottleneck]
    
    F --> G{Can Optimize?}
    G -->|Yes| H[Choose Better Algorithm]
    G -->|No| I[Accept Complexity]
    
    H --> E
    I --> E
    
    E --> J[Test & Verify]
    
    style A fill:#e1f5ff
    style J fill:#e1f5e1

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love