Merge Sort

interview_workbook/algorithms/sorting /app/src/interview_workbook/algorithms/sorting/merge_sort.py
View Source

Algorithm Notes

Summary: Divide-and-conquer; recursively sort halves then merge.
Time: Best/Avg/Worst O(n log n)
Space: O(n) auxiliary (linked-list variant can be O(1))
Stability: Stable
When to use: Guaranteed O(n log n), external sorting, stable requirement.

Big-O Guide

Source

def merge_sort(a: list[int]) -> list[int]:
    """
    Stable O(n log n) sort via divide-and-conquer.

    Time: O(n log n) - always, regardless of input
    Space: O(n) - due to merging buffer and recursion stack

    Pitfalls:
    - Avoid excessive list slicing in tight loops for large n
    - Consider iterative bottom-up approach for lower recursion overhead
    - Not in-place, requires O(n) extra space

    Interview follow-ups:
    - How to make it in-place? (Complex, involves rotations)
    - How does it compare to quicksort? (Stable, guaranteed O(n log n), but uses more space)
    - When would you choose merge sort? (When stability is required, external sorting)
    """
    n = len(a)
    if n <= 1:
        return a[:]

    mid = n // 2
    left = merge_sort(a[:mid])
    right = merge_sort(a[mid:])
    return _merge(left, right)


def _merge(left: list[int], right: list[int]) -> list[int]:
    """Merge two sorted arrays into one sorted array."""
    i = j = 0
    result = []

    # Merge while both arrays have elements
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:  # <= ensures stability
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    # Add remaining elements
    result.extend(left[i:])
    result.extend(right[j:])
    return result


def merge_sort_inplace(a: list[int]) -> None:
    """
    In-place merge sort (modifies input array).
    Still requires O(n) auxiliary space for merging.
    """

    def merge_sort_helper(arr: list[int], temp: list[int], left: int, right: int):
        if left >= right:
            return

        mid = (left + right) // 2
        merge_sort_helper(arr, temp, left, mid)
        merge_sort_helper(arr, temp, mid + 1, right)
        merge_inplace(arr, temp, left, mid, right)

    def merge_inplace(arr: list[int], temp: list[int], left: int, mid: int, right: int):
        # Copy to temp array
        for i in range(left, right + 1):
            temp[i] = arr[i]

        i, j, k = left, mid + 1, left

        while i <= mid and j <= right:
            if temp[i] <= temp[j]:
                arr[k] = temp[i]
                i += 1
            else:
                arr[k] = temp[j]
                j += 1
            k += 1

        # Copy remaining elements
        while i <= mid:
            arr[k] = temp[i]
            i += 1
            k += 1
        while j <= right:
            arr[k] = temp[j]
            j += 1
            k += 1

    if len(a) <= 1:
        return

    temp = [0] * len(a)
    merge_sort_helper(a, temp, 0, len(a) - 1)


def demo():
    """Demo function for merge sort."""
    print("Merge Sort Demo")
    print("=" * 40)

    test_cases = [[5, 1, 4, 2, 8, 0, 2], [1], [], [3, 3, 3, 3], [5, 4, 3, 2, 1], list(range(10))]

    for i, arr in enumerate(test_cases):
        print(f"Test {i + 1}: {arr}")
        sorted_arr = merge_sort(arr)
        print(f"Sorted:  {sorted_arr}")

        # Test in-place version
        arr_copy = arr[:]
        merge_sort_inplace(arr_copy)
        print(f"In-place: {arr_copy}")
        print()


if __name__ == "__main__":
    demo()