Facebook Pixel

2659. Make Array Empty

Problem Description

You have an integer array nums containing distinct numbers. You need to empty this array by repeatedly performing one of two operations:

  1. If the first element is the smallest value in the entire array, remove it from the array
  2. Otherwise, move the first element to the end of the array

Your task is to count how many total operations it takes to completely empty the array.

For example, if you have nums = [3, 1, 2]:

  • First element is 3, which is not the smallest (1 is), so move 3 to the end: [1, 2, 3] (1 operation)
  • First element is 1, which is the smallest, so remove it: [2, 3] (1 operation)
  • First element is 2, which is the smallest, so remove it: [3] (1 operation)
  • First element is 3, which is the smallest (and only), so remove it: [] (1 operation)
  • Total: 4 operations

The challenge is to efficiently calculate this count without actually simulating the entire process, since the array could be large. The solution uses the fact that elements will be removed in sorted order (smallest to largest), and tracks how many rotations occur between removing consecutive elements.

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

Intuition

Since elements can only be removed when they're the smallest and at the front, we'll always remove elements in ascending order. This means if we have [3, 1, 4, 2], we'll remove them in the order: 1, 2, 3, 4, regardless of their initial positions.

The key insight is that we don't need to simulate the actual rotations. Instead, we can calculate how many operations it takes to bring each element to the front position when it's its turn to be removed.

Consider the sorted sequence of elements to be removed. For the first (smallest) element, we need to rotate the array until it reaches the front. That's simply its initial position + 1 operations (the positions ahead of it plus the removal itself).

For subsequent elements, the calculation becomes more complex. When moving from removing element a to element b, we need to consider:

  1. The distance between their original positions
  2. Which elements between them have already been removed (they no longer contribute to the rotation count)
  3. Whether we need to "wrap around" the array (if b's position comes before a's position in the original array)

The "wrap around" case is crucial: if element b was originally positioned before element a, it means after removing a, we continue rotating past the end of the array and back to the beginning to reach b. This adds n - k extra operations, where k is the number of elements already removed.

To efficiently track which elements have been removed between any two positions, we use a data structure like a Fenwick tree or sorted list. This allows us to query "how many elements between positions i and j have been removed?" in O(log n) time.

The formula for operations between consecutive removals becomes:

  • Basic distance: j - i
  • Subtract already removed elements between them: - (removed elements in range)
  • Add wrap-around cost if needed: + (n - k) if i > j

Learn more about Greedy, Segment Tree, Binary Search and Sorting patterns.

Solution Approach

First, we create a hash table pos to map each element to its original index in the array: pos = {x: i for i, x in enumerate(nums)}. This allows us to quickly look up where each element was initially positioned.

Next, we sort the nums array. Since elements must be removed in ascending order (smallest first), this sorted array represents the order in which elements will be removed.

We initialize the answer with the operations needed to remove the first (smallest) element: ans = pos[nums[0]] + 1. This represents rotating the array until the smallest element reaches the front, then removing it.

To track which positions have been processed (removed), we use a SortedList data structure. This allows us to efficiently:

  • Add removed positions with sl.add(i)
  • Query how many positions below a certain value have been removed using sl.bisect(j) and sl.bisect(i)

For each consecutive pair of elements (a, b) in the sorted array:

  1. Get their original positions: i = pos[a] and j = pos[b]

  2. Calculate the distance between positions, adjusting for already-removed elements:

    • d = j - i - sl.bisect(j) + sl.bisect(i)
    • This computes the raw distance j - i, then subtracts the count of removed elements between them
  3. Add wrap-around cost if necessary:

    • If i > j (meaning b was originally before a), we need to wrap around the array
    • This adds (n - k) * int(i > j) operations, where k is the current iteration index (number of elements already removed minus 1)
  4. Update the total: ans += d + (n - k) * int(i > j)

  5. Mark position i as removed: sl.add(i)

The time complexity is O(n log n) due to sorting and the log-time operations on the SortedList. The space complexity is O(n) for the hash table and SortedList.

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 trace through the solution with nums = [4, 2, 1, 3].

Step 1: Create position mapping

  • pos = {4: 0, 2: 1, 1: 2, 3: 3}
  • This tells us: 4 is at index 0, 2 is at index 1, 1 is at index 2, 3 is at index 3

Step 2: Sort the array

  • Sorted nums = [1, 2, 3, 4]
  • This is the order we'll remove elements (smallest to largest)

Step 3: Remove first element (1)

  • Element 1 is at position 2 (from pos[1] = 2)
  • Operations needed: 2 + 1 = 3 (rotate twice to bring to front, then remove)
  • ans = 3
  • Add position 2 to removed set: sl = [2]

Step 4: Remove second element (2)

  • Previous element 1 was at position i = 2
  • Current element 2 is at position j = 1
  • Calculate distance: d = 1 - 2 - bisect(1) + bisect(2)
    • bisect(1) in [2] = 0 (no removed positions less than 1)
    • bisect(2) in [2] = 0 (no removed positions less than 2)
    • d = 1 - 2 - 0 + 0 = -1
  • Since i > j (2 > 1), we need wrap-around: add n - k = 4 - 1 = 3
  • Total operations: -1 + 3 = 2
  • ans = 3 + 2 = 5
  • Add position 1 to removed set: sl = [1, 2]

Step 5: Remove third element (3)

  • Previous element 2 was at position i = 1
  • Current element 3 is at position j = 3
  • Calculate distance: d = 3 - 1 - bisect(3) + bisect(1)
    • bisect(3) in [1, 2] = 2 (two removed positions less than 3)
    • bisect(1) in [1, 2] = 0 (no removed positions less than 1)
    • d = 3 - 1 - 2 + 0 = 0
  • Since i < j (1 < 3), no wrap-around needed
  • Total operations: 0 + 1 = 1 (the +1 for removal)
  • ans = 5 + 1 = 6
  • Add position 3 to removed set: sl = [1, 2, 3]

Step 6: Remove fourth element (4)

  • Previous element 3 was at position i = 3
  • Current element 4 is at position j = 0
  • Calculate distance: d = 0 - 3 - bisect(0) + bisect(3)
    • bisect(0) in [1, 2, 3] = 0 (no removed positions less than 0)
    • bisect(3) in [1, 2, 3] = 2 (two removed positions less than 3)
    • d = 0 - 3 - 0 + 2 = -1
  • Since i > j (3 > 0), we need wrap-around: add n - k = 4 - 3 = 1
  • Total operations: -1 + 1 + 1 = 1 (the last +1 for removal)
  • ans = 6 + 1 = 7

Final answer: 7 operations

The algorithm efficiently calculates that we need 7 total operations to empty the array [4, 2, 1, 3] without actually simulating the rotations.

Solution Implementation

1class Solution:
2    def countOperationsToEmptyArray(self, nums: List[int]) -> int:
3        # Create a mapping from each value to its original position in the array
4        value_to_original_position = {value: index for index, value in enumerate(nums)}
5      
6        # Sort the values to determine the removal order (smallest to largest)
7        sorted_values = sorted(nums)
8      
9        # SortedList to track which positions have been removed
10        removed_positions = SortedList()
11      
12        # Initialize answer with operations to remove the first (smallest) element
13        # We need original_position + 1 operations to reach and remove it
14        total_operations = value_to_original_position[sorted_values[0]] + 1
15      
16        # Total number of elements
17        array_length = len(nums)
18      
19        # Process each consecutive pair of values in sorted order
20        for removal_index, (current_value, next_value) in enumerate(pairwise(sorted_values)):
21            # Get original positions of current and next values
22            current_position = value_to_original_position[current_value]
23            next_position = value_to_original_position[next_value]
24          
25            # Calculate how many unremoved elements are between current and next positions
26            # This accounts for already removed elements that don't affect the distance
27            removed_between = removed_positions.bisect(next_position) - removed_positions.bisect(current_position)
28            distance = next_position - current_position - removed_between
29          
30            # Add the distance to move from current to next position
31            total_operations += distance
32          
33            # If next position is before current position (wraparound case)
34            # We need to account for cycling through remaining elements
35            if current_position > next_position:
36                remaining_elements = array_length - removal_index
37                total_operations += remaining_elements
38          
39            # Mark current position as removed
40            removed_positions.add(current_position)
41      
42        return total_operations
43
1/**
2 * Binary Indexed Tree (Fenwick Tree) for efficient range sum queries and updates
3 */
4class BinaryIndexedTree {
5    private int size;
6    private int[] tree;
7
8    /**
9     * Initialize Binary Indexed Tree with given size
10     * @param n The size of the array (1-indexed internally)
11     */
12    public BinaryIndexedTree(int n) {
13        this.size = n;
14        this.tree = new int[n + 1];  // 1-indexed array for BIT
15    }
16
17    /**
18     * Update the value at position x by adding delta
19     * @param x The position to update (1-indexed)
20     * @param delta The value to add
21     */
22    public void update(int x, int delta) {
23        // Propagate the update up the tree using the BIT property
24        while (x <= size) {
25            tree[x] += delta;
26            x += x & -x;  // Move to next node by adding lowest set bit
27        }
28    }
29
30    /**
31     * Query the prefix sum from index 1 to x
32     * @param x The end position of the range (1-indexed)
33     * @return The sum of elements from 1 to x
34     */
35    public int query(int x) {
36        int sum = 0;
37        // Accumulate sum by traversing down the tree
38        while (x > 0) {
39            sum += tree[x];
40            x -= x & -x;  // Move to parent by removing lowest set bit
41        }
42        return sum;
43    }
44}
45
46/**
47 * Solution class for counting operations to empty an array
48 */
49class Solution {
50    /**
51     * Count the minimum number of operations to empty the array
52     * by removing elements in ascending order of their values
53     * @param nums The input array
54     * @return The total number of operations needed
55     */
56    public long countOperationsToEmptyArray(int[] nums) {
57        int n = nums.length;
58      
59        // Map each value to its original position in the array
60        Map<Integer, Integer> valueToPosition = new HashMap<>();
61        for (int i = 0; i < n; ++i) {
62            valueToPosition.put(nums[i], i);
63        }
64      
65        // Sort array to process elements in ascending order
66        Arrays.sort(nums);
67      
68        // Initialize answer with operations for the first (smallest) element
69        // Add 1 because positions are 0-indexed but we need 1-based count
70        long totalOperations = valueToPosition.get(nums[0]) + 1;
71      
72        // Initialize Binary Indexed Tree to track removed elements
73        BinaryIndexedTree removedTracker = new BinaryIndexedTree(n);
74      
75        // Process remaining elements in ascending order
76        for (int k = 0; k < n - 1; ++k) {
77            // Get original positions of current and next elements
78            int currentPos = valueToPosition.get(nums[k]);
79            int nextPos = valueToPosition.get(nums[k + 1]);
80          
81            // Calculate distance between positions, excluding already removed elements
82            // Query uses 1-indexed positions, so we add 1
83            long distance = nextPos - currentPos - 
84                          (removedTracker.query(nextPos + 1) - removedTracker.query(currentPos + 1));
85          
86            // Add distance to total operations
87            // If next element is before current (wrapped around), add remaining elements count
88            totalOperations += distance + (n - k) * (currentPos > nextPos ? 1 : 0);
89          
90            // Mark current element as removed (1-indexed for BIT)
91            removedTracker.update(currentPos + 1, 1);
92        }
93      
94        return totalOperations;
95    }
96}
97
1class BinaryIndexedTree {
2public:
3    // Constructor: Initialize BIT with size n
4    BinaryIndexedTree(int n) : size(n), tree(n + 1, 0) {}
5
6    // Update: Add delta to position index (1-indexed internally)
7    void update(int index, int delta) {
8        // Propagate the update up the tree using lowbit operation
9        while (index <= size) {
10            tree[index] += delta;
11            index += index & (-index);  // Move to next affected node
12        }
13    }
14
15    // Query: Get prefix sum from 1 to index (1-indexed internally)
16    int query(int index) {
17        int sum = 0;
18        // Accumulate values down the tree using lowbit operation
19        while (index > 0) {
20            sum += tree[index];
21            index -= index & (-index);  // Move to parent node
22        }
23        return sum;
24    }
25
26private:
27    int size;           // Size of the array
28    vector<int> tree;   // Binary Indexed Tree array (1-indexed)
29};
30
31class Solution {
32public:
33    long long countOperationsToEmptyArray(vector<int>& nums) {
34        // Map each value to its original position
35        unordered_map<int, int> valueToPosition;
36        int n = nums.size();
37        for (int i = 0; i < n; ++i) {
38            valueToPosition[nums[i]] = i;
39        }
40      
41        // Sort the array to process elements in ascending order
42        sort(nums.begin(), nums.end());
43      
44        // Initialize Binary Indexed Tree to track removed elements
45        BinaryIndexedTree fenwickTree(n);
46      
47        // Initialize answer with operations to remove the smallest element
48        long long totalOperations = valueToPosition[nums[0]] + 1;
49      
50        // Process remaining elements in ascending order
51        for (int k = 0; k < n - 1; ++k) {
52            // Get positions of current and next elements to remove
53            int currentPos = valueToPosition[nums[k]];
54            int nextPos = valueToPosition[nums[k + 1]];
55          
56            // Calculate distance between positions, excluding already removed elements
57            int removedBetween = fenwickTree.query(nextPos + 1) - fenwickTree.query(currentPos + 1);
58            long long distance = nextPos - currentPos - removedBetween;
59          
60            // Add operations needed:
61            // - distance to move to next element
62            // - if nextPos < currentPos, need to cycle through remaining elements
63            totalOperations += distance + (long long)(n - k) * (currentPos > nextPos);
64          
65            // Mark current position as removed in the BIT
66            fenwickTree.update(currentPos + 1, 1);
67        }
68      
69        return totalOperations;
70    }
71};
72
1// Binary Indexed Tree (Fenwick Tree) for efficient range sum queries
2let n: number;
3let c: number[];
4
5// Initialize the Binary Indexed Tree with size n
6function initBinaryIndexedTree(size: number): void {
7    n = size;
8    c = Array(size + 1).fill(0);
9}
10
11// Update the value at position x by adding v
12// Uses the lowbit technique (x & -x) to navigate the tree
13function update(x: number, v: number): void {
14    while (x <= n) {
15        c[x] += v;
16        x += x & -x;  // Move to next index by adding lowbit
17    }
18}
19
20// Query the prefix sum from index 1 to x
21// Traverses the tree using lowbit to accumulate the sum
22function query(x: number): number {
23    let sum = 0;
24    while (x > 0) {
25        sum += c[x];
26        x -= x & -x;  // Move to parent index by subtracting lowbit
27    }
28    return sum;
29}
30
31// Count the minimum number of operations to empty an array
32// Operations: remove the smallest element or move elements to the end
33function countOperationsToEmptyArray(nums: number[]): number {
34    // Map to store the original position of each element
35    const positionMap: Map<number, number> = new Map();
36    const arrayLength = nums.length;
37  
38    // Store original positions of all elements
39    for (let i = 0; i < arrayLength; ++i) {
40        positionMap.set(nums[i], i);
41    }
42  
43    // Sort the array to process elements in ascending order
44    nums.sort((a, b) => a - b);
45  
46    // Initialize Binary Indexed Tree
47    initBinaryIndexedTree(arrayLength);
48  
49    // Start with operations needed to remove the first (smallest) element
50    let totalOperations = positionMap.get(nums[0])! + 1;
51  
52    // Process remaining elements in sorted order
53    for (let k = 0; k < arrayLength - 1; ++k) {
54        // Get positions of current and next elements
55        const currentPos = positionMap.get(nums[k])!;
56        const nextPos = positionMap.get(nums[k + 1])!;
57      
58        // Calculate distance between positions, excluding already removed elements
59        let distance = nextPos - currentPos - (query(nextPos + 1) - query(currentPos + 1));
60      
61        // If next element is before current position, need to wrap around
62        if (currentPos > nextPos) {
63            distance += arrayLength - k;  // Add remaining unprocessed elements
64        }
65      
66        totalOperations += distance;
67      
68        // Mark current position as removed in the Binary Indexed Tree
69        update(currentPos + 1, 1);
70    }
71  
72    return totalOperations;
73}
74

Time and Space Complexity

Time Complexity: O(n × log n)

The time complexity is dominated by the following operations:

  • Creating the position dictionary pos: O(n) where we iterate through all elements once
  • Sorting the nums array: O(n × log n) using Python's built-in sort
  • The main loop iterates through n-1 pairs of consecutive elements: O(n) iterations
  • Inside each iteration:
    • Two bisect operations on the SortedList: each takes O(log k) where k is the current size of the sorted list (at most n)
    • One add operation to the SortedList: O(log k)
    • Other operations like dictionary lookups and arithmetic: O(1)

Since the loop runs n times and each iteration performs operations that cost O(log n) in the worst case, the total time complexity for the loop is O(n × log n).

Therefore, the overall time complexity is O(n) + O(n × log n) + O(n × log n) = O(n × log n).

Space Complexity: O(n)

The space complexity comes from:

  • The pos dictionary storing position mappings for all n elements: O(n)
  • The sorted copy of nums (in-place sort still requires O(n) for Timsort in worst case): O(n)
  • The SortedList which can store up to n-1 elements: O(n)
  • Other variables like ans, i, j, d, etc.: O(1)

The total space complexity is O(n) + O(n) + O(n) + O(1) = O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrectly Calculating Distance Between Elements

A critical pitfall is misunderstanding how to calculate the actual distance between two elements after some positions have been removed. Many attempts incorrectly compute this as simply j - i without accounting for the gaps created by removed elements.

Incorrect approach:

distance = next_position - current_position  # Wrong! Doesn't account for removed elements

Correct approach:

removed_between = removed_positions.bisect(next_position) - removed_positions.bisect(current_position)
distance = next_position - current_position - removed_between

The key insight is that when elements are removed, they create "gaps" in the array that don't contribute to the movement count. We must subtract the number of already-removed positions between our current and target positions.

2. Mishandling the Wrap-Around Case

Another common mistake is incorrectly determining when and how to handle wrap-around scenarios. When the next element to remove is at a position earlier than the current one (in the original array), we need to wrap around to reach it.

Incorrect approach:

if current_position > next_position:
    total_operations += array_length  # Wrong! Should use remaining elements

Correct approach:

if current_position > next_position:
    remaining_elements = array_length - removal_index
    total_operations += remaining_elements

The wrap-around cost isn't the full array length but rather the number of elements still remaining in the array at that point. Since removal_index represents how many elements we've already removed minus one, the remaining count is array_length - removal_index.

3. Off-by-One Error in Initial Operation Count

A subtle mistake is forgetting to add 1 when counting the initial operations needed to remove the first element.

Incorrect approach:

total_operations = value_to_original_position[sorted_values[0]]  # Missing the removal operation itself

Correct approach:

total_operations = value_to_original_position[sorted_values[0]] + 1

The position gives us the number of rotations needed to bring the element to the front, but we also need one additional operation to actually remove it.

4. Using Wrong Data Structure for Tracking Removed Positions

Using a regular set or list instead of a SortedList leads to inefficient queries when determining how many elements have been removed in a range.

Incorrect approach:

removed_positions = set()  # Can't efficiently count elements in a range
# Later...
removed_between = sum(1 for p in removed_positions if current_position < p < next_position)  # O(n) per query!

Correct approach:

removed_positions = SortedList()  # Maintains sorted order
removed_between = removed_positions.bisect(next_position) - removed_positions.bisect(current_position)  # O(log n)

The SortedList allows binary search operations, making range queries logarithmic rather than linear in complexity.

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

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

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

Load More