Problem overview
The function sortArray(nums) sorts a slice of integers in ascending order using Quick Sort. It returns nothing and rearranges nums itself. The statement asks for O(n log n) time and O(log n) extra space.
For nums = [3, 1, 3, 2, 5, 4] the slice becomes [1, 2, 3, 3, 4, 5].
Approach: partition around a pivot, then sort both sides
Quick Sort picks one element, the pivot, and rearranges the range so that everything not larger than the pivot sits before it and everything larger sits after it. This step is called partitioning. After it, the pivot is in its final position, and the two sides can be sorted independently, with the same function, without ever merging.
Compare this with Merge Sort. Merge Sort does its cheap work after the recursive calls, by merging, and needs a buffer of n elements. Quick Sort does the work before the recursive calls, by partitioning, and rearranges the elements in place. Its only extra memory is the call stack.
The running time depends on how evenly the pivot splits the range. If each pivot lands near the middle, there are about log2(n) levels of O(n) partitioning work, which is O(n log n). If the pivot is always the smallest or largest element, one side is empty each time and the work grows to O(n²). A fixed choice such as always taking the last element does exactly that on input that is already sorted. Choosing the pivot at random makes bad splits unlikely whatever the input order, so the expected time is O(n log n) and the expected recursion depth is O(log n).
Step by step
- Start the recursion.
sortArraycallsquickSort(nums, 0, len(nums)-1). Both bounds are inclusive. - Declare
quickSortwithnums []intandleft, right int. A slice shares its underlying array, so changes made inside are visible to the caller. - Base case. When
left >= right, the range has at most one element and is already sorted, so return. - Partition. Call
partition(nums, left, right)and store the pivot's final index inpivot. - Sort the left side, from
lefttopivot - 1. - Sort the right side, from
pivot + 1toright. The pivot is excluded from both calls because it is already in place. - Declare
partitionwith the same parameters, returning anint. - Pick a random index between
leftandright.rand.Intn(right-left+1)returns 0 toright-left, and addingleftshifts it into the range. - Move the pivot to the end with a tuple assignment,
nums[randomIndex], nums[right] = nums[right], nums[randomIndex]. Go evaluates the right side first, so no temporary variable is needed. - Remember the pivot value in
value. - Track the boundary.
correctIndexstarts atleft. It is where the next element that is at mostvaluebelongs, and everything before it already is. - Scan the range with
ifromlefttoright, includingright. - Move small elements left. If
nums[i] <= value, swapnums[i]withnums[correctIndex]and increasecorrectIndex. - Return
correctIndex - 1. The pivot itself, at indexright, is the last element moved to the front, so it ends just beforecorrectIndex.
The solution in Go
func sortArray(nums []int) {
quickSort(nums, 0, len(nums)-1)
}
func quickSort(nums []int, left, right int) {
if left >= right {
return
}
pivot := partition(nums, left, right)
quickSort(nums, left, pivot-1)
quickSort(nums, pivot+1, right)
}
func partition(nums []int, left, right int) int {
randomIndex := left + rand.Intn(right-left+1)
nums[randomIndex], nums[right] = nums[right], nums[randomIndex]
value := nums[right]
correctIndex := left
for i := left; i <= right; i++ {
if nums[i] <= value {
nums[i], nums[correctIndex] = nums[correctIndex], nums[i]
correctIndex++
}
}
return correctIndex - 1
}
Here is one partition by hand. Take [4, 7, 1, 9, 3, 6] and suppose the random pivot is already at the end, so value is 6. The scan swaps 4, 1 and 3 to the front and then the pivot itself, giving [4, 1, 3, 6, 7, 9]. The function returns 3. Everything before index 3 is at most 6 and everything after it is larger.
Go details worth knowing:
- The import.
rand.Intncomes from themath/randpackage, which a complete program must import. Since Go 1.20 that package seeds itself randomly, so no seeding call is needed. - Swapping.
a, b = b, ais a single statement. When the two positions are the same element, as wheniequalscorrectIndex, the swap does nothing harmful. - Sharing the array. Passing
numscopies only the slice header. All the calls work on the same elements, so no copying happens and no slice is returned. - The loop includes
right. The pivot at the end satisfiesnums[i] <= value, so it is swapped into its place last. That is why the pivot ends atcorrectIndex - 1. - The empty case is safe. For an empty slice the first call has
right = -1and returns beforepartitionasks the random generator for a number.
Complexity
Time: expected O(n log n). Each partition is a single pass over its range, and with random pivots the ranges shrink geometrically on average. The worst case is O(n²), but the random choice makes it extremely unlikely on ordinary input.
Space: expected O(log n) for the call stack. Nothing else is allocated.
Edge cases and common mistakes
- Empty slice and single element. Both end at the base case.
- Duplicates.
[3, 1, 3, 2, 5, 4]sorts correctly because elements equal to the pivot go to its left side. A slice where every element is equal is the exception to the speed guarantee: each partition moves all elements to the left, so the pivot ends atrightand the recursion shrinks by one element per call. With 2,000 equal elements the partitions scan about two million positions, which is quadratic. Handling this takes a three-way partition, which this lesson does not use. - Negative numbers. Only comparisons are used, so sign doesn't matter.
[-3, 8, 0, -3, 2]becomes[-3, -3, 0, 2, 8]. - Looping to
right - 1. That variant needs a final swap to put the pivot in place. This lesson's loop includesrightand avoids the extra step. - Recursing on the pivot. Calling
quickSort(nums, left, pivot)can leave the range unchanged and recurse forever. Usepivot - 1andpivot + 1. - A non-random pivot. Always using
nums[right]works but degrades to O(n²) on input that is already sorted. - Unstable sort. Equal elements can change their relative order. For plain integers that is invisible.
Testing
[3, 1, 3, 2, 5, 4] gives [1, 2, 3, 3, 4, 5]. An empty slice stays empty, [5, 5, 5, 5] stays the same, and [-3, 8, 0, -3, 2] gives [-3, -3, 0, 2, 8].