SwadhyAI Notes

All DSA notes

DSA Complexity

Merge sort cost

Dual conflict: you need a deterministic ceiling, and you can afford a buffer. Quicksort's average is tempting; the SLA is not.

Merge sort costs time in best/average/worst for the standard algorithm, with on arrays. Stable with left-first ties. Matches comparison lower bounds up to constants.

Algorithm journey

1

Halve

log n depth.

2

Linear merge

Theta(n) per level.

3

Sum levels

Theta(n log n).

4

Buffer

Reuse one aux array if careful.

What actually happens

Naive idea: hope quicksort's average holds under adversarial load.

Better idea: pay the buffer for a flat Theta(n log n) curve.

Constants and memory traffic can still lose to introsort when stability is irrelevant.

Invariant

Each merge level moves every element a constant number of times; depth is Theta(log n) for balanced splits.

How to read merge sort's complexity

1. Need predictable Theta(n log n)? Yes.

2. Memory-tight embedded? Heapsort/introsort may fit better.

3. Linked lists? Mergesort shines with O(1) extra beyond pointers.

Quantitative check

n = 2^20 ≈ 1.05e6: about 20 full levels. If each level does ~c n moves, total ~20 c n element motions.

How it works

1

Divide

Find the middle point to divide the array into two halves.

2

Conquer (Recursion)

Call Merge Sort for the first half and the second half recursively.

3

Merge

Merge the two sorted halves into a single sorted array.

🎮 Interactive Merge Sort Demo

Watch how the array is split down to individual elements and then merged back up in sorted order!

Speed:
ℹ️ Ready
Comparing/Active
Sorted

Tip: Stability Matters!

Merge Sort is a stable sort, meaning it preserves the relative order of equal elements. This is crucial when sorting complex objects (e.g., sorting a list of people by name, then by age - the name order is preserved for people of the same age).

💻 Implementation

Python

python
def merge_sort(arr):
    if len(arr) > 1:
        # Finding the mid of the array
        mid = len(arr) // 2
        
        # Dividing the array elements
        L = arr[:mid]
        R = arr[mid:]
        
        # Sorting the first half
        merge_sort(L)
        
        # Sorting the second half
        merge_sort(R)
        
        i = j = k = 0
        
        # Copy data to temp arrays L[] and R[]
        while i < len(L) and j < len(R):
            if L[i] <= R[j]:
                arr[k] = L[i]
                i += 1
            else:
                arr[k] = R[j]
                j += 1
            k += 1
            
        # Checking if any element was left
        while i < len(L):
            arr[k] = L[i]
            i += 1
            k += 1
            
        while j < len(R):
            arr[k] = R[j]
            j += 1
            k += 1
            
    return arr

# Example Usage
arr = [12, 11, 13, 5, 6, 7]
print(f"Original: {arr}")
merge_sort(arr)
print(f"Sorted: {arr}")

Java

java
public class MergeSort {
    /**
     * Merge Sort - Divide and Conquer algorithm
     * 
     * Time Complexity: O(n log n) - All cases
     * Space Complexity: O(n) - Requires temporary arrays
     * 
     * Stable sort - preserves relative order of equal elements
     */
    
    // Merges two subarrays of arr[]
    // First subarray is arr[l..m]
    // Second subarray is arr[m+1..r]
    static void merge(int[] arr, int l, int m, int r) {
        // Find sizes of two subarrays to be merged
        int n1 = m - l + 1;
        int n2 = r - m;
        
        // Create temp arrays
        int[] L = new int[n1];
        int[] R = new int[n2];
        
        // Copy data to temp arrays
        for (int i = 0; i < n1; i++) {
            L[i] = arr[l + i];
        }
        for (int j = 0; j < n2; j++) {
            R[j] = arr[m + 1 + j];
        }
        
        // Merge the temp arrays
        
        // Initial indexes of first and second subarrays
        int i = 0, j = 0;
        
        // Initial index of merged subarray
        int k = l;
        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                arr[k] = L[i];
                i++;
            } else {
                arr[k] = R[j];
                j++;
            }
            k++;
        }
        
        // Copy remaining elements of L[] if any
        while (i < n1) {
            arr[k] = L[i];
            i++;
            k++;
        }
        
        // Copy remaining elements of R[] if any
        while (j < n2) {
            arr[k] = R[j];
            j++;
            k++;
        }
    }
    
    // Main function that sorts arr[l..r] using merge()
    static void mergeSort(int[] arr, int l, int r) {
        if (l < r) {
            // Find the middle point
            int m = l + (r - l) / 2;
            
            // Sort first and second halves
            mergeSort(arr, l, m);
            mergeSort(arr, m + 1, r);
            
            // Merge the sorted halves
            merge(arr, l, m, r);
        }
    }
    
    public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6, 7};
        System.out.println("Original: " + java.util.Arrays.toString(arr));
        
        mergeSort(arr, 0, arr.length - 1);
        
        System.out.println("Sorted: " + java.util.Arrays.toString(arr));
    }
}

Merge invariant and correctness

During merge(left,right)

The output prefix is sorted and contains exactly the smallest consumed elements from both inputs. Because each input suffix is sorted, the smaller front element is globally next. Copying the remaining suffix is safe when one side empties.

Across recursive levels

Base cases of length 0 or 1 are sorted. Assuming both recursive halves are sorted, the merge invariant produces a sorted union. Structural induction over subarray length proves the whole array is sorted and is a permutation of the input.

Stability lives in one character: on equal keys, take from the left run first (<=, not <). Tag equal records with original positions in tests; their positions must remain increasing after the sort.

Merge-sort family: choose the shape around the storage

Merge sort implementation variants
VariantSchedulingMemory / localityBest fit
Top-downRecursively split, then mergeO(n) buffer + O(log n) framesClarity and divide-and-conquer reasoning
Bottom-upMerge widths 1, 2, 4, 8…O(n) buffer; no recursionPredictable iterative array processing
Natural mergeDetect existing monotone runsAdaptive to presorted structureReal data with long ordered runs; TimSort builds on this
Linked-list mergeSplit by slow/fast pointersRelinks nodes; O(log n) recursion, no array bufferSequential-access lists where random-access sorts suffer
Parallel merge sortFork sufficiently large independent halvesExtra bandwidth and task overheadLarge in-memory data after a measured cutoff

Exact work and why the bound is Θ(n log n)

Recurrence

T(n)=2T(n/2)+Θ(n)

Two half-size sorts plus one linear merge.

Per level

Θ(n) copied/merged

Subproblems partition all n records.

Levels

⌈log₂ n⌉

Halving continues to singleton runs.

For n a power of two, comparisons are at most n log₂n − n + 1; a merge of lengths a and b needs at mosta+b−1 comparisons. Standard top-down merge sort still performs Θ(n log n) work on already sorted input unless it adds an ordered-boundary check such as a[mid] <= a[mid+1] to skip a merge.

External merge sort: when data does not fit in RAM

1 · Run generation

Read a memory-sized chunk, sort it, and write one sorted run. Replacement selection can create longer runs.

2 · Buffered readers

Allocate an input buffer per run and one output buffer to turn random records into sequential block I/O.

3 · k-way merge

Keep each run head in a min-heap; emit the minimum and refill only that run.

4 · Merge passes

If all runs cannot be opened/buffered together, merge batches until one final run remains.

CPU cost for a k-way merge is O(N log k), but the engineering objective is often fewer block transfers and sequential access. Choose fan-in from memory M, block size B, file-descriptor limits, compression, and failure/restart requirements, not from Big-O alone.

Production and testing checklist

  • • Allocate one reusable auxiliary buffer; per-merge allocation creates avoidable GC pressure.
  • • Define comparator ordering, null policy, stability, and whether input is mutated.
  • • For objects, copy references rather than payloads and account for write barriers/cache bandwidth.
  • • Parallelize only above a cutoff; sibling sorts are independent, but merging still consumes bandwidth.
  • • Test empty/singleton, odd lengths, duplicates, sorted/reverse, and adversarial comparators.
  • • Verify sortedness, permutation preservation, and tagged-record stability separately.
  • • Property-test against the platform stable sort across randomized arrays.
  • • Prefer the standard library unless you need external storage, a custom list structure, or measured scheduling control.

⚡ Complexity Analysis

Time Complexity

  • ✓
    O(n log n) - All Cases

    Guaranteed performance

Space Complexity

  • ⚠
    O(n)

    Requires extra space for merging

Other Properties

  • ✓Stable: Preserves order
  • ✗Not In-place: O(n) extra memory

✅ Pros & ❌ Cons

✅ Advantages

  • ✓Guaranteed O(n log n) worst-case time
  • ✓Stable sorting (great for linked lists)
  • ✓Independent halves can be sorted in parallel above a practical cutoff

❌ Disadvantages

  • ✗Requires O(n) extra space (memory heavy)
  • ✗Slower than Quick Sort for small arrays
  • ✗Recursive overhead

Exercises

  1. Solve T(n)=2T(n/2)+n by recursion tree.
  2. How does bottom-up mergesort change the stack-space term?
  3. Why is "best case Theta(n)" false for vanilla mergesort but true for some natural-merge variants?

Primary sources

  • CLRS: merge sort recurrence.
  • Knuth TAOCP Vol. 3: merging.
  • TimSort: natural runs as an adaptive mergesort cousin.

Short takeaway

Merge Sort = Divide in half, sort, merge.Time: O(n log n) always.Space: O(n).Use when: You need stable sort or guaranteed performance.Avoid when: Memory is tight (embedded systems).