384. Shuffle an Array
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:
-
Constructor
__init__(nums)
: Takes an integer arraynums
as input and initializes the object with this array. -
reset()
method: Returns the array back to its original configuration (the state it was in when first passed to the constructor). -
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 positioni
in the array, randomly selects an indexj
from the range[i, n)
wheren
is the array length, and swaps the elements at positionsi
andj
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.
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 indexj
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
andj
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 arrayreset()
: O(n) time for copying, O(1) additional spaceshuffle()
: 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 EvaluatorExample 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)
wheren
is the length of the input array, due to copying the array withnums.copy()
reset()
:O(n)
because it creates a copy of the original array usingself.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
andself.original
) __init__
:O(n)
additional space for storing the copy of the original arrayreset()
: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.
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!