DSA Merge Sort

Merge sort recursively splits a list in half, sorts each half independently, and merges the two sorted halves back into one sorted list.

Divide and Conquer

Merge sort follows the classic divide-and-conquer pattern. It divides the list into two roughly equal halves, conquers each half by recursively sorting it, and combines the results with a merge step that walks through both sorted halves and interleaves them in order. The recursion bottoms out at lists of length zero or one, which are trivially sorted.

  • Divide the list into two roughly equal halves.
  • Recursively sort the left half.
  • Recursively sort the right half.
  • Merge the two sorted halves by repeatedly taking the smaller front element.
  • Return the fully merged, sorted list.

Example

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result.extend(left[i:])
    result.extend(right[j:])
    return result

def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

print(merge_sort([38, 27, 43, 3, 9, 82, 10]))

The Merge Step in Detail

The merge function never re-examines an element once it has been placed. It keeps two pointers, one into each sorted half, and always copies whichever pointed-to value is smaller into the result, advancing that pointer. Once one half runs out of elements, the remainder of the other half is copied over directly since it is already sorted.

Example

def merge_sort_verbose(arr, depth=0):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    print('  ' * depth + f'Splitting {arr}')
    left = merge_sort_verbose(arr[:mid], depth + 1)
    right = merge_sort_verbose(arr[mid:], depth + 1)
    merged = merge(left, right)
    print('  ' * depth + f'Merged {left} and {right} -> {merged}')
    return merged

merge_sort_verbose([5, 2, 8, 1])
Note: Merge sort is a stable sort: when two elements compare as equal, the one that appeared first in the original list also appears first in the sorted output. This matters when sorting records by one field while preserving prior ordering on another.

Complexity Analysis

CaseTime ComplexitySpace Complexity
BestO(n log n)O(n)
AverageO(n log n)O(n)
WorstO(n log n)O(n)

Example

def count_inversions(arr):
    if len(arr) <= 1:
        return arr, 0
    mid = len(arr) // 2
    left, left_inv = count_inversions(arr[:mid])
    right, right_inv = count_inversions(arr[mid:])
    merged = []
    i = j = 0
    inversions = left_inv + right_inv
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            merged.append(left[i])
            i += 1
        else:
            merged.append(right[j])
            j += 1
            inversions += len(left) - i
    merged.extend(left[i:])
    merged.extend(right[j:])
    return merged, inversions

sorted_arr, total = count_inversions([2, 4, 1, 3, 5])
print(sorted_arr, total)
Note: Unlike insertion sort or quick sort, merge sort is not in-place — the merge step needs O(n) auxiliary space to hold the combined result before it overwrites the original array.

Exercise: DSA Merge Sort

What are the two main phases of merge sort?