DSA DAY ONE
DSA DAY ONE
PART I — FOUNDATIONS
Chapter 1: C++ Essentials for DSA
Chapter 2: C++ STL Deep Dive
Chapter 3: Complexity Analysis
PART II — LINEAR DATA STRUCTURES
Chapter 4: Arrays & Strings
Chapter 5: Two Pointers & Sliding Window
Chapter 6: Hashing
Chapter 7: Linked Lists
Chapter 8: Stacks
Chapter 9: Queues & Deques
PART III — TREES
Chapter 10: Binary Trees
Chapter 11: Binary Search Trees (BST)
Chapter 12: Heaps & Priority Queues
Chapter 13: Tries (Prefix Trees)
Chapter 14: Segment Tree & Fenwick Tree
PART IV — ALGORITHMS
Chapter 15: Sorting Algorithms
Why Sorting Matters in InterviewsO(n²) Sorts — Bubble, Selection, InsertionMerge Sort — Stable, Divide & ConquerQuick Sort — Partition Schemes & MitigationHeap SortNon-Comparison Sorts — Counting, Radix, BucketSTL Sort Functions — Complete ReferenceCustom Comparators & Sorting StructsSorting as a Problem-Solving ToolSample ProblemsLeetCode Problem Set
Chapter 16: Binary Search
Chapter 17: Recursion & Backtracking
Chapter 18: Graphs — Fundamentals & Traversals
Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 20: Dynamic Programming
Chapter 21: Greedy Algorithms
Chapter 22: Divide & Conquer
PART V — COMPETITIVE & MATH
Chapter 23: Bit Manipulation
Chapter 24: Math & Number Theory
PART VI — PATTERNS
Chapter 25: Pattern Recognition Mastery
PART VII — SYSTEM DESIGN
Chapter 26: System Design for FAANG Interviews
PART VIII — INTERVIEW STRATEGY
Chapter 27: Cracking FAANG — Strategy & Communication
15

Sorting Algorithms


Chapter 15 ·  Part 4: ALGORITHMS ·  Est. 29 min read

Chapter Goal: Master every sorting algorithm from O(n²) to O(n), understand when each is appropriate, and — crucially — recognize sorting as a preprocessing superpower that unlocks two pointers, binary search, greedy, and interval algorithms. This chapter also teaches you to write custom comparators for any data type without hesitation.


15.1 Why Sorting Matters in Interviews

Sorting Unlocks Other Techniques

Sorting is not just a problem in itself — it is a preprocessing step that makes harder problems tractable:

After Sorting, You Can Use Example Problem
Two Pointers Two Sum on sorted array, 3Sum
Binary Search Search in sorted array, lower/upper bound
Greedy Interval scheduling, minimum arrows
Merge Merge intervals, merge k sorted arrays
Counting Inversions, rank of each element
Sort → O(n log n) preprocessing
Then → O(n) or O(log n) per operation
Total: O(n log n) instead of O(n²)

The Theoretical Lower Bound

Any comparison-based sort requires at least Ω(n log n) comparisons.

Why? There are n! possible orderings. Each comparison eliminates at most half, so we need at least log₂(n!) comparisons. By Stirling's approximation: log₂(n!) ≈ n log₂n.

This is a hard lower bound — no comparison-based sort can beat O(n log n). Non-comparison sorts (counting, radix, bucket) break this barrier by exploiting the structure of the values.

Stability — When It Matters

A stable sort preserves the original relative order of equal elements.

Input:  [(3,a), (1,b), (3,c), (2,d)]   (sorted by first element)
Stable output:   [(1,b), (2,d), (3,a), (3,c)]   ← a before c preserved
Unstable output: [(1,b), (2,d), (3,c), (3,a)]   ← order of equal elements may flip

Stability matters when: you sort by multiple criteria successively, or when equal elements have meaningful secondary ordering.


15.2 O(n²) Sorts — Bubble, Selection, Insertion

Bubble Sort — Repeatedly Bubble the Maximum Up

void bubbleSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n-1; i++) {
        bool swapped = false;
        for (int j = 0; j < n-1-i; j++) {
            if (arr[j] > arr[j+1]) {
                swap(arr[j], arr[j+1]);
                swapped = true;
            }
        }
        if (!swapped) break;   // Early exit: already sorted
    }
}
// Best:  O(n)   — already sorted (with early exit)
// Avg:   O(n²)
// Worst: O(n²)  — reverse sorted
// Space: O(1) in-place
// Stable: YES

Selection Sort — Find Minimum, Place at Front

void selectionSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 0; i < n-1; i++) {
        int minIdx = i;
        for (int j = i+1; j < n; j++)
            if (arr[j] < arr[minIdx]) minIdx = j;
        swap(arr[i], arr[minIdx]);
    }
}
// Best/Avg/Worst: O(n²) — always scans full remaining array
// Space: O(1) in-place
// Stable: NO — swap may reorder equal elements
// Advantage: minimum number of swaps (exactly n-1)

Insertion Sort — Insert Each Element Into Its Correct Position

void insertionSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j+1] = arr[j];   // Shift right
            j--;
        }
        arr[j+1] = key;          // Insert
    }
}
// Best:  O(n)   — already sorted
// Avg:   O(n²)
// Worst: O(n²)  — reverse sorted
// Space: O(1) in-place
// Stable: YES
// Key advantage: O(n) for nearly sorted data — used internally by std::sort
//                for small subarrays (typically n < 16)

O(n²) Comparison Table

Algorithm Best Avg Worst Space Stable Best For
Bubble O(n) O(n²) O(n²) O(1) Yes Educational only
Selection O(n²) O(n²) O(n²) O(1) No Minimizing swaps
Insertion O(n) O(n²) O(n²) O(1) Yes Small n, nearly sorted

15.3 Merge Sort — Stable, Divide & Conquer

The Algorithm

Split the array in half, recursively sort each half, merge the sorted halves.

[38, 27, 43, 3, 9, 82, 10]
         Split
[38, 27, 43]       [3, 9, 82, 10]
    Split               Split
[38, 27] [43]     [3, 9]    [82, 10]
  Split              Split       Split
[38][27]           [3][9]     [82][10]
   Merge              Merge       Merge
[27, 38] [43]     [3, 9]    [10, 82]
       Merge               Merge
  [27, 38, 43]         [3, 9, 10, 82]
              Merge (final)
         [3, 9, 10, 27, 38, 43, 82]
void merge(vector<int>& arr, int l, int mid, int r) {
    vector<int> left(arr.begin()+l,   arr.begin()+mid+1);
    vector<int> right(arr.begin()+mid+1, arr.begin()+r+1);
 
    int i = 0, j = 0, k = l;
    while (i < (int)left.size() && j < (int)right.size()) {
        if (left[i] <= right[j]) arr[k++] = left[i++];   // <= for stability
        else                      arr[k++] = right[j++];
    }
    while (i < (int)left.size())  arr[k++] = left[i++];
    while (j < (int)right.size()) arr[k++] = right[j++];
}
 
void mergeSort(vector<int>& arr, int l, int r) {
    if (l >= r) return;
    int mid = l + (r-l)/2;
    mergeSort(arr, l, mid);
    mergeSort(arr, mid+1, r);
    merge(arr, l, mid, r);
}
 
// Entry point:
void mergeSort(vector<int>& arr) { mergeSort(arr, 0, arr.size()-1); }
 
// Time:  O(n log n) — always, regardless of input
// Space: O(n) — auxiliary array for merging
// Stable: YES — uses <= in merge comparison

Counting Inversions — Merge Sort Application

An inversion is a pair (i, j) where i < j but arr[i] > arr[j]. Merge sort counts them in O(n log n) by counting cross-array merges.

long long mergeCount(vector<int>& arr, int l, int r) {
    if (l >= r) return 0;
    int mid = l + (r-l)/2;
    long long count = 0;
    count += mergeCount(arr, l, mid);
    count += mergeCount(arr, mid+1, r);
 
    // Count inversions during merge
    vector<int> temp;
    int i = l, j = mid+1;
    while (i <= mid && j <= r) {
        if (arr[i] <= arr[j]) {
            temp.push_back(arr[i++]);
        } else {
            // arr[i..mid] are all > arr[j] (since left is sorted)
            count += (mid - i + 1);   // All remaining left elements form inversions with arr[j]
            temp.push_back(arr[j++]);
        }
    }
    while (i <= mid) temp.push_back(arr[i++]);
    while (j <= r)   temp.push_back(arr[j++]);
    copy(temp.begin(), temp.end(), arr.begin()+l);
    return count;
}
 
long long countInversions(vector<int>& arr) {
    return mergeCount(arr, 0, arr.size()-1);
}
// Time: O(n log n), Space: O(n)

15.4 Quick Sort — Partition Schemes & Mitigation

The Algorithm

Pick a pivot, partition the array so that elements < pivot are on the left and elements > pivot are on the right, then recursively sort both sides.

Lomuto Partition (Simpler — Pivot = Last Element)

int partitionLomuto(vector<int>& arr, int lo, int hi) {
    int pivot = arr[hi];
    int i = lo - 1;   // i = last index of "less than pivot" section
 
    for (int j = lo; j < hi; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(arr[i], arr[j]);
        }
    }
    swap(arr[i+1], arr[hi]);   // Place pivot in correct position
    return i + 1;              // Pivot's final index
}
 
void quickSortLomuto(vector<int>& arr, int lo, int hi) {
    if (lo >= hi) return;
    int pivot = partitionLomuto(arr, lo, hi);
    quickSortLomuto(arr, lo, pivot-1);
    quickSortLomuto(arr, pivot+1, hi);
}
// Time:  O(n log n) avg, O(n²) worst (sorted input with last-element pivot)
// Space: O(log n) avg recursion stack, O(n) worst
// Stable: NO

Hoare Partition (Faster in Practice — Pivot = First Element)

int partitionHoare(vector<int>& arr, int lo, int hi) {
    int pivot = arr[lo + (hi-lo)/2];   // Middle element as pivot
    int i = lo - 1, j = hi + 1;
 
    while (true) {
        do { i++; } while (arr[i] < pivot);
        do { j--; } while (arr[j] > pivot);
        if (i >= j) return j;
        swap(arr[i], arr[j]);
    }
}
 
void quickSortHoare(vector<int>& arr, int lo, int hi) {
    if (lo >= hi) return;
    int p = partitionHoare(arr, lo, hi);
    quickSortHoare(arr, lo, p);
    quickSortHoare(arr, p+1, hi);
}
// Hoare: ~3x fewer swaps than Lomuto in practice

Three-Way Partition (Dutch National Flag — Handles Duplicates)

For arrays with many duplicates, standard quicksort degrades to O(n²). Three-way partition segments into: < pivot | == pivot | > pivot.

void quickSort3Way(vector<int>& arr, int lo, int hi) {
    if (lo >= hi) return;
    int pivot = arr[lo + (hi-lo)/2];
    int lt = lo, gt = hi, i = lo;
 
    while (i <= gt) {
        if      (arr[i] < pivot) swap(arr[lt++], arr[i++]);  // < pivot: swap to front
        else if (arr[i] > pivot) swap(arr[i], arr[gt--]);    // > pivot: swap to back
        else                     i++;                         // == pivot: skip
    }
    // After: arr[lo..lt-1] < pivot, arr[lt..gt] == pivot, arr[gt+1..hi] > pivot
    quickSort3Way(arr, lo, lt-1);
    quickSort3Way(arr, gt+1, hi);
}
// Key advantage: arr[lt..gt] is already sorted (all equal to pivot)
// For arrays with many duplicates: O(n) for a uniform array!

Randomized Quick Sort — Eliminating Worst Case

mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
 
void quickSortRandom(vector<int>& arr, int lo, int hi) {
    if (lo >= hi) return;
    // Random pivot selection — makes O(n²) worst case astronomically unlikely
    int pivotIdx = lo + rng() % (hi - lo + 1);
    swap(arr[pivotIdx], arr[hi]);   // Move to end for Lomuto
    int p = partitionLomuto(arr, lo, hi);
    quickSortRandom(arr, lo, p-1);
    quickSortRandom(arr, p+1, hi);
}
// Expected time: O(n log n) for ANY input
// Worst case: O(n²) with probability 1/n! (negligible)

What std::sort Actually Uses — Introsort

std::sort uses Introsort — a hybrid of three algorithms:

  1. Quicksort for the main work (fast in practice)
  2. Heapsort when recursion depth exceeds 2×log(n) (guarantees O(n log n))
  3. Insertion sort for small partitions (n < 16, exploits nearly-sorted subarrays)
Result: O(n log n) worst case + O(1) space + fast constant factor

Quick Sort Summary

Aspect Value
Best case O(n log n) — balanced partitions
Average case O(n log n)
Worst case O(n²) — sorted input + bad pivot
Space O(log n) avg, O(n) worst (recursion)
Stable No
In-place Yes
Cache performance Excellent (sequential access pattern)

15.5 Heap Sort

The Algorithm

Phase 1: Build a max-heap in O(n). Phase 2: Repeatedly extract the max — place it at the end of the array.

void siftDown(vector<int>& arr, int i, int n) {
    while (true) {
        int largest = i, l = 2*i+1, r = 2*i+2;
        if (l < n && arr[l] > arr[largest]) largest = l;
        if (r < n && arr[r] > arr[largest]) largest = r;
        if (largest == i) break;
        swap(arr[i], arr[largest]);
        i = largest;
    }
}
 
void heapSort(vector<int>& arr) {
    int n = arr.size();
 
    // Phase 1: Build max-heap in O(n)
    for (int i = n/2-1; i >= 0; i--)
        siftDown(arr, i, n);
 
    // Phase 2: Extract elements one by one
    for (int i = n-1; i > 0; i--) {
        swap(arr[0], arr[i]);     // Move max to end
        siftDown(arr, 0, i);      // Restore heap on arr[0..i-1]
    }
}
// Time:  O(n log n) always — build O(n) + n extractions × O(log n)
// Space: O(1) — in-place
// Stable: NO — long-distance swaps break stability
// Cache: Poor — accesses distant elements (heap jumps)
// In practice: slower than quicksort due to poor cache performance

15.6 Non-Comparison Sorts — Counting, Radix, Bucket

These algorithms break the Ω(n log n) barrier by exploiting value structure.

Counting Sort — O(n + k) for Range [0, k]

void countingSort(vector<int>& arr, int maxVal) {
    vector<int> count(maxVal+1, 0);
 
    for (int x : arr) count[x]++;           // Count occurrences
    for (int i = 1; i <= maxVal; i++)
        count[i] += count[i-1];              // Prefix sum → positions
 
    vector<int> output(arr.size());
    for (int i = arr.size()-1; i >= 0; i--) { // Reverse for stability
        output[--count[arr[i]]] = arr[i];
    }
    arr = output;
}
// Time:  O(n + k) where k = range of values
// Space: O(n + k)
// Stable: YES (traverse input array right to left)
// Use when: k is small (e.g., sort chars, sort ages 0-100)
// Fails when: k is large (e.g., k = 10^9 → needs too much memory)

Radix Sort — O(d × (n + k)) for d-Digit Numbers

Sort integers digit by digit using a stable sort (counting sort) for each digit. LSD (Least Significant Digit first) is the standard form.

void countingSortByDigit(vector<int>& arr, int exp) {
    int n = arr.size();
    vector<int> output(n), count(10, 0);
 
    for (int x : arr) count[(x / exp) % 10]++;
    for (int i = 1; i < 10; i++) count[i] += count[i-1];
    for (int i = n-1; i >= 0; i--) {
        output[--count[(arr[i] / exp) % 10]] = arr[i];
    }
    arr = output;
}
 
void radixSort(vector<int>& arr) {
    int maxVal = *max_element(arr.begin(), arr.end());
    for (int exp = 1; maxVal / exp > 0; exp *= 10)
        countingSortByDigit(arr, exp);
}
// Time:  O(d × (n + 10)) = O(d × n) where d = number of digits
// Space: O(n + 10) = O(n)
// Stable: YES (LSD uses stable counting sort at each pass)
// For 32-bit integers: d = 10, so effectively O(10n) = O(n)
// In practice: competitive with comparison sorts for large n with bounded values
 
/*
  arr = [170, 45, 75, 90, 802, 24, 2, 66]
  exp=1:   [170, 90, 802, 2, 24, 45, 75, 66]   (sort by units digit)
  exp=10:  [802, 2, 24, 45, 66, 170, 75, 90]   (sort by tens digit)
  exp=100: [2, 24, 45, 66, 75, 90, 170, 802]   (sort by hundreds digit) ✓
*/

Bucket Sort — O(n) for Uniformly Distributed Data

void bucketSort(vector<double>& arr) {
    int n = arr.size();
    vector<vector<double>> buckets(n);
 
    for (double x : arr) {
        int idx = x * n;      // Assumes arr[i] in [0, 1)
        buckets[idx].push_back(x);
    }
    for (auto& bucket : buckets)
        sort(bucket.begin(), bucket.end());   // Insertion sort within bucket
 
    int k = 0;
    for (auto& bucket : buckets)
        for (double x : bucket) arr[k++] = x;
}
// Time: O(n) avg (uniform distribution), O(n²) worst (all in one bucket)
// Space: O(n)
// Stable: YES (if using stable sort for buckets)
// Use when: data is uniformly distributed over a known range

When to Use Which Non-Comparison Sort

Algorithm Best For Requirement
Counting Sort Small range integers Range k must be O(n)
Radix Sort Large integers, strings Fixed/bounded length
Bucket Sort Uniform distribution floats Known range [a, b]

15.7 STL Sort Functions — Complete Reference

#include <algorithm>
#include <numeric>
 
vector<int> v = {5, 3, 1, 4, 2};
 
// ── std::sort ─────────────────────────────────────── O(n log n), NOT stable
sort(v.begin(), v.end());                     // Ascending
sort(v.begin(), v.end(), greater<int>());     // Descending
sort(v.begin(), v.begin()+3);                 // Sort only first 3 elements
 
// ── std::stable_sort ──────────────────────────────── O(n log n), STABLE
// Preserves relative order of equal elements
stable_sort(v.begin(), v.end());
stable_sort(v.begin(), v.end(), [](int a, int b){ return a > b; });
 
// ── std::partial_sort ─────────────────────────────── O(n log k)
// Rearranges so [first, middle) contains the k smallest in sorted order
partial_sort(v.begin(), v.begin()+3, v.end());
// v[0..2] = {1,2,3} (sorted), v[3..4] = {4,5} or {5,4} (unspecified)
 
// ── std::nth_element ──────────────────────────────── O(n) average
// Guarantees: v[k] = what it would be if sorted
//             elements before k are ≤ v[k], elements after k are ≥ v[k]
nth_element(v.begin(), v.begin()+2, v.end());
// v[2] = 3rd smallest. Others are partitioned but NOT sorted.
 
// ── std::inplace_merge ────────────────────────────── O(n log n)
// Merges two consecutive sorted ranges in-place
vector<int> m = {1,3,5, 2,4,6};
inplace_merge(m.begin(), m.begin()+3, m.end());
// m = {1,2,3,4,5,6}
 
// ── Checking if sorted ───────────────────────────────
bool sorted_asc = is_sorted(v.begin(), v.end());
auto it = is_sorted_until(v.begin(), v.end());   // Iterator to first unsorted element
 
// ── std::sort_heap ────────────────────────────────── O(n log n)
// Sorts a range that satisfies heap property (built with make_heap)
make_heap(v.begin(), v.end());
sort_heap(v.begin(), v.end());   // Now sorted ascending

Choosing the Right STL Sort

Need Function Time Stable
Sort everything std::sort O(n log n) No
Sort preserving equal order std::stable_sort O(n log n) Yes
Only need k smallest/largest sorted std::partial_sort O(n log k) No
Only need kth element value std::nth_element O(n) avg No
Sort a heap in-place std::sort_heap O(n log n) No

15.8 Custom Comparators & Sorting Structs

Lambda Comparator — The Standard Approach

// Sort pairs by second element, break ties by first (descending)
vector<pair<int,int>> v = {{1,3},{2,1},{3,3},{4,2}};
sort(v.begin(), v.end(), [](const auto& a, const auto& b) {
    if (a.second != b.second) return a.second < b.second;
    return a.first > b.first;
});
// Result: {2,1}, {4,2}, {3,3}, {1,3}
 
// Sort strings by length, break ties lexicographically
vector<string> words = {"banana","fig","apple","kiwi"};
sort(words.begin(), words.end(), [](const string& a, const string& b) {
    if (a.size() != b.size()) return a.size() < b.size();
    return a < b;
});
// Result: "fig", "kiwi", "apple", "banana"

Operator Overloading for Structs

struct Interval {
    int start, end;
    bool operator<(const Interval& other) const {
        return start < other.start;   // Sort by start time
    }
};
vector<Interval> intervals = {{1,3},{0,2},{4,6}};
sort(intervals.begin(), intervals.end());   // Uses operator<
 
// Or use std::tie for lexicographic multi-field comparison:
struct Task { string name; int priority; int deadline; };
bool operator<(const Task& a, const Task& b) {
    return tie(a.priority, a.deadline, a.name) <
           tie(b.priority, b.deadline, b.name);
}

Sorting with Indices (Indirect Sort)

// Sort indices by value — preserves original array, gives sorted permutation
vector<int> arr = {50, 30, 10, 40, 20};
vector<int> idx(arr.size());
iota(idx.begin(), idx.end(), 0);   // Fill with 0,1,2,...
 
sort(idx.begin(), idx.end(), [&](int a, int b) {
    return arr[a] < arr[b];
});
// idx = {2, 4, 1, 3, 0}  → arr[idx[0]]=10, arr[idx[1]]=20, ...
 
// Now access elements in sorted order without modifying arr:
for (int i : idx) cout << arr[i] << " ";   // 10 20 30 40 50

The Comparator Contract — Strict Weak Ordering

A valid comparator cmp(a, b) must satisfy:

  1. Irreflexivity: cmp(a, a) == false
  2. Asymmetry: if cmp(a, b) then !cmp(b, a)
  3. Transitivity: if cmp(a, b) and cmp(b, c) then cmp(a, c)
// ❌ INVALID: using <= instead of < causes undefined behavior!
sort(v.begin(), v.end(), [](int a, int b){ return a <= b; });
// cmp(3, 3) = true → violates irreflexivity → UB (likely crash/infinite loop)
 
// ✅ VALID: strict <
sort(v.begin(), v.end(), [](int a, int b){ return a < b; });

15.9 Sorting as a Problem-Solving Tool

Pattern 1 — Sort Then Two Pointers

// 3Sum: sort first, then fix one element + two-pointer for the pair
// Sort: O(n log n) once, then O(n) per fixed element → O(n²) total

Pattern 2 — Sort Then Binary Search

// Search insert position, count elements ≤ x, find closest value
// Sort: O(n log n) once, then O(log n) per query

Pattern 3 — Sort By Custom Criterion for Greedy

// Activity selection: sort by end time → greedy select non-overlapping
// Fractional knapsack: sort by value/weight ratio → greedy fill
 
// Template:
sort(activities.begin(), activities.end(), [](auto& a, auto& b) {
    return a.end < b.end;   // Sort by end time
});

Pattern 4 — Sort to Enable Merge

// Merge intervals: sort by start, then scan and merge overlapping
// Valid if intervals[i].start <= intervals[i-1].end

Pattern 5 — Cyclic Sort for [1..n] Arrays

// If array contains values in [1, n], sort in O(n) by placing each
// value at its correct index (index = value - 1):
void cyclicSort(vector<int>& nums) {
    int i = 0;
    while (i < (int)nums.size()) {
        int correct = nums[i] - 1;
        if (nums[i] != nums[correct]) swap(nums[i], nums[correct]);
        else i++;
    }
}
// After cyclic sort: find missing/duplicate by scanning for nums[i] != i+1

15.10 Sample Problems


Problem 1 — Sort Colors (Medium) — LC 75

Sort an array of 0s, 1s, and 2s in-place without counting. Classic Dutch National Flag — three-way partition.

void sortColors(vector<int>& nums) {
    int lo=0, mid=0, hi=nums.size()-1;
    while (mid <= hi) {
        if      (nums[mid] == 0) swap(nums[lo++], nums[mid++]);
        else if (nums[mid] == 1) mid++;
        else                     swap(nums[mid], nums[hi--]);
        // Note: don't increment mid after swap with hi (need to re-examine)
    }
}
// Time: O(n), Space: O(1) — single pass!

Problem 2 — Merge Intervals (Medium) — LC 56

vector<vector<int>> merge(vector<vector<int>>& intervals) {
    sort(intervals.begin(), intervals.end());   // Sort by start time
    vector<vector<int>> result;
 
    for (auto& interval : intervals) {
        if (!result.empty() && interval[0] <= result.back()[1]) {
            // Overlapping — extend the current merged interval's end
            result.back()[1] = max(result.back()[1], interval[1]);
        } else {
            result.push_back(interval);   // Non-overlapping — start new
        }
    }
    return result;
}
// Time: O(n log n) for sort + O(n) for merge pass = O(n log n)
// Space: O(n) for output

Problem 3 — Largest Number (Medium) — LC 179

Arrange numbers to form the largest possible number.

Key insight: Compare two numbers a and b by checking whether str(a)+str(b) > str(b)+str(a).

string largestNumber(vector<int>& nums) {
    vector<string> strs;
    for (int x : nums) strs.push_back(to_string(x));
 
    sort(strs.begin(), strs.end(), [](const string& a, const string& b) {
        return a + b > b + a;   // Custom: prefer whichever concatenation is larger
    });
 
    if (strs[0] == "0") return "0";   // All zeros edge case
    string result;
    for (const string& s : strs) result += s;
    return result;
}
// Time: O(n × m log n) where m = max digit length. Space: O(n)
 
/*
  nums = [3, 30, 34, 5, 9]
  Compare 3 and 30: "330" vs "303" → 330 > 303 → 3 before 30
  Compare 3 and 34: "334" vs "343" → 334 < 343 → 34 before 3
  Sorted: [9, 5, 34, 3, 30] → "9534330"
*/

Problem 4 — Count Inversions (Classic)

Count pairs (i, j) where i < j but arr[i] > arr[j].

long long mergeCount(vector<int>& arr, int l, int r) {
    if (l >= r) return 0;
    int mid = l + (r-l)/2;
    long long count = mergeCount(arr,l,mid) + mergeCount(arr,mid+1,r);
 
    vector<int> temp;
    int i=l, j=mid+1;
    while (i<=mid && j<=r) {
        if (arr[i] <= arr[j]) { temp.push_back(arr[i++]); }
        else {
            count += (mid - i + 1);   // All left[i..mid] > arr[j]
            temp.push_back(arr[j++]);
        }
    }
    while (i<=mid) temp.push_back(arr[i++]);
    while (j<=r)   temp.push_back(arr[j++]);
    copy(temp.begin(), temp.end(), arr.begin()+l);
    return count;
}
long long countInversions(vector<int>& arr) { return mergeCount(arr,0,arr.size()-1); }
// Time: O(n log n), Space: O(n)

Problem 5 — Non-overlapping Intervals (Medium) — LC 435

Find the minimum number of intervals to remove to make the rest non-overlapping.

int eraseOverlapIntervals(vector<vector<int>>& intervals) {
    sort(intervals.begin(), intervals.end(), [](auto& a, auto& b) {
        return a[1] < b[1];   // Sort by END time (greedy: keep earliest-ending)
    });
 
    int removed=0, prevEnd = INT_MIN;
    for (auto& iv : intervals) {
        if (iv[0] >= prevEnd) {
            prevEnd = iv[1];  // No overlap — keep this interval
        } else {
            removed++;        // Overlap — remove this interval (it ends later)
        }
    }
    return removed;
}
// Time: O(n log n), Space: O(1)

Problem 6 — Minimum Number of Arrows to Burst Balloons (Medium) — LC 452

int findMinArrowShots(vector<vector<int>>& points) {
    sort(points.begin(), points.end(), [](auto& a, auto& b) {
        return a[1] < b[1];   // Sort by end point
    });
 
    int arrows=1, arrowPos = points[0][1];
    for (int i=1; i<(int)points.size(); i++) {
        if (points[i][0] > arrowPos) {      // Current balloon starts after arrow
            arrows++;
            arrowPos = points[i][1];         // New arrow at new end point
        }
        // Else: current balloon is hit by the existing arrow
    }
    return arrows;
}
// Time: O(n log n), Space: O(1)

Problem 7 — H-Index (Medium) — LC 274

Find the maximum h such that h papers have at least h citations each.

// Approach 1: Sort descending — O(n log n)
int hIndex(vector<int>& citations) {
    sort(citations.rbegin(), citations.rend());
    int h = 0;
    for (int i=0; i<(int)citations.size(); i++) {
        if (citations[i] >= i+1) h = i+1;   // i+1 papers have >= i+1 citations
        else break;
    }
    return h;
}
 
// Approach 2: Counting sort — O(n) (since h ≤ n)
int hIndex_count(vector<int>& citations) {
    int n = citations.size();
    vector<int> cnt(n+1, 0);
    for (int c : citations) cnt[min(c, n)]++;  // Cap at n (h ≤ n always)
 
    int papers = 0;
    for (int h=n; h>=0; h--) {
        papers += cnt[h];
        if (papers >= h) return h;             // papers papers have >= h citations
    }
    return 0;
}
// Approach 2: Time O(n), Space O(n)

Problem 8 — Maximum Gap (Hard) — LC 164

Find the maximum gap between successive elements in the sorted form of an array. Must run in O(n) time and O(n) space.

Key insight: Pigeonhole principle + bucket sort. If there are n-1 gaps between n sorted elements spanning range [min, max], at least one gap ≥ (max-min)/(n-1). Put each element in a bucket of size (max-min)/(n-1). The maximum gap must cross bucket boundaries (same-bucket elements have gap < bucket size).

int maximumGap(vector<int>& nums) {
    int n = nums.size();
    if (n < 2) return 0;
 
    int lo = *min_element(nums.begin(), nums.end());
    int hi = *max_element(nums.begin(), nums.end());
    if (lo == hi) return 0;
 
    // Bucket size: at least ceil((hi-lo)/(n-1))
    int bucketSize = max(1, (hi - lo) / (n-1));
    int bucketCount = (hi - lo) / bucketSize + 1;
 
    vector<int> bucketMin(bucketCount, INT_MAX);
    vector<int> bucketMax(bucketCount, INT_MIN);
 
    for (int x : nums) {
        int idx = (x - lo) / bucketSize;
        bucketMin[idx] = min(bucketMin[idx], x);
        bucketMax[idx] = max(bucketMax[idx], x);
    }
 
    // Maximum gap is between consecutive non-empty buckets
    int maxGap = 0, prevMax = lo;
    for (int i = 0; i < bucketCount; i++) {
        if (bucketMin[i] == INT_MAX) continue;   // Empty bucket
        maxGap = max(maxGap, bucketMin[i] - prevMax);
        prevMax = bucketMax[i];
    }
    return maxGap;
}
// Time: O(n), Space: O(n)

Problem 9 — Wiggle Sort II (Medium) — LC 324

Reorder array so nums[0] < nums[1] > nums[2] < nums[3] ...

void wiggleSort(vector<int>& nums) {
    int n = nums.size();
    vector<int> sorted = nums;
    sort(sorted.begin(), sorted.end());
 
    // Place larger half at odd indices (descending), smaller at even (descending)
    int small = (n-1)/2, large = n-1;   // Start from end to handle duplicates
    for (int i=0; i<n; i++) {
        nums[i] = (i % 2 == 0) ? sorted[small--] : sorted[large--];
    }
}
// Time: O(n log n), Space: O(n)
 
// O(n) time using nth_element:
void wiggleSort_ON(vector<int>& nums) {
    int n = nums.size();
    auto mid = nums.begin() + (n-1)/2;
    nth_element(nums.begin(), mid, nums.end());
    int median = *mid;
 
    // Virtual indexing: map i to 3-way partitioned positions
    // (1-indexed odd positions first, then even positions)
    auto mapIdx = [&](int i) { return (1 + 2*i) % (n | 1); };
 
    int lo=0, mi=0, hi=n-1;
    while (mi <= hi) {
        if      (nums[mapIdx(mi)] > median) swap(nums[mapIdx(lo++)], nums[mapIdx(mi++)]);
        else if (nums[mapIdx(mi)] < median) swap(nums[mapIdx(mi)],   nums[mapIdx(hi--)]);
        else                                mi++;
    }
}
// Time: O(n) with nth_element, Space: O(1)

Problem 10 — Relative Sort Array (Easy) — LC 1122

Sort arr1 so that elements in arr2 appear first in the same order as arr2, then remaining elements sorted ascending.

vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {
    // Map each arr2 element to its priority (position in arr2)
    unordered_map<int,int> priority;
    for (int i=0; i<(int)arr2.size(); i++) priority[arr2[i]] = i;
 
    sort(arr1.begin(), arr1.end(), [&](int a, int b) {
        bool aInP = priority.count(a), bInP = priority.count(b);
        if (aInP && bInP) return priority[a] < priority[b];  // Both in arr2: use arr2 order
        if (aInP) return true;    // Only a in arr2: a comes first
        if (bInP) return false;   // Only b in arr2: b comes first
        return a < b;             // Neither in arr2: ascending
    });
    return arr1;
}
// Time: O(n log n), Space: O(m) where m = arr2.size()

Problem 11 — Find the Duplicate Number (Medium) — LC 287

Array of n+1 integers in [1, n]. Find the duplicate using O(1) extra space.

// Approach 1: Cyclic sort (modifies array)
int findDuplicate_cyclic(vector<int>& nums) {
    int i = 0;
    while (i < (int)nums.size()) {
        if (nums[i] != i+1) {
            if (nums[nums[i]-1] == nums[i]) return nums[i];  // Duplicate found
            swap(nums[i], nums[nums[i]-1]);
        } else { i++; }
    }
    return -1;
}
 
// Approach 2: Binary search on answer (O(n log n), O(1) space, no modification)
int findDuplicate(vector<int>& nums) {
    int lo=1, hi=nums.size()-1;
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        // Count numbers in [1..mid]
        int cnt = count_if(nums.begin(), nums.end(), [&](int x){ return x<=mid; });
        if (cnt > mid) hi = mid;   // Duplicate is in [1..mid]
        else           lo = mid+1; // Duplicate is in [mid+1..n]
    }
    return lo;
}
// Time: O(n log n), Space: O(1)
 
// Approach 3: Fast/slow pointer (Floyd's — O(n), O(1), no modification)
int findDuplicate_floyd(vector<int>& nums) {
    int slow=nums[0], fast=nums[0];
    do { slow=nums[slow]; fast=nums[nums[fast]]; } while (slow!=fast);
    slow=nums[0];
    while (slow!=fast) { slow=nums[slow]; fast=nums[fast]; }
    return slow;
}

Problem 12 — Sort List (Medium) — LC 148

Sort a linked list in O(n log n) time and O(1) space.

ListNode* sortList(ListNode* head) {
    if (!head || !head->next) return head;
 
    // Split: find first middle using fast/slow
    ListNode* slow=head, *fast=head->next;
    while (fast && fast->next) { slow=slow->next; fast=fast->next->next; }
    ListNode* mid = slow->next;
    slow->next = nullptr;
 
    ListNode* l = sortList(head);
    ListNode* r = sortList(mid);
 
    // Merge two sorted lists
    ListNode dummy(0); ListNode* tail=&dummy;
    while (l && r) {
        if (l->val <= r->val) { tail->next=l; l=l->next; }
        else                  { tail->next=r; r=r->next; }
        tail=tail->next;
    }
    tail->next = l ? l : r;
    return dummy.next;
}
// Time: O(n log n), Space: O(log n) recursion stack

15.11 LeetCode Problem Set

# Problem Difficulty Core Technique Link
1 Sort Colors Medium Dutch National Flag / 3-way partition LC 75
2 Merge Intervals Medium Sort by start + greedy merge LC 56
3 Largest Number Medium Custom string comparator LC 179
4 Non-overlapping Intervals Medium Sort by end + greedy count LC 435
5 H-Index Medium Sort desc + scan / Counting sort LC 274
6 Sort List Medium Merge sort on linked list LC 148
7 Maximum Gap Hard Bucket sort + pigeonhole principle LC 164
8 Count of Smaller Numbers After Self Hard Merge sort (count during merge) LC 315

Suggested order: #1 (Sort Colors) — three-way partition from memory. #2 (Merge Intervals) — the canonical sort-then-greedy problem. #3 (Largest Number) — custom comparator thinking. #4 and #5 reinforce greedy + sort. #6 (Sort List) reinforces merge sort on a linked list. #7 (Maximum Gap) is the hardest — bucket sort with the pigeonhole insight. #8 (Count of Smaller) shows merge sort as a counting tool (complement to the BIT approach in Ch14).


← PreviousChapter 14: Segment Tree & Fenwick TreeNext →Chapter 16: Binary Search
Chapter 15 — Progress
0 / 28 COMPLETE
O(n²) Sorts
  • Implement bubble sort with the early-exit optimization
  • Know: insertion sort is O(n) for nearly sorted — used by std::sort for small n
  • Know: selection sort makes only n-1 swaps regardless of input — minimum swaps
  • State stability for each: bubble , insertion , selection
Merge Sort
  • Implement merge sort from memory (split, recurse, merge)
  • Implement count inversions: add `count += (mid - i + 1)` when right-picking
  • Know: O(n log n) always, O(n) space, stable, good for external sort
Quick Sort
  • Implement Lomuto partition: `i = lo-1`, scan j, swap when arr[j] <= pivot
  • Implement 3-way partition (Dutch National Flag): lt, gt, i pointers
  • Know: worst case O(n²) for sorted input + last-element pivot → use random pivot
  • Know that std::sort uses Introsort: QuickSort + HeapSort + InsertionSort
Non-Comparison Sorts
  • Implement counting sort: count array → prefix sum → stable placement
  • Implement LSD radix sort: d passes of counting sort by digit
  • Know the lower bound: Ω(n log n) for comparison-based sorts
  • Know when each non-comparison sort beats comparison sorts
STL Sort Functions
  • Know `std::sort` vs `std::stable_sort` (stability), `std::partial_sort` (top-k), `std::nth_element` (O(n) average)
  • Use `std::sort` with lambda comparators confidently
  • Use `std::partial_sort` for "find k smallest sorted" in O(n log k)
  • Use `std::nth_element` for "find kth element" in O(n) average
Custom Comparators
  • Write a multi-field comparator using if-else chain or `std::tie`
  • Write an indirect sort (sort indices by value)
  • State the 3 strict-weak-ordering requirements — know that `<=` is INVALID
  • Know: using `<=` instead of `<` causes undefined behavior in std::sort
Sorting as a Tool
  • Sort → two pointers (3Sum)
  • Sort → binary search (insert position, count ≤ x)
  • Sort by end time → greedy interval problems (LC 435, LC 452)
  • Sort by custom string concatenation (LC 179 Largest Number)
  • Cyclic sort for [1..n] arrays: O(n) sort + O(n) scan for missing/duplicate
Notes▸