← Back to DevBytes

Jump Game: Multiple Solutions and Complexity Analysis

Introduction to the Jump Game

The Jump Game is a classic algorithmic problem that tests your ability to reason about array traversal, optimal substructure, and greedy decision-making. At its core, the problem asks: given an array of non-negative integers where each element represents your maximum jump distance from that position, can you determine whether you can reach the last index starting from the first?

This problem appears frequently in coding interviews at companies like Google, Amazon, and Facebook. It serves as an excellent lens for understanding how different algorithmic paradigms—backtracking, dynamic programming, and greedy algorithms—can be applied to the same problem, each with dramatically different performance characteristics.

Problem Statement

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

You are given an integer array nums of length n. The value at nums[i] represents the maximum number of steps you can jump forward from index i. You start at index 0. Return true if you can reach the last index (index n - 1), or false otherwise.

Example 1

Input: nums = [2, 3, 1, 1, 4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

Example 2

Input: nums = [3, 2, 1, 0, 4]
Output: false
Explanation: You will always arrive at index 3. Its maximum jump is 0,
so you can never reach index 4.

Why This Problem Matters

Beyond interview preparation, the Jump Game embodies concepts that translate directly to real-world systems:

Most importantly, this problem teaches you to question your assumptions. The brute-force solution is exponential, but with clever reasoning, you can achieve linear time complexity.

Solution 1: Brute Force via Backtracking

The most intuitive approach mimics how a player might explore possibilities: at each position, try every valid jump amount and see if any path leads to the goal. This is a depth-first search over the implicit graph formed by array indices.

Approach

Write a recursive function that takes the current index. If the current index is the last index, return true. Otherwise, for each possible jump from 1 up to nums[currentIndex], recurse to the new index. If any recursive call succeeds, propagate true upward.

JavaScript Implementation

/**
 * Brute-force backtracking solution for Jump Game
 * Time Complexity: O(2^n) in the worst case
 * Space Complexity: O(n) for the recursion stack
 */
function canJumpBruteForce(nums) {
    function backtrack(index) {
        // Base case: reached or passed the last index
        if (index >= nums.length - 1) {
            return true;
        }
        
        // If current position has zero jump, we're stuck
        if (nums[index] === 0) {
            return false;
        }
        
        // Try every possible jump length from 1 to nums[index]
        const maxJump = nums[index];
        for (let jump = 1; jump <= maxJump; jump++) {
            const nextIndex = index + jump;
            if (backtrack(nextIndex)) {
                return true;
            }
        }
        
        // No path found from this position
        return false;
    }
    
    return backtrack(0);
}

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

Python Implementation

def can_jump_brute_force(nums):
    def backtrack(index):
        # Base case: reached or passed the last index
        if index >= len(nums) - 1:
            return True
        
        # If stuck at a zero, no progress possible
        if nums[index] == 0:
            return False
        
        # Explore all possible jumps
        max_jump = nums[index]
        for jump in range(1, max_jump + 1):
            next_index = index + jump
            if backtrack(next_index):
                return True
        
        return False
    
    return backtrack(0)

# Example usage:
print(can_jump_brute_force([2, 3, 1, 1, 4]))  # True
print(can_jump_brute_force([3, 2, 1, 0, 4]))  # False

Complexity Analysis

This solution works for small inputs but times out on arrays larger than about 20–30 elements. We clearly need optimization.

Solution 2: Dynamic Programming — Top-Down Memoization

The backtracking approach recomputes the same subproblems many times. For example, if indices 3 and 5 both lead to index 7, the reachability of index 7 is evaluated twice. We can cache results in a memoization table to avoid redundant work.

Approach

Create a memoization array memo initialized to null (unknown). When you compute whether an index can reach the end, store the boolean result. Before recursing, check the memo table first.

JavaScript Implementation

/**
 * Top-down DP with memoization
 * Time Complexity: O(n²) — each index computed once, each computation
 *   may scan up to nums[i] possibilities
 * Space Complexity: O(n) for memo array and recursion stack
 */
function canJumpMemo(nums) {
    const n = nums.length;
    const memo = new Array(n).fill(null);
    
    function dp(index) {
        // Base case: at or beyond the last index
        if (index >= n - 1) {
            return true;
        }
        
        // Check memoization cache
        if (memo[index] !== null) {
            return memo[index];
        }
        
        // Try each possible jump
        const maxJump = nums[index];
        for (let jump = 1; jump <= maxJump; jump++) {
            const nextIndex = index + jump;
            if (dp(nextIndex)) {
                memo[index] = true;
                return true;
            }
        }
        
        // Mark as unreachable from this index
        memo[index] = false;
        return false;
    }
    
    return dp(0);
}

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

Python Implementation

def can_jump_memo(nums):
    n = len(nums)
    memo = [None] * n
    
    def dp(index):
        if index >= n - 1:
            return True
        
        if memo[index] is not None:
            return memo[index]
        
        max_jump = nums[index]
        for jump in range(1, max_jump + 1):
            next_index = index + jump
            if dp(next_index):
                memo[index] = True
                return True
        
        memo[index] = False
        return False
    
    return dp(0)

# Example usage:
print(can_jump_memo([2, 3, 1, 1, 4]))  # True
print(can_jump_memo([3, 2, 1, 0, 4]))  # False

Complexity Analysis

This is a significant improvement over brute force and will handle arrays of hundreds of elements comfortably. But we can still do better.

Solution 3: Dynamic Programming — Bottom-Up

Instead of recursion, we can build the solution iteratively from the end of the array toward the beginning. This eliminates recursion overhead and makes the algorithm purely iterative.

Approach

Create a boolean array dp of length n. Set dp[n-1] = true (the last index is always reachable from itself). Then iterate from n-2 down to 0. For each index i, check if any reachable index within i+1 to i+nums[i] is marked true in dp. If so, dp[i] = true.

JavaScript Implementation

/**
 * Bottom-up dynamic programming
 * Time Complexity: O(n²) worst case, O(n) best case
 * Space Complexity: O(n)
 */
function canJumpBottomUp(nums) {
    const n = nums.length;
    const dp = new Array(n).fill(false);
    
    // The last index is trivially reachable from itself
    dp[n - 1] = true;
    
    // Process from right to left
    for (let i = n - 2; i >= 0; i--) {
        const maxJump = nums[i];
        // Check if any position within our jump range is reachable
        for (let jump = 1; jump <= maxJump && i + jump < n; jump++) {
            if (dp[i + jump]) {
                dp[i] = true;
                break; // No need to check further
            }
        }
        // If no reachable position was found, dp[i] stays false
    }
    
    return dp[0];
}

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

Python Implementation

def can_jump_bottom_up(nums):
    n = len(nums)
    dp = [False] * n
    dp[n - 1] = True  # Last index is always reachable from itself
    
    # Process from right to left
    for i in range(n - 2, -1, -1):
        max_jump = nums[i]
        # Check positions within jump range
        for jump in range(1, max_jump + 1):
            next_idx = i + jump
            if next_idx < n and dp[next_idx]:
                dp[i] = True
                break  # No need to check further
    
    return dp[0]

# Example usage:
print(can_jump_bottom_up([2, 3, 1, 1, 4]))  # True
print(can_jump_bottom_up([3, 2, 1, 0, 4]))  # False

Complexity Analysis

This bottom-up approach is cleaner and avoids recursion limits, making it suitable for very large inputs where recursion depth might be a concern. However, we still have room for a breakthrough optimization.

Solution 4: Greedy Algorithm (Optimal)

Here's where the problem reveals its elegance. We don't need to track reachability for every index. Instead, we can maintain a single variable: the farthest reachable index so far. If at any point the current index surpasses this reachable range, we know we're stuck.

Key Insight

As we iterate from left to right, we greedily update the maximum index we can reach. If at index i, the farthest we've ever been able to reach is i itself (meaning we can't go further), and nums[i] is 0, we return false. Otherwise, we extend our reach. This works because the greedy choice—always pushing the boundary as far as possible—is optimal: you never need to deliberately not extend your maximum reach.

JavaScript Implementation

/**
 * Greedy solution — optimal
 * Time Complexity: O(n) — single pass through the array
 * Space Complexity: O(1) — only a few variables
 */
function canJumpGreedy(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
        if (i > maxReach) {
            return false;
        }
        
        // Update the farthest reachable index
        maxReach = Math.max(maxReach, i + nums[i]);
        
        // Early exit: if we can already reach the last index
        if (maxReach >= n - 1) {
            return true;
        }
    }
    
    // If we processed all indices without returning false,
    // check if we can reach the last index
    return maxReach >= n - 1;
}

// Example usage:
console.log(canJumpGreedy([2, 3, 1, 1, 4])); // true
console.log(canJumpGreedy([3, 2, 1, 0, 4])); // false

Python Implementation

def can_jump_greedy(nums):
    max_reach = 0
    n = len(nums)
    
    for i in range(n):
        # If current index is beyond our maximum reach
        if i > max_reach:
            return False
        
        # Greedily extend our reach
        max_reach = max(max_reach, i + nums[i])
        
        # Early termination if we can already reach the end
        if max_reach >= n - 1:
            return True
    
    return max_reach >= n - 1

# Example usage:
print(can_jump_greedy([2, 3, 1, 1, 4]))  # True
print(can_jump_greedy([3, 2, 1, 0, 4]))  # False

Complexity Analysis

This is the solution interviewers hope to see. It's elegant, fast, and memory-efficient. The greedy insight—that you should always push your maximum reach as far as possible—is provably correct because any "conservative" strategy that doesn't extend the boundary can only hurt your chances of reaching the end.

Extension: Jump Game II — Minimum Jumps

A natural follow-up problem asks: what is the minimum number of jumps required to reach the last index? This is Jump Game II, and it introduces new algorithmic challenges. Let's explore solutions for this variation.

Problem Statement (Jump Game II)

Given the same input array, return the minimum number of jumps to reach the last index. You can assume you can always reach the last index.

Greedy BFS-like Solution (Optimal)

The key insight: group indices by "levels" (jump count). At each level, determine the farthest you can reach in the next jump. When the current level's range can touch the last index, you've found the answer.

JavaScript Implementation

/**
 * Jump Game II: Minimum number of jumps
 * Greedy BFS-like approach
 * Time Complexity: O(n)
 * Space Complexity: O(1)
 */
function minJumps(nums) {
    const n = nums.length;
    
    // Edge case: already at the end
    if (n <= 1) return 0;
    
    let jumps = 0;
    let currentEnd = 0;   // End of the current jump range
    let farthest = 0;     // Farthest we can reach in the next jump
    
    for (let i = 0; i < n - 1; i++) {
        // Update the farthest reachable index from within the current range
        farthest = Math.max(farthest, i + nums[i]);
        
        // If we've reached the end of the current jump's range
        if (i === currentEnd) {
            jumps++;
            currentEnd = farthest;
            
            // If the new range covers the last index, we're done
            if (currentEnd >= n - 1) {
                break;
            }
        }
    }
    
    return jumps;
}

// Example usage:
console.log(minJumps([2, 3, 1, 1, 4])); // 2 (jump to index 1, then to 4)
console.log(minJumps([2, 3, 0, 1, 4])); // 3

Python Implementation

def min_jumps(nums):
    n = len(nums)
    if n <= 1:
        return 0
    
    jumps = 0
    current_end = 0
    farthest = 0
    
    for i in range(n - 1):
        # Extend the farthest reach within the current level
        farthest = max(farthest, i + nums[i])
        
        # When we hit the boundary of the current level
        if i == current_end:
            jumps += 1
            current_end = farthest
            
            # If we can already reach the last index
            if current_end >= n - 1:
                break
    
    return jumps

# Example usage:
print(min_jumps([2, 3, 1, 1, 4]))  # 2
print(min_jumps([2, 3, 0, 1, 4]))  # 3

How It Works

Imagine partitioning the array into "layers" based on jump count:

The variable currentEnd marks the boundary of the current jump level. When i hits currentEnd, you've exhausted the current level and must take another jump. The farthest variable continuously tracks the maximum boundary of the next level.

Complexity Analysis (Jump Game II)

Complexity Analysis Summary

Here's a side-by-side comparison of all approaches for Jump Game I:

Approach Time Complexity Space Complexity Notes
Brute Force Backtracking O(2n) O(n) Intractable for n > 20
Top-Down Memoization O(n²) O(n) Good for moderate n
Bottom-Up DP O(n²) O(n) Iterative, no recursion risk
Greedy O(n) O(1) Optimal in both time and space

Best Practices and Interview Tips

When Asked This Problem in an Interview

Common Pitfalls

Edge Cases to Test

// Single element array
canJumpGreedy([0]);        // true (already at the end)

// Immediate reach
canJumpGreedy([5, 0, 0]);  // true

// Zero trap
canJumpGreedy([1, 0, 1]);  // false (stuck at index 1)

// Large jumps
canJumpGreedy([10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); // true

// All ones
canJumpGreedy([1, 1, 1, 1, 1]); // true

Conclusion

The Jump Game problem suite is a masterclass in algorithmic optimization. Starting from an exponential backtracking approach, we progressively refined our solution through memoization, bottom-up dynamic programming, and finally arrived at an elegant O(n) greedy algorithm that uses constant space. The extension to Jump Game II demonstrates how greedy thinking can solve seemingly complex minimization problems with remarkable simplicity. When you encounter a problem involving sequential choices and optimal outcomes, always ask: can I track just the best possible state so far? More often than you might expect, the answer is yes—and the resulting code will be both beautiful and blazingly fast.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles