Start learning free

LESSON 287 OF 415 · DIVIDE AND CONQUER

Quick Sort in Swift: Random Pivot, O(n log n) Expected Time

Practice O(n log n) Read the full explanation

In this video lesson we will study how the Quick Sort algorithm works, we will analyze its time and space complexities and then we will implement it:


Problem - Quick Sort:

Given an array of integers nums, sort it in ascending order using Quick Sort

Example 1:

Input: nums = [3, 1, 3, 2, 5, 4]
Output: [1, 2, 3, 3, 4, 5]

Note:

Your algorithm should run in O(n log n) time on average and use O(log n) extra space on average (for the recursion).


STEP 1 OF 18
    Ctrl + Enter to check

    SWIFT · DIVIDE AND CONQUER

    Quick Sort in Swift, explained

    Problem overview

    The method sort receives an array of integers, nums, as an inout parameter and must sort it in ascending order with Quick Sort. The statement asks for O(n log n) time and O(log n) extra space. The sorted result replaces the contents of the caller's array.

    For nums = [3, 1, 3, 2, 5, 4] the array 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 every element not larger than the pivot comes before it and every larger element after it. This is partitioning. The pivot is then in its final position, and nothing has to cross it again. What is left is two smaller sorting problems, one on each side of the pivot, solved with the same method.

    Unlike Merge Sort, Quick Sort works inside the array and needs no second array. The extra memory is only the recursion stack.

    The speed depends on the pivot. A pivot near the middle halves the problem, giving about log₂ n levels of O(n) partitioning work each, so O(n log n). A pivot that is always the smallest or largest value peels off one element per level and costs O(n²). Always picking the first or last element makes that happen on already sorted input. The lesson picks a random index, so no particular input is reliably bad, and O(n log n) holds in expectation.

    Take [3, 1, 3, 2, 5, 4] with 3 as the pivot value. Four elements (3, 1, 3, 2) are not larger than 3, so after partitioning they fill indices 0 to 3, the pivot's final index is 3, and 5 and 4 sit to its right. Both sides are then sorted the same way.

    Step by step

    1. Start the recursion from sort with quickSort(&nums, 0, nums.count - 1), which covers the whole array.
    2. Declare quickSort with an inout [Int] and the bounds left and right, without labels.
    3. Base case: if left >= right, the range has at most one element, so return. This also covers the empty ranges that come from pivot - 1 and pivot + 1.
    4. Partition the range and store the pivot's final index in pivot.
    5. Sort the left part, left to pivot - 1, and then the right part, pivot + 1 to right. The pivot is left out because it is already in place.
    6. Write partition. It takes the same three parameters and returns an Int.
    7. Choose a random pivot with Int.random(in: left...right) and swap it to the end of the range, so the loop that follows can treat all other elements uniformly.
    8. Remember the pivot value as value, and start correctIndex at left. Everything before correctIndex is already not larger than value.
    9. Scan from left through right inclusive. Each element that is <= value is swapped to correctIndex, and correctIndex grows by one.
    10. Return correctIndex - 1. The pivot sits at the end and is <= value, so it is the last element moved into the left group, just before correctIndex.
    11. Write swap, which exchanges two elements through a temporary constant.

    The solution in Swift

    class Solution {
        func swap(_ nums: inout [Int], _ i: Int, _ j: Int) {
            let temp = nums[i]
            nums[i] = nums[j]
            nums[j] = temp
        }
        func partition(_ nums: inout [Int], _ left: Int, _ right: Int) -> Int {
            let randomIndex = Int.random(in: left...right)
            swap(&nums, randomIndex, right)
            let value = nums[right]
            var correctIndex = left
            for i in left...right {
                if nums[i] <= value {
                    swap(&nums, i, correctIndex)
                    correctIndex += 1
                }
            }
            return correctIndex - 1
        }
        func quickSort(_ nums: inout [Int], _ left: Int, _ right: Int) {
            if left >= right {
                return
            }
            let pivot = partition(&nums, left, right)
            quickSort(&nums, left, pivot - 1)
            quickSort(&nums, pivot + 1, right)
        }
        func sort(_ nums: inout [Int]) {
            quickSort(&nums, 0, nums.count - 1)
        }
    }
    

    The Swift details worth a closer look:

    • inout and &. Swift parameters are constants unless marked inout, and each call must pass the array with &. Without it the compiler reports passing value of type '[Int]' to an inout parameter requires explicit '&'.
    • A method called swap. Swift also has a global swap(_:_:) function. Inside the class, the call swap(&nums, i, j) finds the class's own method first, so the three-argument version above is the one that runs. The global function takes two inout values and would need different code.
    • Closed ranges. Int.random(in: left...right) and the for loop over left...right both include right. The loop visits the pivot last on purpose, and moving it into the left group is what puts it in its final position.
    • Swapping an element with itself. When i equals correctIndex, swap(&nums, i, i) changes nothing and is harmless.
    • No ++. Swift has no increment operator, so the lesson writes correctIndex += 1.
    • Empty ranges. quickSort may be called with right smaller than left, such as pivot - 1 when the pivot is at left. The base case catches it before left...right is ever formed. A closed range with a lower bound above its upper bound would crash the program.

    Complexity

    Time: O(n log n) expected. One partition touches each element of its range once. With a random pivot the ranges shrink geometrically on average, giving about log n levels, so the total is O(n log n). The worst case is still O(n²), but the random choice makes it very unlikely on any input.

    Space: O(log n) expected. Sorting happens in place. The only extra memory is the recursion stack, whose depth follows the number of levels, about log n when pivots are reasonably balanced.

    Edge cases and common mistakes

    • Empty and single-element arrays. For an empty array the first call is quickSort(&nums, 0, -1), which returns at once because left >= right. A single element returns the same way.
    • Duplicates. [2, 2, 2, 1, 1, 9, -4] becomes [-4, 1, 1, 2, 2, 2, 9]. Because the test is <=, elements equal to the pivot go to its left side. With many equal values the pivot ends up at the right end of its group, so the partitions are lopsided: an array of identical values costs O(n²) time and a recursion depth of about n. Sorting an array of twenty thousand equal values still finishes, but very large inputs of that kind could exhaust the stack.
    • Skipping the swap to the end. The loop assumes the pivot is the last element. If it stays at its random position, the final correctIndex - 1 no longer points at it.
    • Including the pivot in a recursive call. Recursing on left...pivot keeps a range the same size when the pivot is at right, and the recursion doesn't end.
    • Scanning only to right - 1. That leaves the pivot unplaced, and the returned index is then wrong.

    Testing

    • [3, 1, 3, 2, 5, 4] gives [1, 2, 3, 3, 4, 5].
    • [] gives [].
    • [2, 2, 2, 1, 1, 9, -4] gives [-4, 1, 1, 2, 2, 2, 9].
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free