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:
- Randomly selecting a pivot element
x
from the current range[l, r]
- 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)
- Elements less than
- Using three pointers
i
,j
, andk
:i
tracks the boundary of elements less thanx
j
tracks the boundary of elements greater thanx
k
iterates through the unprocessed elements
- 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.
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 3
s 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:
-
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
andk
to expand the "less than" region
- Swap with the element at
-
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
- Decrement
-
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 EvaluatorExample 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]
andnums[1]
, incrementi
andk
- 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]
andnums[3]
, incrementi
andk
- 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, swapnums[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:
quick_sort(0, 1)
to sort[1, 2]
- Pivot: 2, partitions to
[1]
and[2]
- Further recursive calls on single elements return immediately
- Pivot: 2, partitions to
quick_sort(5, 5)
to sort[4]
- Single element, returns immediately
Final Result: [1, 2, 3, 3, 3, 4]
Notice how all three 3
s 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 performO(n)
work for partitioning, resulting inO(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 toO(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 atl-1
(before the range)j
should track the first element greater than the pivot, so it starts atr+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
toi
(inclusive) are less than the pivot - Elements from
j
toright
(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:
- Test with small arrays to verify pointer movement:
[2, 1, 2, 2, 3]
- Trace through the algorithm step-by-step with paper and pencil
- Add assertions during development to verify invariants:
assert left <= i + 1 <= k <= j <= right + 1
- Test edge cases: empty array, single element, all duplicates, already sorted
- Visualize the three regions as you code:
[< pivot | = pivot | unprocessed | > pivot]
Which of the following array represent a max heap?
Recommended Readings
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
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
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!