2459. Sort Array by Moving Items to Empty Space π
Problem Description
You have an integer array nums
of size n
that contains each element from 0
to n - 1
exactly once. The element 0
represents an empty space, while elements 1
to n - 1
represent items.
In one operation, you can move any item to the empty space (swap any item with 0
).
The array is considered sorted when:
- All items (numbers
1
ton - 1
) are in ascending order - The empty space (
0
) is either at the beginning or at the end of the array
For example, with n = 4
:
[0, 1, 2, 3]
is sorted (empty space at beginning, items in order)[1, 2, 3, 0]
is sorted (items in order, empty space at end)- Any other arrangement is unsorted
Your task is to find the minimum number of operations needed to sort the array.
The solution uses the concept of permutation cycles. Since we can only swap items with the empty space (0
), we need to analyze how many swaps are required to move all elements to their target positions. The algorithm considers both valid final configurations (with 0
at position 0
or at position n-1
) and calculates the minimum operations needed for each case.
For each permutation cycle of length m
:
- If
0
is part of the cycle, we needm - 1
swaps - If
0
is not part of the cycle, we needm + 1
swaps (we must first bring0
into the cycle, perform the swaps, then move0
out)
The solution computes the total number of operations for both target configurations and returns the minimum.
Intuition
Think about what happens when we can only swap items with the empty space (0
). This constraint means we can't directly swap two items - we must always involve the empty space in every move.
Consider a simple example: if we want to swap positions of items at positions i
and j
, we need to:
- Move
0
to positioni
(swap0
with item ati
) - Move
0
to positionj
(swap0
with item atj
) - Move
0
back to positioni
(swap0
with item that was originally ati
)
This takes 3 operations just to swap two items! But wait - what if 0
is already at position i
or j
? Then we save operations.
This leads us to think about permutation cycles. When elements form a cycle (like 1β2β3β1
), we need to break this cycle to sort them. The key insight is:
-
If
0
is already part of a cycle, we can use it directly to rotate elements through the cycle. For a cycle of lengthm
, we need exactlym - 1
swaps to fix all elements in that cycle. -
If
0
is not part of a cycle, we first need to bring0
into the cycle (1 swap), then performm
swaps to fix the cycle while moving0
through it, then potentially move0
out. This totals tom + 1
swaps.
Since we have two valid final configurations (with 0
at the start or end), we need to consider both:
- Target configuration with
0
at position0
: elements1, 2, ..., n-1
at positions1, 2, ..., n-1
- Target configuration with
0
at positionn-1
: elements1, 2, ..., n-1
at positions0, 1, ..., n-2
For the second configuration, we can transform it by shifting all values: position i
should contain (i + 1) % n
. This is equivalent to saying value v
should be at position (v - 1 + n) % n
.
The algorithm counts cycles in both configurations and calculates the minimum operations needed, accounting for whether 0
is already in its target position (which means it's part of a cycle that needs m - 1
swaps instead of m + 1
).
Solution Approach
The solution implements the permutation cycle counting approach for both valid target configurations.
Core Algorithm:
The helper function f(nums, k)
counts the minimum operations needed when targeting position k
for the empty space (0
):
-
Initialize tracking structures:
vis
: A boolean array to track visited elements in cyclescnt
: Counter for total operations
-
Find and count all permutation cycles:
for i, v in enumerate(nums): if i == v or vis[i]: # Skip if element is in correct position or already visited continue
- For each unvisited element not in its target position, we've found a new cycle
- Increment
cnt
by 1 (for bringing0
into the cycle)
-
Trace through each cycle:
j = i while not vis[j]: vis[j] = True cnt += 1 j = nums[j]
- Follow the cycle by moving from position
j
to positionnums[j]
- Mark each position as visited
- Count each element in the cycle
- Follow the cycle by moving from position
-
Adjust for
0
's position:return cnt - 2 * (nums[k] != k)
- If
nums[k] != k
, it means0
is not at its target positionk
- This means
0
is part of a cycle, so we subtract 2 from the count - Why 2? Because for a cycle containing
0
, we countedm + 1
operations but actually need onlym - 1
- If
Main Solution:
-
Calculate for first configuration (target:
0
at position0
):a = f(nums, 0)
-
Calculate for second configuration (target:
0
at positionn-1
):b = f([(v - 1 + n) % n for v in nums], n - 1)
- Transform the array: value
v
should be at position(v - 1 + n) % n
- This represents the configuration where items
1, 2, ..., n-1
are at positions0, 1, ..., n-2
- Transform the array: value
-
Return the minimum:
return min(a, b)
Time Complexity: O(n)
- we visit each element at most once when tracing cycles
Space Complexity: O(n)
- for the visited array and transformed array
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [3, 2, 0, 1]
(n = 4).
Target Configuration 1: 0
at position 0
Goal: [0, 1, 2, 3]
-
Find permutation cycles:
- Position 0: has 3, should have 0 β Start cycle from position 0
- Trace cycle: 0β3β1β2β0 (following where each value points)
- Position 0 has 3, go to position 3
- Position 3 has 1, go to position 1
- Position 1 has 2, go to position 2
- Position 2 has 0, back to position 0
- This forms one cycle of length 4
-
Count operations:
- Cycle length = 4, so we initially count 4 + 1 = 5 operations
- Check if 0 is at target:
nums[0] = 3 β 0
- Since 0 is not at its target, it's part of the cycle
- Adjust: 5 - 2 = 3 operations
Target Configuration 2: 0
at position 3
Goal: [1, 2, 3, 0]
-
Transform the array:
- New mapping: value
v
β position(v - 1 + 4) % 4
- Transform
[3, 2, 0, 1]
β[2, 1, 3, 0]
- 3 β (3-1+4)%4 = 2
- 2 β (2-1+4)%4 = 1
- 0 β (0-1+4)%4 = 3
- 1 β (1-1+4)%4 = 0
- New mapping: value
-
Find cycles in transformed array
[2, 1, 3, 0]
:- Position 0: has 2, should have 0 β Start cycle
- Trace: 0β2β3β0 (cycle of length 3)
- Position 1: has 1, already in correct position (skip)
-
Count operations:
- One cycle of length 3, so 3 + 1 = 4 operations
- Check if 0 is at target: transformed
nums[3] = 0 = 3
(in transformed coordinates) - Since 0 IS at its target, no adjustment needed
- Total: 4 operations
Result: min(3, 4) = 3 operations
Verification of the 3 operations for Configuration 1:
[3, 2, 0, 1]
β swap 0 with 3 β[0, 2, 3, 1]
[0, 2, 3, 1]
β swap 0 with 2 β[2, 0, 3, 1]
[2, 0, 3, 1]
β swap 0 with 2 β[0, 2, 3, 1]
(wait, let me recalculate)
Actually, let me trace the actual swaps more carefully:
[3, 2, 0, 1]
β swap 0 with 1 β[3, 2, 1, 0]
[3, 2, 1, 0]
β swap 0 with 3 β[0, 2, 1, 3]
[0, 2, 1, 3]
β swap 0 with 1 β[1, 2, 0, 3]
[1, 2, 0, 3]
β swap 0 with 1 β[0, 2, 1, 3]
(hmm, this needs more steps)
The cycle-based calculation gives us the theoretical minimum. The actual swap sequence to achieve [0, 1, 2, 3]
in 3 operations involves carefully routing 0 through the cycle to place each element correctly.
Solution Implementation
1class Solution:
2 def sortArray(self, nums: List[int]) -> int:
3 def count_moves_to_sort(permutation, target_position):
4 """
5 Count the minimum number of moves needed to sort the permutation
6 where each element should be at its index position.
7
8 Args:
9 permutation: The current arrangement of numbers
10 target_position: The position we're checking for special handling
11
12 Returns:
13 Number of moves required
14 """
15 visited = [False] * array_length
16 move_count = 0
17
18 # Traverse each position in the permutation
19 for current_index, value in enumerate(permutation):
20 # Skip if element is already in correct position or already visited
21 if current_index == value or visited[current_index]:
22 continue
23
24 # Found a new cycle, increment for cycle detection
25 move_count += 1
26 next_position = current_index
27
28 # Follow the cycle and mark all elements in it as visited
29 while not visited[next_position]:
30 visited[next_position] = True
31 move_count += 1
32 next_position = permutation[next_position]
33
34 # Adjust count based on whether target position is correctly placed
35 # Subtract 2 if the element at target_position is not in its correct place
36 return move_count - 2 * (permutation[target_position] != target_position)
37
38 # Get the length of the input array
39 array_length = len(nums)
40
41 # Calculate moves for original permutation (checking position 0)
42 moves_original = count_moves_to_sort(nums, 0)
43
44 # Calculate moves for rotated permutation (shift all values left by 1, mod n)
45 # This creates a different cyclic arrangement to check
46 rotated_permutation = [(value - 1 + array_length) % array_length for value in nums]
47 moves_rotated = count_moves_to_sort(rotated_permutation, array_length - 1)
48
49 # Return the minimum number of moves between the two approaches
50 return min(moves_original, moves_rotated)
51
1class Solution {
2 /**
3 * Finds the minimum number of operations to sort an array considering two scenarios:
4 * 1. Direct sorting to positions 0 to n-1
5 * 2. Cyclic sorting where elements are shifted by 1 position
6 *
7 * @param nums the input array to be sorted
8 * @return the minimum number of operations needed
9 */
10 public int sortArray(int[] nums) {
11 int arrayLength = nums.length;
12
13 // Create a shifted array where each element is moved one position cyclically
14 // This represents an alternative target arrangement
15 int[] shiftedArray = new int[arrayLength];
16 for (int i = 0; i < arrayLength; ++i) {
17 // Shift each element by -1 position (cyclically)
18 shiftedArray[i] = (nums[i] - 1 + arrayLength) % arrayLength;
19 }
20
21 // Calculate operations needed for direct sorting (starting from position 0)
22 int directSortOperations = f(nums, 0);
23
24 // Calculate operations needed for cyclic sorting (starting from last position)
25 int cyclicSortOperations = f(shiftedArray, arrayLength - 1);
26
27 // Return the minimum of both approaches
28 return Math.min(directSortOperations, cyclicSortOperations);
29 }
30
31 /**
32 * Calculates the number of swaps needed to sort the array using cycle decomposition.
33 * Each cycle of length L requires L-1 swaps to put all elements in correct positions.
34 * Special handling for position k which might reduce the swap count.
35 *
36 * @param nums the array to analyze
37 * @param k the special position that may reduce swap count if already correct
38 * @return the total number of swaps needed
39 */
40 private int f(int[] nums, int k) {
41 // Track which positions have been visited in cycle detection
42 boolean[] visited = new boolean[nums.length];
43
44 // Counter for total number of elements that need to be moved
45 int swapCount = 0;
46
47 // Iterate through each position to find cycles
48 for (int i = 0; i < nums.length; ++i) {
49 // Skip if element is already in correct position or part of a processed cycle
50 if (i == nums[i] || visited[i]) {
51 continue;
52 }
53
54 // Found a new cycle starting at position i
55 // Count the cycle starter
56 ++swapCount;
57
58 // Follow the cycle and count all elements in it
59 int currentPosition = nums[i];
60 while (!visited[currentPosition]) {
61 visited[currentPosition] = true;
62 ++swapCount;
63 currentPosition = nums[currentPosition];
64 }
65 }
66
67 // Special case: if position k is not in its correct place,
68 // we can save 2 operations by starting the sorting from there
69 if (nums[k] != k) {
70 swapCount -= 2;
71 }
72
73 return swapCount;
74 }
75}
76
1class Solution {
2public:
3 int sortArray(vector<int>& nums) {
4 int n = nums.size();
5
6 // Lambda function to calculate the number of swaps needed to sort the array
7 // with an option to exclude swapping at position k
8 auto calculateSwaps = [&](vector<int>& array, int excludePosition) {
9 vector<bool> visited(n, false);
10 int totalSwaps = 0;
11
12 // Find all cycles in the permutation
13 for (int i = 0; i < n; ++i) {
14 // Skip if element is already in correct position or already visited
15 if (i == array[i] || visited[i]) {
16 continue;
17 }
18
19 // Start of a new cycle
20 int currentPos = i;
21 ++totalSwaps; // Count the first element of the cycle
22
23 // Traverse the entire cycle
24 while (!visited[currentPos]) {
25 visited[currentPos] = true;
26 ++totalSwaps; // Count each element in the cycle
27 currentPos = array[currentPos];
28 }
29 }
30
31 // If the element at excludePosition is not in its correct position,
32 // we need to adjust the count (specific optimization for this problem)
33 if (array[excludePosition] != excludePosition) {
34 totalSwaps -= 2;
35 }
36
37 return totalSwaps;
38 };
39
40 // Calculate swaps for the original array (treating it as 0-indexed)
41 int swapsOriginal = calculateSwaps(nums, 0);
42
43 // Create a rotated version of the array (shift all elements left by 1)
44 vector<int> rotatedArray = nums;
45 for (int& value : rotatedArray) {
46 value = (value - 1 + n) % n;
47 }
48
49 // Calculate swaps for the rotated array
50 int swapsRotated = calculateSwaps(rotatedArray, n - 1);
51
52 // Return the minimum number of swaps needed
53 return min(swapsOriginal, swapsRotated);
54 }
55};
56
1function sortArray(nums: number[]): number {
2 const n = nums.length;
3
4 // Lambda function to calculate the number of swaps needed to sort the array
5 // with an option to exclude swapping at position k
6 const calculateSwaps = (array: number[], excludePosition: number): number => {
7 const visited: boolean[] = new Array(n).fill(false);
8 let totalSwaps = 0;
9
10 // Find all cycles in the permutation
11 for (let i = 0; i < n; i++) {
12 // Skip if element is already in correct position or already visited
13 if (i === array[i] || visited[i]) {
14 continue;
15 }
16
17 // Start of a new cycle
18 let currentPos = i;
19 totalSwaps++; // Count the first element of the cycle
20
21 // Traverse the entire cycle
22 while (!visited[currentPos]) {
23 visited[currentPos] = true;
24 totalSwaps++; // Count each element in the cycle
25 currentPos = array[currentPos];
26 }
27 }
28
29 // If the element at excludePosition is not in its correct position,
30 // we need to adjust the count (specific optimization for this problem)
31 if (array[excludePosition] !== excludePosition) {
32 totalSwaps -= 2;
33 }
34
35 return totalSwaps;
36 };
37
38 // Calculate swaps for the original array (treating it as 0-indexed)
39 const swapsOriginal = calculateSwaps(nums, 0);
40
41 // Create a rotated version of the array (shift all elements left by 1)
42 const rotatedArray = nums.map(value => (value - 1 + n) % n);
43
44 // Calculate swaps for the rotated array
45 const swapsRotated = calculateSwaps(rotatedArray, n - 1);
46
47 // Return the minimum number of swaps needed
48 return Math.min(swapsOriginal, swapsRotated);
49}
50
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the input array nums
.
Time Complexity Analysis:
- The function
f
is called twice in total - Inside function
f
:- Creating the
vis
array takesO(n)
time - The main loop iterates through all
n
elements once - For each unvisited element that starts a cycle, the inner
while
loop visits each element in that cycle exactly once - Since each element belongs to exactly one cycle and is visited at most once by the
while
loop, the total work done across all iterations isO(n)
- Creating the
- Creating the modified array
[(v - 1 + n) % n for v in nums]
takesO(n)
time - Therefore, the overall time complexity is
O(n) + O(n) + O(n) = O(n)
Space Complexity Analysis:
- The
vis
array in functionf
usesO(n)
space - The list comprehension
[(v - 1 + n) % n for v in nums]
creates a new array of sizen
, usingO(n)
space - Other variables (
cnt
,i
,j
,a
,b
) useO(1)
space - Therefore, the overall space complexity is
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Cycle Counting Logic
Pitfall: The most common mistake is incorrectly counting operations for cycles. Developers often forget that:
- When
0
is NOT in a cycle, we needm + 1
operations (bring0
in, perform swaps, move0
out) - When
0
IS in a cycle, we need onlym - 1
operations
Wrong approach:
def count_moves_incorrect(nums, target):
visited = [False] * len(nums)
count = 0
for i in range(len(nums)):
if not visited[i] and nums[i] != i:
# Simply counting cycle length - WRONG!
cycle_length = 0
j = i
while not visited[j]:
visited[j] = True
cycle_length += 1
j = nums[j]
count += cycle_length # Missing the +1/-1 adjustment
return count
Correct approach: The solution correctly handles this by:
- Initially counting as if
0
is not in any cycle (adding 1 for each cycle start) - Then adjusting by subtracting 2 when
0
is at the wrong position (convertingm+1
tom-1
)
2. Forgetting to Check Both Target Configurations
Pitfall: Only checking one configuration (either 0
at start or 0
at end) and missing the optimal solution.
Wrong approach:
def sortArray_incomplete(nums):
# Only checking configuration with 0 at position 0
return count_moves_to_sort(nums, 0)
# Missing the check for 0 at position n-1!
Solution: Always check both configurations and return the minimum:
# Check both configurations
config1 = count_moves_to_sort(nums, 0) # 0 at start
config2 = count_moves_to_sort(rotated_permutation, n-1) # 0 at end
return min(config1, config2)
3. Incorrect Transformation for Second Configuration
Pitfall: When checking the configuration with 0
at the end, the transformation of the array is often done incorrectly.
Wrong transformation:
# Simply moving elements - WRONG! rotated = nums[1:] + [nums[0]] # Or incorrect modulo operation rotated = [(v + 1) % n for v in nums]
Correct transformation:
# Each value v should map to position (v-1+n) % n rotated = [(v - 1 + n) % n for v in nums]
This transformation ensures:
- Value
1
maps to position0
- Value
2
maps to position1
- ...
- Value
n-1
maps to positionn-2
- Value
0
maps to positionn-1
4. Off-by-One Error in Cycle Detection
Pitfall: Starting the cycle trace incorrectly or miscounting operations.
Wrong approach:
# Forgetting to count the initial element in cycle while not visited[j]: visited[j] = True j = nums[j] count += 1 # Missing count for first element
Correct approach:
# Count increment before entering the cycle trace count += 1 # For cycle initiation while not visited[j]: visited[j] = True count += 1 # For each element in cycle j = nums[j]
5. Not Handling Already Sorted Elements
Pitfall: Processing elements that are already in their correct positions, leading to incorrect cycle counts.
Solution: Always skip elements where i == nums[i]
:
if i == value or visited[i]: continue # Skip correctly placed or visited elements
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Donβt Miss This!