Facebook Pixel

384. Shuffle an Array

MediumDesignArrayMathRandomized
Leetcode Link

Problem Description

This problem asks you to implement a class that can shuffle an integer array randomly while maintaining the ability to reset it to its original state.

You need to create a Solution class with three methods:

  1. Constructor __init__(nums): Takes an integer array nums as input and initializes the object with this array.

  2. reset() method: Returns the array back to its original configuration (the state it was in when first passed to the constructor).

  3. shuffle() method: Returns a randomly shuffled version of the array. The key requirement is that all possible permutations of the array must have an equal probability of occurring.

The implementation uses the Fisher-Yates shuffle algorithm (also known as Knuth shuffle):

  • In the __init__ method, both the working array (self.nums) and a copy of the original array (self.original) are stored
  • The reset() method creates a fresh copy of the original array and returns it
  • The shuffle() method iterates through each position i in the array, randomly selects an index j from the range [i, n) where n is the array length, and swaps the elements at positions i and j

This algorithm ensures that each of the n! possible permutations has an equal probability of 1/n! of being generated, satisfying the "equally likely" requirement. The time complexity for shuffling is O(n) and space complexity is O(n) for storing the original array.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The core challenge is generating a truly random shuffle where each possible arrangement has equal probability. Think about how you'd shuffle a deck of cards by hand - you'd pick cards randomly and rearrange them.

The naive approach might be to randomly pick two positions and swap them, repeating this many times. However, this doesn't guarantee uniform distribution of all permutations.

The key insight comes from building the shuffled array position by position. For each position i, we need to decide which element should go there. To ensure fairness, we should give every remaining element (from position i to the end) an equal chance of being selected.

This leads us to the Fisher-Yates algorithm logic:

  • At position 0, any of the n elements could be first, so we pick from [0, n-1]
  • At position 1, we pick from the remaining unplaced elements [1, n-1]
  • At position i, we pick from [i, n-1]

By swapping the randomly chosen element with position i, we're essentially "placing" that element in its final shuffled position and then moving on to the next position.

The mathematical proof: For an array of length n, there are n! possible permutations. At each step i, we have (n-i) choices. So the total number of different outcomes is n × (n-1) × (n-2) × ... × 1 = n!, and since each choice is made uniformly at random, each permutation has probability 1/n!.

For the reset functionality, we simply need to maintain a copy of the original array. When reset is called, we restore from this preserved copy, allowing us to shuffle multiple times from the same starting point.

Solution Approach

The implementation consists of three main components:

1. Initialization (__init__ method):

def __init__(self, nums: List[int]):
    self.nums = nums
    self.original = nums.copy()
  • Store the input array in self.nums for manipulation
  • Create a deep copy of the original array in self.original to preserve the initial state
  • Using .copy() is crucial here - without it, both variables would reference the same list, and modifications to one would affect the other

2. Reset functionality (reset method):

def reset(self) -> List[int]:
    self.nums = self.original.copy()
    return self.nums
  • Create a fresh copy of the original array and assign it to self.nums
  • Return the reset array
  • Again, .copy() is essential to avoid modifying the preserved original

3. Shuffle implementation (shuffle method):

def shuffle(self) -> List[int]:
    for i in range(len(self.nums)):
        j = random.randrange(i, len(self.nums))
        self.nums[i], self.nums[j] = self.nums[j], self.nums[i]
    return self.nums
  • Iterate through each position i from 0 to n-1
  • For each position i, randomly select an index j from the range [i, len(self.nums))
    • random.randrange(i, len(self.nums)) returns a random integer in [i, n-1]
  • Swap elements at positions i and j using Python's tuple unpacking syntax
  • After the loop completes, return the shuffled array

Time and Space Complexity:

  • __init__: O(n) time and O(n) space for copying the array
  • reset(): O(n) time for copying, O(1) additional space
  • shuffle(): O(n) time for the single pass through the array, O(1) additional space
  • Overall space: O(n) for storing the original array copy

The algorithm guarantees uniform randomness because at each step i, every element from position i onward has an equal probability 1/(n-i) of being placed at position i.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with a small array [1, 2, 3] to understand how the Fisher-Yates shuffle works:

Initial Setup:

solution = Solution([1, 2, 3])
# self.nums = [1, 2, 3]
# self.original = [1, 2, 3] (a separate copy)

First Shuffle Call: Let's trace through one possible execution of shuffle():

# Initial: nums = [1, 2, 3]

# Iteration i=0:
# - Pick random j from range [0, 3): suppose j=2
# - Swap nums[0] with nums[2]
# - Result: nums = [3, 2, 1]

# Iteration i=1:
# - Pick random j from range [1, 3): suppose j=1
# - Swap nums[1] with nums[1] (no change)
# - Result: nums = [3, 2, 1]

# Iteration i=2:
# - Pick random j from range [2, 3): only option is j=2
# - Swap nums[2] with nums[2] (no change)
# - Result: nums = [3, 2, 1]

# Return: [3, 2, 1]

Reset Call:

solution.reset()
# Creates new copy from self.original
# self.nums = [1, 2, 3]
# Returns: [1, 2, 3]

Second Shuffle Call: Another possible execution starting fresh from [1, 2, 3]:

# Initial: nums = [1, 2, 3]

# Iteration i=0:
# - Pick random j from range [0, 3): suppose j=0
# - Swap nums[0] with nums[0] (no change)
# - Result: nums = [1, 2, 3]

# Iteration i=1:
# - Pick random j from range [1, 3): suppose j=2
# - Swap nums[1] with nums[2]
# - Result: nums = [1, 3, 2]

# Iteration i=2:
# - Pick random j from range [2, 3): only option is j=2
# - Swap nums[2] with nums[2] (no change)
# - Result: nums = [1, 3, 2]

# Return: [1, 3, 2]

Why This Works: For an array of length 3, there are 3! = 6 possible permutations:

  • [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]

The algorithm makes:

  • 3 choices for position 0 (any of the 3 elements)
  • 2 choices for position 1 (from the remaining 2 elements)
  • 1 choice for position 2 (the last element)

Total outcomes: 3 × 2 × 1 = 6, matching the number of permutations. Since each choice is random and independent, each permutation has probability 1/6.

Solution Implementation

1import random
2from typing import List
3
4
5class Solution:
6    def __init__(self, nums: List[int]):
7        """
8        Initialize the object with the array nums.
9        Store both the original array and a working copy.
10      
11        Args:
12            nums: The input array of integers to be shuffled
13        """
14        self.nums = nums
15        self.original = nums.copy()  # Store a copy of the original array for reset
16
17    def reset(self) -> List[int]:
18        """
19        Reset the array to its original configuration and return it.
20      
21        Returns:
22            The array in its original order
23        """
24        # Restore nums to the original configuration by copying from stored original
25        self.nums = self.original.copy()
26        return self.nums
27
28    def shuffle(self) -> List[int]:
29        """
30        Return a random shuffling of the array using Fisher-Yates algorithm.
31      
32        The algorithm iterates through the array and swaps each element
33        with a randomly selected element from the remaining unshuffled portion.
34      
35        Returns:
36            The array after random shuffling
37        """
38        # Fisher-Yates shuffle algorithm implementation
39        for i in range(len(self.nums)):
40            # Pick a random index from i to end of array (inclusive)
41            j = random.randrange(i, len(self.nums))
42            # Swap elements at positions i and j
43            self.nums[i], self.nums[j] = self.nums[j], self.nums[i]
44      
45        return self.nums
46
47
48# Your Solution object will be instantiated and called as such:
49# obj = Solution(nums)
50# param_1 = obj.reset()
51# param_2 = obj.shuffle()
52
1class Solution {
2    // Array to store the current state (can be shuffled)
3    private int[] nums;
4    // Array to store the original configuration
5    private int[] original;
6    // Random number generator for shuffling
7    private Random rand;
8
9    /**
10     * Constructor initializes the object with the integer array nums
11     * @param nums The input array to be shuffled
12     */
13    public Solution(int[] nums) {
14        this.nums = nums;
15        // Create a deep copy of the original array to preserve initial state
16        this.original = Arrays.copyOf(nums, nums.length);
17        this.rand = new Random();
18    }
19
20    /**
21     * Resets the array to its original configuration and returns it
22     * @return The array in its original order
23     */
24    public int[] reset() {
25        // Restore nums array to original configuration
26        nums = Arrays.copyOf(original, original.length);
27        return nums;
28    }
29
30    /**
31     * Returns a random shuffling of the array using Fisher-Yates algorithm
32     * @return The shuffled array
33     */
34    public int[] shuffle() {
35        // Fisher-Yates shuffle algorithm
36        // Iterate through the array from start to end
37        for (int i = 0; i < nums.length; i++) {
38            // Pick a random index from i to end of array
39            int randomIndex = i + rand.nextInt(nums.length - i);
40            // Swap current element with randomly selected element
41            swap(i, randomIndex);
42        }
43        return nums;
44    }
45
46    /**
47     * Helper method to swap two elements in the nums array
48     * @param i First index to swap
49     * @param j Second index to swap
50     */
51    private void swap(int i, int j) {
52        int temp = nums[i];
53        nums[i] = nums[j];
54        nums[j] = temp;
55    }
56}
57
58/**
59 * Your Solution object will be instantiated and called as such:
60 * Solution obj = new Solution(nums);
61 * int[] param_1 = obj.reset();
62 * int[] param_2 = obj.shuffle();
63 */
64
1class Solution {
2private:
3    vector<int> nums;      // Current array that can be shuffled
4    vector<int> original;  // Original array to restore from
5  
6public:
7    /**
8     * Constructor: Initialize with the given array
9     * @param nums: Input array to be shuffled
10     */
11    Solution(vector<int>& nums) {
12        this->nums = nums;
13        this->original = nums;  // Direct copy is more efficient than resize + copy
14    }
15  
16    /**
17     * Reset the array to its original configuration
18     * @return: The original array
19     */
20    vector<int> reset() {
21        nums = original;  // Direct assignment is cleaner than copy()
22        return nums;
23    }
24  
25    /**
26     * Shuffle the array using Fisher-Yates algorithm
27     * @return: The shuffled array
28     */
29    vector<int> shuffle() {
30        // Fisher-Yates shuffle: iterate through array and swap each element
31        // with a random element from the remaining unshuffled portion
32        for (int i = 0; i < nums.size(); ++i) {
33            // Generate random index from [i, nums.size() - 1]
34            int randomIndex = i + rand() % (nums.size() - i);
35          
36            // Swap current element with randomly selected element
37            swap(nums[i], nums[randomIndex]);
38        }
39        return nums;
40    }
41};
42
43/**
44 * Your Solution object will be instantiated and called as such:
45 * Solution* obj = new Solution(nums);
46 * vector<int> param_1 = obj->reset();
47 * vector<int> param_2 = obj->shuffle();
48 */
49
1// Store the original array
2let originalArray: number[] = [];
3
4/**
5 * Initialize with the given array
6 * @param nums - The input array to be stored
7 */
8function initialize(nums: number[]): void {
9    originalArray = nums;
10}
11
12/**
13 * Reset the array to its original configuration and return it
14 * @returns The original array
15 */
16function reset(): number[] {
17    return originalArray;
18}
19
20/**
21 * Shuffle the array using Fisher-Yates algorithm and return it
22 * @returns A randomly shuffled copy of the array
23 */
24function shuffle(): number[] {
25    const arrayLength: number = originalArray.length;
26    // Create a copy of the original array to avoid modifying it
27    const shuffledArray: number[] = [...originalArray];
28  
29    // Fisher-Yates shuffle algorithm
30    for (let currentIndex = 0; currentIndex < arrayLength; currentIndex++) {
31        // Generate a random index from 0 to arrayLength - 1
32        const randomIndex: number = Math.floor(Math.random() * arrayLength);
33      
34        // Swap elements at currentIndex and randomIndex
35        [shuffledArray[currentIndex], shuffledArray[randomIndex]] = 
36            [shuffledArray[randomIndex], shuffledArray[currentIndex]];
37    }
38  
39    return shuffledArray;
40}
41

Time and Space Complexity

Time Complexity:

  • __init__: O(n) where n is the length of the input array, due to copying the array with nums.copy()
  • reset(): O(n) because it creates a copy of the original array using self.original.copy()
  • shuffle(): O(n) for the Fisher-Yates shuffle algorithm, which iterates through the array once and performs constant-time swaps

Space Complexity:

  • Overall space: O(n) for storing two copies of the array (self.nums and self.original)
  • __init__: O(n) additional space for storing the copy of the original array
  • reset(): O(1) additional space (not counting the space already used by the object)
  • shuffle(): O(1) additional space as it shuffles in-place using only a constant amount of extra variables

The shuffle algorithm implements the Fisher-Yates shuffle (also known as Knuth shuffle), which generates each permutation with equal probability 1/n! where n is the array length.

Common Pitfalls

1. Not Creating Proper Copies of the Array

The Pitfall: A very common mistake is storing references instead of copies:

def __init__(self, nums: List[int]):
    self.nums = nums
    self.original = nums  # WRONG! Both variables point to the same list

When you assign self.original = nums without .copy(), both variables reference the same list object in memory. Any modifications to self.nums during shuffling will also modify self.original, making it impossible to reset to the true original state.

The Solution: Always use .copy() to create independent copies:

def __init__(self, nums: List[int]):
    self.nums = nums
    self.original = nums.copy()  # Creates a separate copy

2. Incorrect Random Range in Shuffle

The Pitfall: Using the wrong range for random selection can create biased shuffles:

def shuffle(self) -> List[int]:
    for i in range(len(self.nums)):
        j = random.randrange(0, len(self.nums))  # WRONG! Creates bias
        self.nums[i], self.nums[j] = self.nums[j], self.nums[i]
    return self.nums

Selecting from the entire array [0, n) instead of [i, n) doesn't guarantee uniform distribution of permutations.

The Solution: Always select from the remaining unshuffled portion:

j = random.randrange(i, len(self.nums))  # Correct: select from [i, n)

3. Forgetting to Copy in Reset Method

The Pitfall: Returning or assigning the original array directly without copying:

def reset(self) -> List[int]:
    self.nums = self.original  # WRONG! Now both reference the same list
    return self.nums

This creates the same reference problem - future shuffles would modify the "original" array.

The Solution: Create a fresh copy when resetting:

def reset(self) -> List[int]:
    self.nums = self.original.copy()
    return self.nums

4. Using random.choice() or random.sample() Incorrectly

The Pitfall: Trying to use high-level random functions without proper implementation:

def shuffle(self) -> List[int]:
    self.nums = random.sample(self.nums, len(self.nums))  # Works but less educational
    return self.nums

While random.sample() works, it doesn't demonstrate understanding of the Fisher-Yates algorithm, which is often the learning objective.

The Solution: Implement the Fisher-Yates algorithm manually to show algorithmic understanding and have full control over the shuffling process.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More