Facebook Pixel

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 to n - 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 need m - 1 swaps
  • If 0 is not part of the cycle, we need m + 1 swaps (we must first bring 0 into the cycle, perform the swaps, then move 0 out)

The solution computes the total number of operations for both target configurations and returns the minimum.

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

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:

  1. Move 0 to position i (swap 0 with item at i)
  2. Move 0 to position j (swap 0 with item at j)
  3. Move 0 back to position i (swap 0 with item that was originally at i)

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 length m, we need exactly m - 1 swaps to fix all elements in that cycle.

  • If 0 is not part of a cycle, we first need to bring 0 into the cycle (1 swap), then perform m swaps to fix the cycle while moving 0 through it, then potentially move 0 out. This totals to m + 1 swaps.

Since we have two valid final configurations (with 0 at the start or end), we need to consider both:

  1. Target configuration with 0 at position 0: elements 1, 2, ..., n-1 at positions 1, 2, ..., n-1
  2. Target configuration with 0 at position n-1: elements 1, 2, ..., n-1 at positions 0, 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).

Learn more about Greedy and Sorting patterns.

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):

  1. Initialize tracking structures:

    • vis: A boolean array to track visited elements in cycles
    • cnt: Counter for total operations
  2. 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 bringing 0 into the cycle)
  3. 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 position nums[j]
    • Mark each position as visited
    • Count each element in the cycle
  4. Adjust for 0's position:

    return cnt - 2 * (nums[k] != k)
    • If nums[k] != k, it means 0 is not at its target position k
    • This means 0 is part of a cycle, so we subtract 2 from the count
    • Why 2? Because for a cycle containing 0, we counted m + 1 operations but actually need only m - 1

Main Solution:

  1. Calculate for first configuration (target: 0 at position 0):

    a = f(nums, 0)
  2. Calculate for second configuration (target: 0 at position n-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 positions 0, 1, ..., n-2
  3. 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 Evaluator

Example 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]

  1. 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
  2. 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]

  1. 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
  2. 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)
  3. 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:

  1. [3, 2, 0, 1] β†’ swap 0 with 3 β†’ [0, 2, 3, 1]
  2. [0, 2, 3, 1] β†’ swap 0 with 2 β†’ [2, 0, 3, 1]
  3. [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:

  1. [3, 2, 0, 1] β†’ swap 0 with 1 β†’ [3, 2, 1, 0]
  2. [3, 2, 1, 0] β†’ swap 0 with 3 β†’ [0, 2, 1, 3]
  3. [0, 2, 1, 3] β†’ swap 0 with 1 β†’ [1, 2, 0, 3]
  4. [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 takes O(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 is O(n)
  • Creating the modified array [(v - 1 + n) % n for v in nums] takes O(n) time
  • Therefore, the overall time complexity is O(n) + O(n) + O(n) = O(n)

Space Complexity Analysis:

  • The vis array in function f uses O(n) space
  • The list comprehension [(v - 1 + n) % n for v in nums] creates a new array of size n, using O(n) space
  • Other variables (cnt, i, j, a, b) use O(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 need m + 1 operations (bring 0 in, perform swaps, move 0 out)
  • When 0 IS in a cycle, we need only m - 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:

  1. Initially counting as if 0 is not in any cycle (adding 1 for each cycle start)
  2. Then adjusting by subtracting 2 when 0 is at the wrong position (converting m+1 to m-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 position 0
  • Value 2 maps to position 1
  • ...
  • Value n-1 maps to position n-2
  • Value 0 maps to position n-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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

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

Load More