Facebook Pixel

912. Sort an Array

Problem Description

You need to sort an array of integers in ascending order (from smallest to largest). The challenge is that you cannot use any built-in sorting functions and must achieve a time complexity of O(n log n) while using the smallest space complexity possible.

Given an input array like [5, 2, 3, 1], you should return [1, 2, 3, 5].

The solution implements a three-way partitioning quicksort algorithm. This approach works by:

  1. Randomly selecting a pivot element x from the current range [l, r]
  2. Partitioning the array into three sections:
    • Elements less than x (moved to the left)
    • Elements equal to x (kept in the middle)
    • Elements greater than x (moved to the right)
  3. Using three pointers i, j, and k:
    • i tracks the boundary of elements less than x
    • j tracks the boundary of elements greater than x
    • k iterates through the unprocessed elements
  4. Recursively sorting the left partition [l, i] and right partition [j, r]

The three-way partitioning is particularly efficient when the array contains duplicate values, as all elements equal to the pivot are placed in their final sorted position in a single pass. This avoids unnecessary recursive calls on elements that are already correctly positioned.

The random pivot selection helps avoid worst-case O(n²) performance that can occur with deterministic pivot selection on already sorted or reverse-sorted arrays, ensuring the expected O(n log n) time complexity.

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

Intuition

When we need to sort an array in O(n log n) time without built-in functions, we think about classic efficient sorting algorithms: merge sort, heap sort, or quicksort. Each has trade-offs in terms of space complexity and implementation complexity.

The key insight here is recognizing that quicksort, when implemented with in-place partitioning, achieves the best space complexity - O(log n) for the recursion stack in the average case. This makes it ideal for meeting the "smallest space complexity possible" requirement.

However, standard quicksort has a weakness: it can degrade to O(n²) time complexity in worst cases (like already sorted arrays). We address this by randomly selecting the pivot, which probabilistically ensures O(n log n) expected time complexity.

The solution takes this a step further by using three-way partitioning instead of the traditional two-way partition. Why? Consider an array with many duplicate values like [3, 1, 3, 4, 3, 2, 3]. With standard quicksort, if we pick 3 as the pivot, we'd split into "less than 3" and "greater than or equal to 3", meaning we'd have to recursively sort all those duplicate 3s again in the next iteration.

Three-way partitioning elegantly solves this by creating three regions: < pivot, = pivot, and > pivot. All elements equal to the pivot are placed in their final sorted position in one pass. This optimization is particularly powerful for arrays with many duplicates, reducing unnecessary recursive calls.

The implementation uses a clever single-pass approach with three pointers that simultaneously partition the array while maintaining the invariants that everything before i is less than the pivot, everything after j is greater than the pivot, and k tracks our current scanning position.

Learn more about Divide and Conquer, Sorting, Heap (Priority Queue) and Merge Sort patterns.

Solution Approach

The implementation uses a modified quicksort with three-way partitioning. Let's walk through how it works:

Main Structure: The solution defines a recursive helper function quick_sort(l, r) that sorts the subarray from index l to r inclusive. The base case checks if l >= r, meaning we have one element or an invalid range, so we return immediately.

Pivot Selection:

x = nums[randint(l, r)]

We randomly select a pivot element x from the current range. This randomization is crucial for avoiding worst-case performance on sorted or nearly-sorted inputs.

Three-Way Partitioning Setup:

i, j, k = l - 1, r + 1, l
  • i = l - 1: Points to the last element less than the pivot (starts before the range)
  • j = r + 1: Points to the first element greater than the pivot (starts after the range)
  • k = l: Current element being examined

Partitioning Process: The main loop while k < j processes each unexamined element. For each nums[k], we have three cases:

  1. Element less than pivot (nums[k] < x):

    nums[i + 1], nums[k] = nums[k], nums[i + 1]
    i, k = i + 1, k + 1
    • Swap with the element at i + 1 (first element in the "equal" region)
    • Increment both i and k to expand the "less than" region
  2. Element greater than pivot (nums[k] > x):

    j -= 1
    nums[j], nums[k] = nums[k], nums[j]
    • Decrement j first to shrink the "unprocessed" region
    • Swap with the element at the new j position
    • Don't increment k since we need to examine the swapped element
  3. Element equal to pivot:

    k = k + 1
    • Simply move to the next element, leaving it in place

Recursive Calls:

quick_sort(l, i)
quick_sort(j, r)

After partitioning, the array has three regions:

  • [l, i]: Elements less than the pivot
  • [i+1, j-1]: Elements equal to the pivot (already in correct position)
  • [j, r]: Elements greater than the pivot

We recursively sort only the first and third regions, skipping the middle region entirely since equal elements are already correctly positioned.

Time and Space Complexity:

  • Time: O(n log n) expected, O(n²) worst case (mitigated by random pivot)
  • Space: O(log n) for recursion stack in average case, O(n) worst case

The three-way partitioning shines when there are duplicate elements, as it reduces the problem size more aggressively than standard two-way partitioning.

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 algorithm with the array [3, 1, 3, 2, 3, 4].

Initial Call: quick_sort(0, 5)

  • Range: entire array [3, 1, 3, 2, 3, 4]
  • Let's say random pivot selects nums[2] = 3
  • Initialize: i = -1, j = 6, k = 0

Partitioning Process:

Step 1: k = 0, examining nums[0] = 3

  • 3 == 3 (equal to pivot)
  • Action: k = 1
  • Array: [3, 1, 3, 2, 3, 4] (unchanged)
  • Pointers: i = -1, j = 6, k = 1

Step 2: k = 1, examining nums[1] = 1

  • 1 < 3 (less than pivot)
  • Action: Swap nums[0] and nums[1], increment i and k
  • Array: [1, 3, 3, 2, 3, 4]
  • Pointers: i = 0, j = 6, k = 2

Step 3: k = 2, examining nums[2] = 3

  • 3 == 3 (equal to pivot)
  • Action: k = 3
  • Array: [1, 3, 3, 2, 3, 4] (unchanged)
  • Pointers: i = 0, j = 6, k = 3

Step 4: k = 3, examining nums[3] = 2

  • 2 < 3 (less than pivot)
  • Action: Swap nums[1] and nums[3], increment i and k
  • Array: [1, 2, 3, 3, 3, 4]
  • Pointers: i = 1, j = 6, k = 4

Step 5: k = 4, examining nums[4] = 3

  • 3 == 3 (equal to pivot)
  • Action: k = 5
  • Array: [1, 2, 3, 3, 3, 4] (unchanged)
  • Pointers: i = 1, j = 6, k = 5

Step 6: k = 5, examining nums[5] = 4

  • 4 > 3 (greater than pivot)
  • Action: Decrement j to 5, swap nums[5] with itself
  • Array: [1, 2, 3, 3, 3, 4] (unchanged)
  • Pointers: i = 1, j = 5, k = 5

Loop ends since k >= j

After partitioning:

  • [1, 2] (indices 0-1): elements < 3
  • [3, 3, 3] (indices 2-4): elements = 3 (already sorted!)
  • [4] (index 5): elements > 3

Recursive Calls:

  1. quick_sort(0, 1) to sort [1, 2]
    • Pivot: 2, partitions to [1] and [2]
    • Further recursive calls on single elements return immediately
  2. quick_sort(5, 5) to sort [4]
    • Single element, returns immediately

Final Result: [1, 2, 3, 3, 3, 4]

Notice how all three 3s were placed in their final positions in a single partitioning pass - this is the power of three-way partitioning!

Solution Implementation

1from random import randint
2from typing import List
3
4class Solution:
5    def sortArray(self, nums: List[int]) -> List[int]:
6        """
7        Sort an array using 3-way quicksort algorithm.
8      
9        Args:
10            nums: List of integers to be sorted
11          
12        Returns:
13            Sorted list of integers
14        """
15      
16        def three_way_quick_sort(left: int, right: int) -> None:
17            """
18            Perform 3-way quicksort on nums[left:right+1].
19            This handles duplicates efficiently by partitioning into three regions:
20            - Elements less than pivot
21            - Elements equal to pivot
22            - Elements greater than pivot
23          
24            Args:
25                left: Starting index of the subarray
26                right: Ending index of the subarray (inclusive)
27            """
28            # Base case: if subarray has 0 or 1 element
29            if left >= right:
30                return
31          
32            # Choose a random pivot to avoid worst-case O(n²) on sorted arrays
33            pivot = nums[randint(left, right)]
34          
35            # Initialize three pointers:
36            # i: boundary of elements < pivot (starts before left)
37            # j: boundary of elements > pivot (starts after right)
38            # k: current element being examined
39            i = left - 1
40            j = right + 1
41            k = left
42          
43            # Partition the array into three regions
44            while k < j:
45                if nums[k] < pivot:
46                    # Move element to the "less than" region
47                    i += 1
48                    nums[i], nums[k] = nums[k], nums[i]
49                    k += 1
50                elif nums[k] > pivot:
51                    # Move element to the "greater than" region
52                    j -= 1
53                    nums[j], nums[k] = nums[k], nums[j]
54                    # Don't increment k as we need to examine the swapped element
55                else:
56                    # Element equals pivot, just move to next element
57                    k += 1
58          
59            # Recursively sort the "less than" and "greater than" regions
60            # Elements equal to pivot are already in correct position
61            three_way_quick_sort(left, i)      # Sort elements < pivot
62            three_way_quick_sort(j, right)     # Sort elements > pivot
63      
64        # Sort the entire array in-place
65        three_way_quick_sort(0, len(nums) - 1)
66        return nums
67
1class Solution {
2    private int[] nums;
3
4    /**
5     * Sorts an array using quicksort algorithm
6     * @param nums the array to be sorted
7     * @return the sorted array
8     */
9    public int[] sortArray(int[] nums) {
10        this.nums = nums;
11        quickSort(0, nums.length - 1);
12        return nums;
13    }
14
15    /**
16     * Recursive quicksort implementation using two-pointer partitioning
17     * @param left the left boundary of the subarray to sort
18     * @param right the right boundary of the subarray to sort
19     */
20    private void quickSort(int left, int right) {
21        // Base case: if subarray has 0 or 1 element, it's already sorted
22        if (left >= right) {
23            return;
24        }
25      
26        // Choose middle element as pivot
27        int pivot = nums[(left + right) >> 1];
28      
29        // Initialize two pointers outside the array bounds
30        int i = left - 1;
31        int j = right + 1;
32      
33        // Partition the array around the pivot
34        while (i < j) {
35            // Find element from left that is >= pivot
36            while (nums[++i] < pivot) {
37                // Keep moving right until we find an element >= pivot
38            }
39          
40            // Find element from right that is <= pivot
41            while (nums[--j] > pivot) {
42                // Keep moving left until we find an element <= pivot
43            }
44          
45            // Swap elements if pointers haven't crossed
46            if (i < j) {
47                int temp = nums[i];
48                nums[i] = nums[j];
49                nums[j] = temp;
50            }
51        }
52      
53        // Recursively sort the left partition (left to j)
54        quickSort(left, j);
55      
56        // Recursively sort the right partition (j+1 to right)
57        quickSort(j + 1, right);
58    }
59}
60
1class Solution {
2public:
3    vector<int> sortArray(vector<int>& nums) {
4        // Lambda function for quicksort implementation
5        function<void(int, int)> quickSort = [&](int left, int right) {
6            // Base case: if left index >= right index, subarray has 0 or 1 element
7            if (left >= right) {
8                return;
9            }
10          
11            // Initialize two pointers for partitioning
12            // i starts before the left boundary, j starts after the right boundary
13            int i = left - 1;
14            int j = right + 1;
15          
16            // Choose the middle element as pivot
17            int pivot = nums[(left + right) >> 1];  // Bit shift for division by 2
18          
19            // Partition the array around the pivot
20            while (i < j) {
21                // Move i pointer right until we find an element >= pivot
22                while (nums[++i] < pivot) {
23                    // Empty body - increment happens in condition
24                }
25              
26                // Move j pointer left until we find an element <= pivot
27                while (nums[--j] > pivot) {
28                    // Empty body - decrement happens in condition
29                }
30              
31                // If pointers haven't crossed, swap the elements
32                if (i < j) {
33                    swap(nums[i], nums[j]);
34                }
35            }
36          
37            // Recursively sort the left partition (left to j)
38            quickSort(left, j);
39          
40            // Recursively sort the right partition (j+1 to right)
41            quickSort(j + 1, right);
42        };
43      
44        // Start the quicksort on the entire array
45        quickSort(0, nums.size() - 1);
46      
47        // Return the sorted array
48        return nums;
49    }
50};
51
1/**
2 * Sorts an array of numbers in ascending order using QuickSort algorithm
3 * @param nums - The array of numbers to be sorted
4 * @returns The sorted array
5 */
6function sortArray(nums: number[]): number[] {
7    /**
8     * Recursive QuickSort implementation using Hoare partition scheme
9     * @param left - The left boundary index of the subarray
10     * @param right - The right boundary index of the subarray
11     */
12    function quickSort(left: number, right: number): void {
13        // Base case: if subarray has 1 or fewer elements, it's already sorted
14        if (left >= right) {
15            return;
16        }
17      
18        // Initialize two pointers for partitioning
19        let i: number = left - 1;
20        let j: number = right + 1;
21      
22        // Choose the middle element as pivot
23        const pivot: number = nums[(left + right) >> 1];
24      
25        // Partition the array around the pivot
26        while (i < j) {
27            // Move left pointer right until finding element >= pivot
28            while (nums[++i] < pivot);
29          
30            // Move right pointer left until finding element <= pivot
31            while (nums[--j] > pivot);
32          
33            // Swap elements if pointers haven't crossed
34            if (i < j) {
35                [nums[i], nums[j]] = [nums[j], nums[i]];
36            }
37        }
38      
39        // Recursively sort left partition (elements <= pivot)
40        quickSort(left, j);
41      
42        // Recursively sort right partition (elements > pivot)
43        quickSort(j + 1, right);
44    }
45  
46    const arrayLength: number = nums.length;
47  
48    // Initiate the sorting process on the entire array
49    quickSort(0, arrayLength - 1);
50  
51    return nums;
52}
53

Time and Space Complexity

Time Complexity: O(n log n) on average, O(n²) in the worst case

The code implements a three-way partitioning quicksort algorithm.

  • In the average case, the pivot divides the array into roughly equal parts at each recursion level, creating a recursion tree of depth log n. At each level, we perform O(n) work for partitioning, resulting in O(n log n) total time.
  • In the worst case (despite random pivot selection), if we consistently get poor pivot choices that create highly unbalanced partitions, the recursion depth becomes O(n), leading to O(n²) time complexity.
  • The three-way partitioning optimization helps when there are many duplicate elements by grouping equal elements together in one pass, potentially reducing the number of recursive calls.

Space Complexity: O(log n) on average, O(n) in the worst case

The space complexity comes from the recursive call stack:

  • In the average case with balanced partitions, the maximum recursion depth is O(log n).
  • In the worst case with unbalanced partitions, the recursion can go as deep as O(n).
  • The algorithm sorts in-place without using additional arrays, so only the call stack contributes to space complexity.
  • Each recursive call uses O(1) additional space for local variables (x, i, j, k).

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

Common Pitfalls

1. Incorrect Pointer Management After Swapping

One of the most common mistakes in three-way partitioning is incorrectly managing the k pointer after swapping elements. Specifically, when swapping an element greater than the pivot:

Incorrect approach:

elif nums[k] > pivot:
    j -= 1
    nums[j], nums[k] = nums[k], nums[j]
    k += 1  # WRONG! Should not increment k

Why it's wrong: When we swap nums[k] with nums[j-1], the element that was at position j-1 is now at position k. We haven't examined this element yet, so we need to check it against the pivot. Incrementing k would skip this unexamined element.

Correct approach:

elif nums[k] > pivot:
    j -= 1
    nums[j], nums[k] = nums[k], nums[j]
    # Don't increment k - we need to examine the swapped element

2. Off-by-One Errors in Boundary Initialization

Another frequent mistake is incorrectly initializing the boundaries:

Incorrect initialization:

i, j, k = l, r, l  # WRONG! i should be l-1, j should be r+1

Why it's wrong:

  • i should track the last element less than the pivot, so it starts at l-1 (before the range)
  • j should track the first element greater than the pivot, so it starts at r+1 (after the range)

Correct initialization:

i, j, k = l - 1, r + 1, l

3. Forgetting to Handle Edge Cases

Missing base case:

def three_way_quick_sort(left, right):
    # Forgetting the base case leads to infinite recursion
    pivot = nums[randint(left, right)]  # This will fail when left > right
    # ... rest of the code

Correct approach:

def three_way_quick_sort(left, right):
    if left >= right:  # Essential base case
        return
    pivot = nums[randint(left, right)]
    # ... rest of the code

4. Using Fixed Pivot Selection

Poor pivot selection:

pivot = nums[left]  # Always choosing first element as pivot

Why it's problematic: This leads to O(n²) worst-case performance on already sorted or reverse-sorted arrays, which are common in real-world scenarios.

Better approach:

pivot = nums[randint(left, right)]  # Random pivot selection

5. Incorrect Recursive Calls

Wrong recursive boundaries:

# After partitioning with final i and j values
three_way_quick_sort(left, i - 1)  # WRONG! Should be (left, i)
three_way_quick_sort(j + 1, right)  # WRONG! Should be (j, right)

Why it's wrong: After the partitioning loop:

  • Elements from left to i (inclusive) are less than the pivot
  • Elements from j to right (inclusive) are greater than the pivot
  • The boundaries should include these positions, not exclude them

Correct recursive calls:

three_way_quick_sort(left, i)
three_way_quick_sort(j, right)

Solution to Avoid These Pitfalls:

  1. Test with small arrays to verify pointer movement: [2, 1, 2, 2, 3]
  2. Trace through the algorithm step-by-step with paper and pencil
  3. Add assertions during development to verify invariants:
    assert left <= i + 1 <= k <= j <= right + 1
  4. Test edge cases: empty array, single element, all duplicates, already sorted
  5. Visualize the three regions as you code: [< pivot | = pivot | unprocessed | > pivot]
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

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

Load More