Skip to content

912. Sort an array

Problem Description

LeetCode Problem 912: Given an array of integers nums, sort the array in ascending order and return it.

You must solve the problem without using any built-in functions in O(nlog(n)) time complexity and with the smallest space complexity possible.

Clarification

  • Can we modify the input array?

Assumption

-

Solution

Approach 1: Merge Sort (Top-Down)

We can use the merge sort algorithm to sort the array. The merge sort algorithm is a divide-and-conquer algorithm that divides the array into two halves, sorts each half recursively, and then merges the two sorted halves.

In this approach, we will use top-down merge sort.

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        nums_copy = nums[:]  # Assump the input can't be chnaged
        aux = [0] * len(nums)  # Allocate auxiliary array for merge sort
        self.sortSubArray(nums_copy, aux, 0, len(nums) - 1)
        return nums_copy

    def sortSubArray(self, nums: list[int], aux: list[int], lo: int, hi: int) -> None:
        # Base Case
        if lo >= hi:
            return

        mid = (lo + hi) // 2
        self.sortSubArray(nums, aux, lo, mid)
        self.sortSubArray(nums, aux, mid + 1, hi)
        self.merge(nums, aux, lo, mid, hi)

    def merge(self, nums: list[int], aux: list[int], lo: int, mid: int, hi: int) -> None:
        aux[lo : hi+1] = nums[lo : hi+1]  # Refresh aux (current window) with partially sorted values in nums

        i, j = lo, mid + 1  # Pointers to left and right halves
        for k in range(lo, hi + 1):
            if i > mid:  # Left half exhausted
                nums[k] = aux[j]
                j += 1
            elif j > hi:  # Right half exhausted
                nums[k] = aux[i]
                i += 1
            elif aux[j] < aux[i]:  # Right element smaller
                nums[k] = aux[j]
                j += 1
            else:  # Left element smaller or equal
                nums[k] = aux[i]
                i += 1

Complexity Analysis of Approach 1

  • Time complexity: \(O(n \log n)\)
    • The array is recursively split in half --> \(O(\log n)\) levels of recursion.
    • At each level, merging multiple two halves, taking \(O(n)\) time across the entire level.
  • Space complexity: \(O(n)\)
    • Auxiliary array of size \(n\) is used to store the merged result.
    • The space complexity of the recursive stack is \(O(\log n)\) in a balanced case, but it is \(O(n)\) in the worst case.
    • The overall space complexity is \(O(n) + O(n) = O(n)\).

Approach 2: Merge Sort (Bottom-Up)

We can also sort the array using bottom-up merge sort.

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        n = len(nums)
        aux = [0] * n  # Auxiliary array for merging

        size = 1
        while size < n:
            for lo in range(0, n - size, size * 2):
                mid = lo + size - 1
                hi = min(lo + size * 2 - 1, n - 1)
                self.merge(nums, aux, lo, mid, hi)
            size *= 2

        return nums

    def merge(self, nums: list[int], aux: list[int], lo: int, mid: int, hi: int) -> None:
        # Refersh aux with partially sorted nums
        aux[lo : hi + 1] = nums[lo : hi + 1]

        i, j = lo, mid + 1
        for k in range(lo, hi + 1):
            if i > mid:
                nums[k] = aux[j]
                j += 1
            elif j > hi:
                nums[k] = aux[i]
                i += 1
            elif aux[j] < aux[i]:
                nums[k] = aux[j]
                j += 1
            else:
                nums[k] = aux[i]
                i += 1

Complexity Analysis of Approach 2

  • Time complexity: \(O(n \log n)\)
    • Doubling subarray size in the main loop leads to \(\log n\) levels of merging.
    • At each level, all \(n\) elements are processed, resulting in a total of \(O(n)\) work per level.
    • The overall time complexity is \(O(n \log n)\).
  • Space complexity: \(O(n)\)
    An auxiliary array of size \(n\) is used to store the merged result.

Approach 3: Quick Sort

The problem can also be solved using the quick sort algorithm. Yet, the solution fails at the test case where a very large number of same elements are present, leading to Time Limit Exceeded (TLE).

May be use quick sort with 3 way partitioning to avoid TLE?

class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
    self._sort(nums, 0, len(nums) - 1)
    return nums

def _sort(self, nums: list[int], low: int, high: int) -> None:
    if low >= high:
        return
    # Randomize the first element
    i_random = low + random.randint(0, len(nums)) % (high - low + 1)
    nums[i_random], nums[low] = nums[low], nums[i_random]
    pivot = self._partition(nums, low, high)
    self._sort(nums, low, pivot - 1)
    self._sort(nums, pivot + 1, high)

def _partition(self, nums: list[int], low: int, high: int) -> int:
    if low >= high:
        return -1
    pivot = low  # Select the first element as a partition element
    l, r = pivot + 1, high

    while (l <= r):
        if nums[l] < nums[pivot]:
            l += 1
        elif nums[r] >= nums[pivot]:
            r -= 1
        else:
            nums[l], nums[r] = nums[r], nums[l]

    nums[pivot], nums[r] = nums[r], nums[pivot]

    return r

Complexity Analysis of Approach 3

  • Time complexity: \(O(n \log n)\)
    • The average time complexity is \(O(n \log n)\).
    • The worst-case time complexity is \(O(n^2)\), which can occur when the smallest or largest element is always chosen as the pivot.
  • Space complexity: \(O(n \log n)\)
    The space complexity is \(O(\log n)\) due to the recursive stack space.

Comparison of Different Approaches

The table below summarize the time complexity and space complexity of different approaches:

Approach Time Complexity Space Complexity
Approach 1 - Merge Sort (Top-Down) \(O(n \log n)\) \(O(n)\)
Approach 2 - Merge Sort (Bottom-Up) \(O(n \log n)\) \(O(n)\)

Test