🎯 Mergesort

A Comprehensive Study Guide

πŸ“š Table of Contents

πŸ“– Overview

What is Mergesort?

Mergesort is a classic divide-and-conquer sorting algorithm that was one of the first algorithms to achieve O(N log N) time complexity. It's considered one of the top 10 algorithms of the 20th century in science and engineering.

🎯 Basic Plan

  1. Divide: Split the array into two halves
  2. Conquer: Recursively sort each half
  3. Combine: Merge the two sorted halves together

Example: Sorting "MERGESORTEXAMPLE"

Input:

M E R G E S O R T E X A M P L E

⬇️ After sorting left half ⬇️

E E G M O R R S T E X A M P L E

⬇️ After sorting right half ⬇️

E E G M O R R S A E E L M P T X

⬇️ After merging ⬇️

A E E E E G L M M O P R R S T X

βš™οΈ How Mergesort Works

The Merge Operation

Goal: Given two sorted subarrays a[lo] to a[mid] and a[mid+1] to a[hi], replace with a single sorted subarray a[lo] to a[hi].

Merge Example

Two sorted subarrays:

E E G M R | A C E R T

⬇️ After merging ⬇️

A C E E E G M R R T

Merge Algorithm Steps

  1. Copy both subarrays to an auxiliary array
  2. Compare elements from each subarray
  3. Select the smaller element and place it in the result
  4. Repeat until all elements are merged

πŸ’» Java Implementation

Merge Method

private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
    // Copy elements to auxiliary array
    for (int k = lo; k <= hi; k++)
        aux[k] = a[k];
    
    // Merge back to original array
    int i = lo, j = mid + 1;
    for (int k = lo; k <= hi; k++) {
        if      (i > mid)              a[k] = aux[j++];  // left exhausted
        else if (j > hi)               a[k] = aux[i++];  // right exhausted
        else if (less(aux[j], aux[i])) a[k] = aux[j++];  // right smaller
        else                            a[k] = aux[i++];  // left smaller or equal
    }
}

Top-Down Mergesort

public class Merge {
    private static void merge(...) { /* as above */ }
    
    private static void sort(Comparable[] a, Comparable[] aux, 
                                   int lo, int hi) {
        if (hi <= lo) return;                // base case
        
        int mid = lo + (hi - lo) / 2;      // find midpoint
        
        sort(a, aux, lo, mid);                // sort left half
        sort(a, aux, mid + 1, hi);            // sort right half
        merge(a, aux, lo, mid, hi);           // merge results
    }
    
    public static void sort(Comparable[] a) {
        Comparable[] aux = new Comparable[a.length];
        sort(a, aux, 0, a.length - 1);
    }
}

πŸ“ Key Implementation Notes

  • The auxiliary array is created once and reused throughout recursion
  • The merge operation is in-place (modifies original array)
  • The algorithm is not truly in-place (requires O(N) extra space)
  • Base case: when subarray has 1 or 0 elements, it's already sorted

πŸ“Š Complexity Analysis

Time Complexity

O(N log N)

Best, Average, and Worst Case

Space Complexity

O(N)

For auxiliary array

Compares

≀ N lg N

Upper bound

Array Accesses

≀ 6N lg N

Upper bound

Why O(N log N)?

Recurrence Relation:

T(N) = 2T(N/2) + N

  • 2T(N/2): Two recursive calls on halves of the array
  • N: Linear time to merge the two halves
  • Depth of recursion tree: logβ‚‚ N (divide by 2 each time)
  • Work at each level: N (merge operations)
  • Total work: N Γ— logβ‚‚ N = O(N log N)

Empirical Analysis

Computer Thousand (10³) Million (10⁢) Billion (10⁹)
Home (10⁸ compares/sec) instant 1 second 18 minutes
Super (10ΒΉΒ² compares/sec) instant instant instant

✨ Bottom Line

Good algorithms are better than supercomputers!

Mergesort on a home computer beats Insertion Sort (O(NΒ²)) on a supercomputer for large datasets.

πŸ”„ Bottom-Up Mergesort

Alternative Approach: Non-Recursive

Instead of recursively dividing the array, start with small subarrays and iteratively merge them.

Algorithm

  1. Pass through array, merging subarrays of size 1
  2. Repeat for subarrays of size 2, 4, 8, 16, ...
  3. Continue until entire array is sorted

Implementation

public static void sort(Comparable[] a) {
    int N = a.length;
    Comparable[] aux = new Comparable[N];
    
    // sz: size of subarrays to merge (1, 2, 4, 8, ...)
    for (int sz = 1; sz < N; sz = sz + sz) {
        // lo: start of left subarray
        for (int lo = 0; lo < N - sz; lo += sz + sz) {
            merge(a, aux, lo, lo + sz - 1, 
                  Math.min(lo + sz + sz - 1, N - 1));
        }
    }
}

βœ… Advantages

  • Simple and elegant
  • No recursion overhead
  • Same O(N log N) guarantee
  • Good for systems without recursion support

❌ Disadvantages

  • About 10% slower than top-down on typical systems
  • Less cache-friendly
  • Harder to optimize with cutoff to insertion sort

βš–οΈ Stability

What is Stability?

A sorting algorithm is stable if it preserves the relative order of items with equal keys.

Why Does Stability Matter?

Typical Use Case: Sort by multiple criteria

  1. First, sort by name (alphabetically)
  2. Then, sort by section number

With a stable sort, students in the same section remain sorted by name!

Example: Student Records

Name Section After Sorting by Section (Stable)
Andrews 3 Andrews (3) ← Still in order!
Chen 3 Chen (3) ← Still in order!
Fox 3 Fox (3) ← Still in order!

Is Mergesort Stable?

βœ… Yes! Mergesort is Stable

Key insight: The merge operation takes from the left subarray when keys are equal.

if (less(aux[j], aux[i])) a[k] = aux[j++];
else                      a[k] = aux[i++];  // Takes from left if equal

Stability Comparison

Algorithm Stable? Reason
Insertion Sort βœ“ Yes Equal items never move past each other
Selection Sort βœ— No Long-distance exchanges can move equal items
Mergesort βœ“ Yes Merge operation preserves order
Shell Sort βœ— No Long-distance exchanges

πŸ”§ Comparators

What is a Comparator?

A Comparator is a Java interface that allows you to define custom comparison logic, separate from the natural ordering of a type.

Comparable vs Comparator

Comparable Comparator
Natural order (part of class) Alternative order (separate class)
compareTo() method compare() method
One ordering per class Multiple orderings possible
Arrays.sort(array) Arrays.sort(array, comparator)

Example: Student Comparators

public class Student {
    private final String name;
    private final int section;
    
    // Comparator for sorting by name
    public static class ByName implements Comparator<Student> {
        public int compare(Student v, Student w) {
            return v.name.compareTo(w.name);
        }
    }
    
    // Comparator for sorting by section
    public static class BySection implements Comparator<Student> {
        public int compare(Student v, Student w) {
            return v.section - w.section;
        }
    }
}

// Usage:
Arrays.sort(students, new Student.ByName());
Arrays.sort(students, new Student.BySection());

Common String Orderings

Order Type Example
Natural Now is the time
Case Insensitive is Now the time
Spanish cafΓ© cafetero churro cuarto nube Γ±oΓ±o
British Phone Book McKinley Mackintosh

πŸ“ Summary & Comparison

Sorting Algorithm Comparison

Algorithm In-Place? Stable? Best Average Worst Remarks
Selection Sort βœ“ βœ— Β½NΒ² Β½NΒ² Β½NΒ² N exchanges
Insertion Sort βœ“ βœ“ N ΒΌNΒ² Β½NΒ² Use for small N or partially ordered
Shell Sort βœ“ βœ— N log N ? N^(3/2) Tight code; subquadratic
Mergesort βœ— βœ“ Β½N log N N log N N log N N log N guarantee; stable

βœ… Mergesort Advantages

  • Guaranteed O(N log N) in all cases
  • Stable - preserves relative order
  • Predictable performance
  • Parallelizable
  • Good for external sorting
  • Used in Java's Arrays.sort() for objects

❌ Mergesort Disadvantages

  • Not in-place - requires O(N) extra space
  • Slower than quicksort in practice
  • Not adaptive to pre-sorted data
  • Overhead from array copying

🎯 When to Use Mergesort

  • Stability is required (sorting by multiple keys)
  • Guaranteed O(N log N) is essential
  • Working with linked lists (can be in-place with lists)
  • External sorting (data doesn't fit in memory)
  • Parallel sorting (easy to parallelize)
  • Extra space is not a concern

πŸ”‘ Key Takeaways

  1. Mergesort achieves O(N log N) through divide-and-conquer
  2. The merge operation is the key to the algorithm
  3. It's stable and has guaranteed performance
  4. Trade-off: O(N) space for O(N log N) time
  5. Bottom-up version eliminates recursion overhead
  6. Used extensively in practice (Java, Python, etc.)

πŸŽ“ Practice Questions

Question 1: The Divide-and-Conquer Recurrence

Q: Write the recurrence relation for mergesort's time complexity and solve it to show the result is O(N log N).

A: The recurrence is T(N) = 2T(N/2) + N, where 2T(N/2) accounts for recursively sorting two halves and N for merging them. By the Master Theorem (Case 2: a=2, b=2, f(N)=N, N^log_b(a) = N^1 = N), T(N) = Θ(N log N). Alternatively, using the recursion tree: there are logβ‚‚ N levels, and each level does exactly N total work (merging all subarrays at that level sums to N). Total work = N Γ— logβ‚‚ N = O(N log N).

Question 2: Trace the Merge Operation

Q: Trace the merge of the two sorted subarrays [1, 3, 5, 7] and [2, 4, 6, 8] step by step. How many comparisons are made in the worst case?

A: i=0 (points to [1,3,5,7]), j=0 (points to [2,4,6,8]).
Compare 1 vs 2 β†’ take 1. Compare 2 vs 3 β†’ take 2. Compare 3 vs 4 β†’ take 3. Compare 4 vs 5 β†’ take 4. Compare 5 vs 6 β†’ take 5. Compare 6 vs 7 β†’ take 6. Compare 7 vs 8 β†’ take 7. Left exhausted β†’ take 8.
Result: [1, 2, 3, 4, 5, 6, 7, 8]. Total comparisons: 7 = N-1, where N=8. The worst case for merging two halves of size N is N-1 comparisons (perfectly interleaved). Best case is N/2 comparisons (one half is entirely smaller).

Question 3: Mergesort Is Stable β€” Why?

Q: Explain precisely why mergesort is stable, referring to the specific line of code that ensures stability.

A: Mergesort is stable because the merge operation takes elements from the LEFT subarray when the two elements being compared are equal. In the merge code: else a[k] = aux[i++]; β€” this branch executes when aux[j] is NOT strictly less than aux[i], i.e., when they are equal or the left element is smaller. By always choosing the left element when values are equal, the original relative order of equal elements is preserved across the merge. Since all recursive calls ultimately build on this stable merge, the entire sort is stable. In contrast, if the code used less(aux[i], aux[j]) and took from the right on equality, stability would be broken.

Question 4: Top-Down vs. Bottom-Up Mergesort

Q: Compare top-down (recursive) and bottom-up (iterative) mergesort. When might you prefer bottom-up?

A: Both algorithms have identical O(N log N) time complexity and O(N) space, and both are stable. The key differences:
- Top-down uses recursion (call stack overhead, ~log N frames), cleaner conceptually, easier to add optimizations like a cutoff to insertion sort for small subarrays.
- Bottom-up is iterative (no recursion), starting with subarrays of size 1 and doubling. It has no function call overhead, which makes it about 10% faster on some systems. It is also preferable in languages/environments that restrict recursion depth, and works naturally for sorting linked lists without requiring random access. Bottom-up is the approach used in Java's Arrays.sort() for primitive types in some implementations.

Question 5: The Auxiliary Array Requirement

Q: Why does mergesort require O(N) extra space? Can mergesort be made truly in-place (O(1) extra space)?

A: The merge operation requires an auxiliary array because merging two sorted halves in-place without extra space is extremely complex and slow β€” any simple in-place merge degrades to O(NΒ²) due to element shifting. The standard merge copies both halves to an auxiliary array and merges back, requiring exactly N extra cells.
Theoretically, O(1)-space mergesort exists but is impractical: it requires complex block-merge algorithms with O(N log N) time but very large constants. In practice, mergesort always uses O(N) auxiliary space, which is why quicksort (O(log N) stack space) is preferred when memory is constrained.

Question 6: Comparator vs. Comparable

Q: What is the difference between Comparable and Comparator in Java? Give a scenario where a Comparator is necessary.

A: Comparable<T> is implemented by a class to define its natural ordering via compareTo(). A class can have only one natural ordering. Comparator<T> is a separate object that defines an alternative ordering via compare(a, b). A class can have many comparators.
Scenario: A Student class has a natural ordering by student ID. You need to sort a list of students first by GPA (descending) then by name (alphabetical) as a tiebreaker. You cannot redefine the natural order, so you create a custom Comparator<Student> and pass it to Arrays.sort(students, comparator). Mergesort uses the comparator's compare() instead of compareTo().

Question 7: Mergesort vs. Quicksort β€” Key Trade-offs

Q: Name three concrete differences between mergesort and quicksort, and explain when you would choose mergesort.

A: 1. Stability: Mergesort is stable; standard quicksort is not. Choose mergesort when you need to sort by multiple keys in sequence (sort by name, then re-sort by grade β€” mergesort preserves the name order within each grade).
2. Worst-case guarantee: Mergesort is always O(N log N); quicksort degrades to O(NΒ²) on sorted or nearly sorted input (without shuffling). Choose mergesort for real-time systems where worst-case latency matters.
3. Space: Mergesort needs O(N) extra space; quicksort needs only O(log N) stack space. Choose quicksort when memory is tight.
Java uses mergesort (TimSort) for object arrays (stability required) and a dual-pivot quicksort for primitive arrays (stability not needed, speed prioritized).

Question 8: Exam Trap β€” All Cases are O(N log N)

Q: A classmate says "Mergesort is O(N log N) on average but O(NΒ²) in the worst case, just like quicksort." Are they correct?

A: The classmate is wrong. Mergesort is O(N log N) in the best, average, AND worst case. The recurrence T(N) = 2T(N/2) + N holds for ALL inputs because the split is always at the midpoint β€” there is no "bad pivot" analog. The best case is slightly better: Β½N log N comparisons (when the two halves are already in perfect interleaved order, saving comparisons during merge). But the worst case remains N log N, not NΒ². This guaranteed O(N log N) worst case is mergesort's key advantage over quicksort.

πŸŽ“ Study Tips

  • ✏️ Practice tracing the algorithm on paper with small examples
  • πŸ’‘ Understand the recurrence: T(N) = 2T(N/2) + N
  • πŸ” Focus on the merge operation - it's the heart of the algorithm
  • πŸ“Š Compare with other sorts - know the trade-offs
  • πŸ’» Implement both versions - top-down and bottom-up
  • 🎯 Remember stability - key advantage over quicksort
  • πŸ“ Practice complexity analysis - time and space