← Back to DevBytes

Solving Jump Game in JavaScript: Step-by-Step Guide

Introduction to the Jump Game Problem

The Jump Game is one of the most popular algorithmic problems you'll encounter in coding interviews and competitive programming. It tests your understanding of arrays, greedy algorithms, and dynamic programming. In this tutorial, we'll walk through the problem step by step, explore multiple solution approaches in JavaScript, and discuss best practices to help you master it.

What Is the Jump Game?

Given an array of non-negative integers, each element represents the maximum number of steps you can jump forward from that position. Starting at index 0, your goal is to determine whether you can reach the last index of the array.

For example, given the array [2, 3, 1, 1, 4], you start at index 0 where the value is 2. You can jump to index 1 or index 2. If you jump to index 1 (value 3), you can then jump up to 3 steps forward, easily reaching the last index. So the answer is true.

However, for the array [3, 2, 1, 0, 4], no matter how you jump, you'll get stuck at index 3 where the value is 0, making it impossible to reach the last index. The answer is false.

Why the Jump Game Matters

The Jump Game is more than just an interview question. It models real-world scenarios where you need to determine reachability under constraints. Some practical applications include:

From an algorithmic perspective, the Jump Game teaches you how to think greedily and optimize solutions that might initially seem to require dynamic programming. This skill transfers to many other problems involving arrays and reachability.

Understanding the Problem Constraints

Before diving into solutions, let's clarify the problem constraints that typically apply:

When the array has only one element, you're already at the last index, so the answer is always true.

Approach 1: Brute Force Recursion

The most intuitive approach is to try every possible jump from each position and see if any path leads to the end. This is a recursive backtracking solution.

function canJumpBruteForce(nums) {
  function recurse(index) {
    // Base case: reached the last index
    if (index >= nums.length - 1) {
      return true;
    }
    
    // Try every possible jump from current position
    const maxJump = nums[index];
    for (let jump = 1; jump <= maxJump; jump++) {
      if (recurse(index + jump)) {
        return true;
      }
    }
    
    return false;
  }
  
  return recurse(0);
}

console.log(canJumpBruteForce([2, 3, 1, 1, 4])); // true
console.log(canJumpBruteForce([3, 2, 1, 0, 4])); // false

While this solution works for small inputs, it has an exponential time complexity of O(n^n) in the worst case. For arrays with more than 30 or 40 elements, this approach becomes impractical. We need something better.

Approach 2: Dynamic Programming with Memoization

We can improve the brute force solution by caching results for indices we've already computed. This technique, called memoization, prevents redundant calculations.

function canJumpMemo(nums) {
  const memo = new Array(nums.length).fill(null);
  
  function recurse(index) {
    if (index >= nums.length - 1) {
      return true;
    }
    
    if (memo[index] !== null) {
      return memo[index];
    }
    
    const maxJump = nums[index];
    for (let jump = 1; jump <= maxJump; jump++) {
      if (recurse(index + jump)) {
        memo[index] = true;
        return true;
      }
    }
    
    memo[index] = false;
    return false;
  }
  
  return recurse(0);
}

console.log(canJumpMemo([2, 3, 1, 1, 4])); // true
console.log(canJumpMemo([3, 2, 1, 0, 4])); // false

This reduces the time complexity to O(n^2) since each index is computed at most once, and for each index we may iterate through up to n jumps. The space complexity is O(n) for the memoization array and the recursion stack. This is a solid solution, but we can do even better.

Approach 3: Dynamic Programming Bottom-Up

Instead of recursing from the start, we can work backward from the last index. We mark the last position as reachable, then for each preceding index, we check if it can reach any position that's already marked as reachable.

function canJumpBottomUp(nums) {
  const n = nums.length;
  const reachable = new Array(n).fill(false);
  reachable[n - 1] = true;
  
  for (let i = n - 2; i >= 0; i--) {
    const maxJump = Math.min(nums[i], n - 1 - i);
    for (let j = 1; j <= maxJump; j++) {
      if (reachable[i + j]) {
        reachable[i] = true;
        break;
      }
    }
  }
  
  return reachable[0];
}

console.log(canJumpBottomUp([2, 3, 1, 1, 4])); // true
console.log(canJumpBottomUp([3, 2, 1, 0, 4])); // false

This approach also runs in O(n^2) time and O(n) space, but it avoids recursion overhead and stack overflow risks for large inputs. However, there's an even more elegant and efficient solution.

Approach 4: Greedy Algorithm (Optimal Solution)

The greedy approach is the most efficient way to solve the Jump Game. The key insight is to track the farthest index we can reach as we iterate through the array. If at any point our current index exceeds the farthest reachable index, we know we can't proceed.

function canJump(nums) {
  let maxReach = 0;
  const n = nums.length;
  
  for (let i = 0; i < n; i++) {
    // If current index is beyond the farthest we can reach, we're stuck
    if (i > maxReach) {
      return false;
    }
    
    // Update the farthest index we can reach from here
    maxReach = Math.max(maxReach, i + nums[i]);
    
    // Early exit: if we can already reach the last index
    if (maxReach >= n - 1) {
      return true;
    }
  }
  
  return true;
}

console.log(canJump([2, 3, 1, 1, 4])); // true
console.log(canJump([3, 2, 1, 0, 4])); // false
console.log(canJump([0])); // true
console.log(canJump([0, 1])); // false

This solution runs in O(n) time and uses O(1) extra space, making it the optimal approach. Let's trace through an example to understand how it works.

Tracing the Greedy Algorithm

Consider the array [2, 3, 1, 1, 4]:

Now consider [3, 2, 1, 0, 4]:

Variant: Jump Game II (Minimum Jumps)

A common follow-up problem asks for the minimum number of jumps needed to reach the last index, assuming it's always reachable. This variant also uses a greedy approach but tracks jump boundaries.

function minJumps(nums) {
  const n = nums.length;
  if (n <= 1) return 0;
  
  let jumps = 0;
  let currentEnd = 0;
  let farthest = 0;
  
  for (let i = 0; i < n - 1; i++) {
    farthest = Math.max(farthest, i + nums[i]);
    
    // When we reach the end of the current jump range
    if (i === currentEnd) {
      jumps++;
      currentEnd = farthest;
      
      // Early exit if we can already reach the end
      if (currentEnd >= n - 1) {
        break;
      }
    }
  }
  
  return jumps;
}

console.log(minJumps([2, 3, 1, 1, 4])); // 2
console.log(minJumps([2, 3, 0, 1, 4])); // 2

The idea here is to think in terms of "levels." Each jump defines a range of indices you can reach. When you exhaust the current range, you increment the jump count and set the new range to the farthest point reachable from the previous range.

Best Practices for Solving Jump Game Problems

1. Always Start with the Greedy Insight

Before reaching for dynamic programming, ask yourself whether a greedy approach works. The Jump Game is a classic example where tracking a single variable (maxReach) eliminates the need for complex state management.

2. Handle Edge Cases Explicitly

Always test your solution against edge cases:

// Edge cases to consider
console.log(canJump([0]));        // true - already at the end
console.log(canJump([1]));        // true - already at the end
console.log(canJump([0, 1]));     // false - stuck at index 0
console.log(canJump([1, 0, 1]));  // false - stuck at index 1
console.log(canJump([5, 0, 0, 0, 0, 0])); // true - can jump directly to end

3. Use Early Exit Conditions

In the greedy solution, we exit early when maxReach >= n - 1. This optimization can save significant iterations, especially when the first element allows a direct jump to the end.

4. Avoid Unnecessary Array Allocations

The greedy approach uses O(1) space, which is a major advantage over DP solutions that require O(n) space. In memory-constrained environments or when processing large datasets, this difference is significant.

5. Write Clear, Self-Documenting Code

Use descriptive variable names like maxReach instead of m. This makes your code easier to understand and maintain, especially in interview settings where communication matters as much as correctness.

function canJumpReadable(nums) {
  const lastIndex = nums.length - 1;
  let farthestReachableIndex = 0;
  
  for (let currentIndex = 0; currentIndex < nums.length; currentIndex++) {
    if (currentIndex > farthestReachableIndex) {
      return false;
    }
    
    farthestReachableIndex = Math.max(
      farthestReachableIndex,
      currentIndex + nums[currentIndex]
    );
    
    if (farthestReachableIndex >= lastIndex) {
      return true;
    }
  }
  
  return true;
}

Performance Comparison

Here's a summary of the time and space complexities for each approach we discussed:

For an array of 10,000 elements, the brute force solution would never finish, the DP solutions would take noticeable time, and the greedy solution would complete almost instantly.

Common Mistakes to Avoid

Confusing Maximum Jump with Exact Jump

A frequent mistake is treating the array value as the exact jump distance rather than the maximum. Remember, if nums[i] = 3, you can jump 1, 2, or 3 steps forward, not just 3.

Forgetting the Zero Case

When nums[0] = 0 and the array has more than one element, you're immediately stuck. Make sure your solution handles this correctly.

Off-by-One Errors in Loop Conditions

In the minimum jumps variant, the loop runs to n - 2, not n - 1, because reaching the last index doesn't require another jump. Pay close attention to these boundary conditions.

Testing Your Solution

Here's a comprehensive test suite you can use to validate your implementation:

function testCanJump() {
  const testCases = [
    { input: [2, 3, 1, 1, 4], expected: true },
    { input: [3, 2, 1, 0, 4], expected: false },
    { input: [0], expected: true },
    { input: [1, 0, 1, 0], expected: false },
    { input: [2, 0, 0], expected: true },
    { input: [1, 1, 1, 1], expected: true },
    { input: [0, 2, 3], expected: false },
    { input: [1, 2, 3], expected: true },
    { input: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], expected: true },
  ];
  
  testCases.forEach(({ input, expected }, index) => {
    const result = canJump(input);
    const status = result === expected ? 'PASS' : 'FAIL';
    console.log(`Test ${index + 1}: ${status} | Input: [${input}] | Expected: ${expected}, Got: ${result}`);
  });
}

testCanJump();

Running this test suite helps ensure your solution handles all the tricky edge cases that interviewers love to throw at you.

Conclusion

The Jump Game is a deceptively simple problem that rewards careful thinking and algorithmic optimization. We started with a brute force recursive approach, improved it with memoization and bottom-up dynamic programming, and finally arrived at the optimal greedy solution that runs in linear time with constant space. The key takeaway is that whenever you encounter a reachability problem on arrays, consider whether a greedy strategy tracking the farthest reachable point can replace more complex state management. By understanding the trade-offs between these approaches and practicing with edge cases, you'll be well-equipped to tackle the Jump Game and its many variants in both interviews and real-world applications.

— Ad —

Google AdSense will appear here after approval

← Back to all articles