Published on

My LeetCode 41 Obsession: A Deep Dive into the First Missing Positive Puzzle

Authors

LeetCode's "First Missing Positive" problem (number 41) has become my latest coding obsession. It's a deceptively simple puzzle that has consumed my thoughts, driving me to explore its intricacies and craft the most efficient TypeScript solution possible. This post isn't just about acing coding interviews (though that's a nice perk). It's about the pure joy of problem-solving, the thrill of optimization, and the elegant beauty of efficient code. And because I'm a bit of a math nerd, we'll even delve into the mathematical underpinnings of the solution.

The Problem: A Wolf in Sheep's Clothing

The problem presents an unsorted array of integers. Your task: find the smallest missing positive integer. Seems straightforward? Think again. The constraints of O(n)O(n) time and O(1)O(1) space complexity transform this seemingly simple task into a challenging algorithmic puzzle.

Initial Struggles: The Brute Force Trap

My initial attempts involved sorting (violating the O(n)O(n) time constraint) and hash sets (exceeding the O(1)O(1) space limit). Clearly, a more sophisticated approach was needed.

The Eureka Moment: In-Place Transformation

The key insight was realizing I could use the input array itself as a hash table. By rearranging elements, I could place the number xx at index x1x - 1 (if 1xn1 \le x \le n, where nn is the array length). This in-place transformation unlocks the path to an efficient solution.

Code Evolution: A TypeScript Saga

Here's the chronicle of my code's evolution, with detailed explanations and mathematical insights:

Version 1: The Naive Approach (with Redundant Swaps)

function firstMissingPositive(nums: number[]): number {
  const n = nums.length

  // Place each number in its correct position if it's in the range [1, n]
  for (let i = 0; i < n; i++) {
    while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] !== nums[i]) {
      const correctIndex = nums[i] - 1
      ;[nums[i], nums[correctIndex]] = [nums[correctIndex], nums[i]] // Swap
    }
  }

  // Find the first missing positive
  for (let i = 0; i < n; i++) {
    if (nums[i] !== i + 1) {
      return i + 1
    }
  }

  // If all numbers are in their correct positions, the missing number is n + 1
  return n + 1
}

This version, while functional, suffers from redundant swaps. The number of swaps in the worst-case scenario approaches O(n2)O(n^2), although the average case is closer to O(n)O(n).

Explanation:

  1. Rearrange the array: Each number nums[i] is placed at its "correct" index (nums[i] - 1) if it is within the range [1, n] and not already in the correct position.

  2. Identify the missing positive: After rearranging, the first index i where nums[i] !== i + 1 indicates that i + 1 is the missing positive integer.

  3. Return n + 1 if needed: If all numbers from 1 to n are in the correct positions, the missing number is n + 1.

Key Points:

  • In-place rearrangement: We modify the array itself to avoid using extra space.
  • Efficiency: Both the rearrangement and the final scan take O(n)O(n) time, ensuring optimal performance.

Version 2: Performance Boost (Eliminating Redundant Swaps)

function firstMissingPositive(nums: number[]): number {
  const n = nums.length
  let idx = 0

  // Rearrange numbers to their correct positions if possible
  while (idx < n) {
    const correctIdx = nums[idx] - 1
    if (correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx]) {
      // Swap without using a temporary variable
      ;[nums[idx], nums[correctIdx]] = [nums[correctIdx], nums[idx]]
    } else {
      idx++
    }
  }

  // Identify the first missing positive
  for (let i = 0; i < n; i++) {
    if (nums[i] !== i + 1) {
      return i + 1
    }
  }

  return n + 1
}

Explanation:

  1. Efficient Rearrangement:

    • The while loop swaps elements directly into their correct positions (nums[idx] - 1) only when the number is in the valid range [1, n] and not already in its correct position.
    • The if condition ensures invalid or already-correct numbers are skipped without unnecessary swaps.
  2. Direct Index Checking:

    • During the rearrangement phase, invalid swaps and redundant operations are avoided by checking the range and value of nums[idx].
  3. Memory-Efficient Swapping:

    • The use of destructuring ([nums[idx], nums[correctIdx]] = [nums[correctIdx], nums[idx]]) eliminates the need for a temporary variable, minimizing memory usage.
  4. Minimal Loops:

    • The algorithm uses only two linear passes:
      • One to rearrange the array in place.
      • Another to find the first missing positive.

Complexity Analysis:

  • Time Complexity:
    • The while loop and the for loop both run in O(n)O(n), making the total time complexity O(n)O(n).
  • Space Complexity:
    • The algorithm operates in-place without using any auxiliary data structures, so the space complexity is O(1)O(1).

Why This Code Is Optimal:

  • Performance: The streamlined while loop ensures minimal redundant operations, maximizing speed.
  • Memory Efficiency: The use of in-place swapping and avoidance of extra variables ensures the lowest possible memory footprint.

Version 3: The Optimized for Memory (Minimal Space Usage)

function firstMissingPositive(nums: number[]): number {
  const n = nums.length
  let idx = 0

  while (idx < n) {
    const correctIdx = nums[idx] - 1

    if (correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx]) {
      nums[idx] = nums[correctIdx]
      nums[correctIdx] = correctIdx + 1
    } else {
      idx++
    }
  }

  for (let idx = 0; idx < n; idx++) {
    if (nums[idx] !== idx + 1) {
      return idx + 1
    }
  }

  return nums.length + 1
}

Key Memory Optimizations:

  1. In-Place Updates with Minimal Temporary Variables:

    • Instead of using a temp variable for swapping, this code directly modifies the array with nums[idx] = nums[correctIdx]; nums[correctIdx] = correctIdx + 1.
    • This eliminates the need for a temporary variable, reducing the memory overhead by a constant amount.
  2. Fewer Temporary Values in the Loop:

    • The code calculates correctIdx directly in the loop, avoiding the need to store additional intermediate variables outside the core logic.
    • Fewer variable assignments mean slightly lower memory usage per iteration.
  3. Single Pass for Rearrangement:

    • Unlike the first code that uses nested conditions in the while loop, this implementation completes swaps or advances the index in a more streamlined manner, reducing the runtime stack depth and memory usage.
    • The loop structure (while with a single increment or rearrangement) is more direct, requiring fewer auxiliary values.
  4. No Redundant Checks:

    • The checks for correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx] are concise and prevent unnecessary operations that could temporarily use stack or CPU cache memory.
    • This avoids the need to allocate extra resources for operations that won't contribute to the result.

Why This Makes a Difference in Memory:

  • Reduced Temporary Storage: By not relying on a temp variable and minimizing intermediate computations, the memory footprint is smaller.
  • Streamlined Execution: Fewer steps and simpler logic translate to less memory usage per operation.
  • Improved Cache Utilization: The algorithm's linear, predictable access pattern improves CPU cache performance, indirectly reducing memory overhead.

This solution's focus on direct in-place updates, minimal variable usage, and efficient memory access patterns makes it the best in terms of memory efficiency. While the savings are constant (O(1)O(1)), they are significant enough to register on competitive platforms like LeetCode, especially for large datasets.

Version 4: The Optimized Masterpiece (In-Place Swapping, No Temp)

function firstMissingPositive(nums: number[]): number {
  for (let i = 0; i < nums.length; i++) {
    while (nums[i] !== i + 1) {
      if (nums[i] <= 0 || nums[i] > nums.length || nums[i] === nums[nums[i] - 1]) {
        break
      }

      const temp = nums[i]
      nums[i] = nums[temp - 1]
      nums[temp - 1] = temp
    }
  }

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

  return nums.length + 1
}

This final version achieves both optimal performance and minimal memory usage through elegant in-place swapping without a temporary variable. The time complexity is now firmly O(n)O(n), and the space complexity is O(1)O(1), fulfilling all requirements. Mathematically, we're performing a kind of cyclic permutation within the array to place elements in their "correct" positions.

By adding a check (nums[i] === nums[nums[i] - 1]), we prevent unnecessary swaps. This significantly improves performance, bringing the time complexity closer to the desired O(n)O(n).

Key Differences in Implementation:

  1. Avoiding Redundant Swaps:

    • The provided code explicitly checks if nums[i] === nums[nums[i] - 1] before performing the swap. This avoids unnecessary swaps when the number is already in the correct position or duplicates exist.
    • In the first implementation, this check is omitted, potentially leading to redundant swaps even when unnecessary. Each additional swap adds overhead, especially for larger arrays.
  2. Direct Index Comparison:

    • The provided code uses while (nums[i] !== i + 1) to ensure a number is repeatedly swapped into its correct position until it is correctly placed or an invalid condition is met. This eliminates unnecessary loop iterations.
    • The first code doesn't explicitly compare the number to its intended index within the loop condition. It may perform more operations in cases where numbers need minimal movement.
  3. Optimized Conditional Checks:

    • By combining conditions in a single if block (e.g., nums[i] <= 0 || nums[i] > nums.length || nums[i] === nums[nums[i] - 1]), the code avoids additional overhead from unnecessary checks or computations.

Why This Matters in LeetCode:

  • Runtime Consistency:

    • LeetCode's runtime measurements can be sensitive to unnecessary operations, such as redundant swaps or extra comparisons.
    • The provided code minimizes these operations, leading to faster runtime on average.
  • Cache Efficiency:

    • Fewer swaps and simpler loop logic result in better CPU cache utilization. Modern processors benefit from predictable and streamlined access patterns, which the provided code exhibits.

Results

Here's the summary presented in a table format:

AttemptRuntimeMemory UsageNotes
4 (Optimized)1 ms58.8 MBBest balance of performance and memory.
3 (Best Memory)2 ms58.5 MBSlightly slower but lower memory usage.
2 (Best Performance)1 ms58.9 MBFaster runtime.
1 (Initial Attempt)3 ms59 MBSlowest and highest memory usage.

This table highlights the trade-offs and the optimality of the final solution.

The summary indicates that the attempt for the best performance and memory is indeed the most optimized, achieving:

  • 1 ms runtime: Matching the fastest result in terms of performance.
  • 58.9 MB memory usage: Slightly higher than the "best memory" clone but lower than other attempts.

Analysis:

  1. Runtime:

    • The 1 ms runtime shows the optimized approach eliminates redundant checks and unnecessary swaps.
    • Consistent time complexity of O(n)O(n) ensures scalability for larger datasets.
  2. Memory:

    • 58.9 MB is competitive, showing that the memory footprint is low despite the fast execution. This slight increase over "best memory" (58.5 MB) might be due to differences in destructuring or engine-specific optimizations.
  3. Trade-offs:

    • The "best memory" solution slightly sacrifices runtime for lower memory usage.
    • The "best performance" solution focuses more on speed but uses marginally more memory.

Conclusion:

The optimized solution strikes a good balance:

  • 1 ms runtime is as fast as the best-performing code.
  • 58.9 MB memory usage is close to the best-memory result, with negligible overhead.

It's a well-rounded choice, especially for scenarios where both performance and memory are critical.

Mathematical Underpinnings: Cyclic Permutations and Pigeonhole Principle

The core idea revolves around the concept of cyclic permutations. We aim to create a cycle where each number xx is placed at index x1x - 1. The while loop effectively traverses these cycles, ensuring each element finds its designated spot.

The Pigeonhole Principle subtly plays a role here. Since we have nn possible positions (indices 00 to n1n-1) and we are looking for a missing positive integer within the range [1,n+1][1, n+1], if all numbers from 1 to nn are present, the missing number must be n+1n + 1.

The Obsession Continues...

My fascination with LeetCode 41 remains. I'm constantly seeking further optimizations and deeper insights. Join me on this quest! Share your thoughts, your own solutions, and let's explore the elegant intersection of mathematics and algorithms together.