Sorting Algorithms
Scope — Sorting algorithms and sort-adjacent techniques — comparison sorts and their stability, counting/bucket/radix, quickselect, custom comparators, and cyclic sort. See also: heap.md — heap sort and top-k; binary_search.md — what sorting enables; advanced_divide_and_conquer.md — merge sort used to count, not to sort; greedy.md — sort-then-scan.

LeetCode Problem Lists
Overview
Sorting is the process of arranging elements in a specific order (ascending or descending). It’s fundamental to many algorithms and data structures, enabling efficient searching, data analysis, and problem-solving.
Key Properties
- Stability: Maintains relative order of equal elements
- In-place: Uses O(1) extra space
- Adaptive: Performs better on partially sorted data
- When to Use: Data ordering, preprocessing for binary search, finding medians/percentiles
Algorithm Selection Guide
- Small datasets (n < 50): Insertion Sort
- General purpose: Quick Sort, Merge Sort
- Guaranteed O(n log n): Heap Sort, Merge Sort
- Nearly sorted: Insertion Sort, Bubble Sort
- Limited range: Counting Sort, Radix Sort
References
| Sorting Algorithm | Time Complexity (Best Case) | Time Complexity (Average Case) | Time Complexity (Worst Case) | Space Complexity |
|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) |
| Counting Sort | O(n + k) | O(n + k) | O(n + k) | O(k) |
| Radix Sort | O(nk) | O(nk) | O(nk) | O(n + k) |
| Bucket Sort | O(n + k) | O(n + k) | O(n²) | O(n) |
Problem Categories
Pattern 1: Custom Comparator Sorting — LC 179
- Description: Sort with custom rules or multiple criteria
- Examples: LC 179, 791, 937, 1029, 1366
- Pattern: Define comparison function for complex ordering
Pattern 2: Topological Sorting — LC 207
- Description: Order elements based on dependencies
- Examples: LC 207, 210, 269, 310, 1136
- Pattern: DFS/BFS with in-degree tracking
Pattern 3: Interval Sorting — LC 56
- Description: Sort intervals for merging/processing
- Examples: LC 56, 57, 252, 253, 435
- Pattern: Sort by start, then process
Pattern 4: K-th Element — LC 215
- Description: Find k-th smallest/largest efficiently
- Examples: LC 215, 347, 378, 658, 973
- Pattern: Quick Select or Heap
Pattern 5: Bucket/Counting Sort — LC 164
- Description: Sort with limited value range
- Examples: LC 164, 274, 451, 1122, 1636
- Pattern: Use value as index
Pattern 6: Merge Sort Applications — LC 148
- Description: Divide-and-conquer with sorting
- Examples: LC 23, 148, 315, 327, 493
- Pattern: Merge sorted sequences
Pattern 7: Greedy Pairing (Sort + Two Pointers) — LC 1877
- Description: Sort then pair smallest with largest to balance pair sums and minimize the maximum
- Core idea: Pairing large numbers together creates unnecessarily large sums; pairing extremes (min+max) distributes weight evenly
- Examples: LC 1877, 561, 881, 2491
- Pattern: Sort → two pointers from both ends → track max/min of pair results
Templates & Algorithms
Algorithm Comparison Table
| Algorithm | Best | Average | Worst | Space | Stable | When to Use |
|---|---|---|---|---|---|---|
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | No | General purpose |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes | Stable, guaranteed O(n log n) |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | No | In-place, guaranteed O(n log n) |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | Yes | Small or nearly sorted |
| Counting Sort | O(n+k) | O(n+k) | O(n+k) | O(k) | Yes | Limited range integers |
| Radix Sort | O(nk) | O(nk) | O(nk) | O(n+k) | Yes | Fixed-width integers |
Template 1: Quick Sort
# Python - Classic Quick Sort
def quickSort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
# Partition and get pivot index
pi = partition(arr, low, high)
# Recursively sort left and right
quickSort(arr, low, pi - 1)
quickSort(arr, pi + 1, high)
return arr
def partition(arr, low, high):
# Choose rightmost as pivot
pivot = arr[high]
i = low - 1 # Smaller element index
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
# 3-way Quick Sort for duplicates
def quickSort3Way(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
lt, gt = partition3Way(arr, low, high)
quickSort3Way(arr, low, lt - 1)
quickSort3Way(arr, gt + 1, high)
return arr
def partition3Way(arr, low, high):
pivot = arr[low]
i = low
lt = low
gt = high
while i <= gt:
if arr[i] < pivot:
arr[lt], arr[i] = arr[i], arr[lt]
lt += 1
i += 1
elif arr[i] > pivot:
arr[i], arr[gt] = arr[gt], arr[i]
gt -= 1
else:
i += 1
return lt, gt
// Java - Quick Sort
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
Variation — run partition3Way ONCE (Dutch National Flag) — LC 75
Twist: when the alphabet is a fixed tiny set ({0,1,2}), you don’t recurse — a single 3-way partition pass fully sorts the array in O(n) / O(1).
// java
// LC 75 - Sort Colors
// time = O(n), space = O(1)
// IDEA: Dutch National Flag — 3 pointers lo / i / hi.
// Invariant: [0, lo) == 0 | [lo, i) == 1 | (hi, n-1] == 2
// KEY TRAP: after swapping with `hi`, do NOT advance i — the value pulled
// in from the back has not been examined yet.
public void sortColors(int[] nums) {
int lo = 0, i = 0, hi = nums.length - 1;
while (i <= hi) {
if (nums[i] == 0) {
int t = nums[lo]; nums[lo] = nums[i]; nums[i] = t;
lo++; i++; // safe: nums[lo] was a 1 (already seen)
} else if (nums[i] == 2) {
int t = nums[hi]; nums[hi] = nums[i]; nums[i] = t;
hi--; // NOTE: i stays put
} else {
i++;
}
}
}
# python
# LC 75 - Sort Colors
# time = O(n), space = O(1)
# IDEA: Dutch National Flag one-pass 3-way partition
class Solution:
def sortColors(self, nums):
lo, i, hi = 0, 0, len(nums) - 1
while i <= hi:
if nums[i] == 0:
nums[lo], nums[i] = nums[i], nums[lo]
lo += 1
i += 1
elif nums[i] == 2:
nums[hi], nums[i] = nums[i], nums[hi]
hi -= 1 # NOTE: do NOT advance i here
else:
i += 1
Follow-up often asked: “can you do it without counting sort (two passes)?” → the DNF one-pass above is the expected answer.
Template 2: Merge Sort
# Python - Merge Sort
def mergeSort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = mergeSort(arr[:mid])
right = mergeSort(arr[mid:])
return merge(left, right)
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
# In-place merge sort
def mergeSortInPlace(arr, left=0, right=None):
if right is None:
right = len(arr) - 1
if left < right:
mid = (left + right) // 2
mergeSortInPlace(arr, left, mid)
mergeSortInPlace(arr, mid + 1, right)
mergeInPlace(arr, left, mid, right)
return arr
def mergeInPlace(arr, left, mid, right):
left_arr = arr[left:mid + 1]
right_arr = arr[mid + 1:right + 1]
i = j = 0
k = left
while i < len(left_arr) and j < len(right_arr):
if left_arr[i] <= right_arr[j]:
arr[k] = left_arr[i]
i += 1
else:
arr[k] = right_arr[j]
j += 1
k += 1
while i < len(left_arr):
arr[k] = left_arr[i]
i += 1
k += 1
while j < len(right_arr):
arr[k] = right_arr[j]
j += 1
k += 1
// Java - Merge Sort
public void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
private void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int j = 0; j < n2; j++) {
rightArr[j] = arr[mid + 1 + j];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = rightArr[j++];
}
}
while (i < n1) {
arr[k++] = leftArr[i++];
}
while (j < n2) {
arr[k++] = rightArr[j++];
}
}
Variation — merge in place by walking BACKWARDS — LC 88
Twist: nums1 already owns the trailing free space, so there is no room for the O(n) buffer the standard merge uses. Filling from the tail (largest first) means every slot you write is either free or already consumed → O(1) extra space.
// java
// LC 88 - Merge Sorted Array
// time = O(m + n), space = O(1)
// IDEA: write from the back. k = last free slot, i/j = last real elems.
// Loop on `j` only: if nums2 is exhausted, the nums1 prefix is already in place.
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1, j = n - 1, k = m + n - 1;
while (j >= 0) {
if (i >= 0 && nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
}
# python
# LC 88 - Merge Sorted Array
# time = O(m + n), space = O(1)
# IDEA: fill nums1 from the tail so we never overwrite an unread element
class Solution:
def merge(self, nums1, m, nums2, n):
i, j, k = m - 1, n - 1, m + n - 1
while j >= 0:
if i >= 0 and nums1[i] > nums2[j]:
nums1[k] = nums1[i]
i -= 1
else:
nums1[k] = nums2[j]
j -= 1
k -= 1
Why backwards? Merging forwards would overwrite
nums1[0..m-1]before reading it, forcing an O(m) copy. This “fill from the largest end” trick reappears in any in-place merge (e.g. the merge step of LC 148 Sort List uses pointer relinking for the same reason).
Template 3: Custom Comparator Sorting — LC 179
# Python - Custom sorting
class Solution:
def customSort(self, items):
# Single key
items.sort(key=lambda x: x[0])
# Multiple keys
items.sort(key=lambda x: (x[0], -x[1], x[2]))
# Complex comparison
def compare(item):
# Return tuple of sort keys
if condition:
return (0, item.value, item.name)
else:
return (1, -item.priority, item.id)
items.sort(key=compare)
# Using functools for traditional comparison
from functools import cmp_to_key
def compare_func(a, b):
if a < b:
return -1
elif a > b:
return 1
else:
return 0
items.sort(key=cmp_to_key(compare_func))
return items
# Custom class for sorting
class CustomComparable:
def __init__(self, value, priority):
self.value = value
self.priority = priority
def __lt__(self, other):
# Define less than for sorting
if self.priority != other.priority:
return self.priority > other.priority
return self.value < other.value
// Java - Custom comparator
public void customSort(List<Item> items) {
// Lambda comparator
items.sort((a, b) -> a.value - b.value);
// Multiple criteria
items.sort((a, b) -> {
if (a.priority != b.priority) {
return b.priority - a.priority; // Descending
}
return a.name.compareTo(b.name); // Ascending
});
// Using Comparator methods
items.sort(Comparator
.comparingInt(Item::getPriority).reversed()
.thenComparing(Item::getName));
// Custom Comparator class
items.sort(new Comparator<Item>() {
@Override
public int compare(Item a, Item b) {
// Custom logic
return customCompare(a, b);
}
});
}
// Comparable interface
class Item implements Comparable<Item> {
int value;
String name;
@Override
public int compareTo(Item other) {
if (this.value != other.value) {
return this.value - other.value;
}
return this.name.compareTo(other.name);
}
}
Template 4: Quick Select (K-th Element) — LC 215
# Python - Quick Select for k-th smallest
def quickSelect(arr, k):
# Find k-th smallest (0-indexed)
return quickSelectHelper(arr, 0, len(arr) - 1, k - 1)
def quickSelectHelper(arr, left, right, k):
if left == right:
return arr[left]
# Random pivot for better average case
import random
pivot_idx = random.randint(left, right)
pivot_idx = partition(arr, left, right, pivot_idx)
if k == pivot_idx:
return arr[k]
elif k < pivot_idx:
return quickSelectHelper(arr, left, pivot_idx - 1, k)
else:
return quickSelectHelper(arr, pivot_idx + 1, right, k)
def partition(arr, left, right, pivot_idx):
pivot = arr[pivot_idx]
# Move pivot to end
arr[pivot_idx], arr[right] = arr[right], arr[pivot_idx]
store_idx = left
for i in range(left, right):
if arr[i] < pivot:
arr[store_idx], arr[i] = arr[i], arr[store_idx]
store_idx += 1
# Move pivot to final position
arr[store_idx], arr[right] = arr[right], arr[store_idx]
return store_idx
Template 5: Counting Sort
# Python - Counting Sort
def countingSort(arr, max_val=None):
if not arr:
return arr
if max_val is None:
max_val = max(arr)
min_val = min(arr)
# Create counting array
range_size = max_val - min_val + 1
count = [0] * range_size
# Count occurrences
for num in arr:
count[num - min_val] += 1
# Reconstruct sorted array
idx = 0
for i in range(range_size):
while count[i] > 0:
arr[idx] = i + min_val
idx += 1
count[i] -= 1
return arr
# Stable counting sort
def stableCountingSort(arr):
if not arr:
return arr
max_val = max(arr)
min_val = min(arr)
range_size = max_val - min_val + 1
count = [0] * range_size
output = [0] * len(arr)
# Count occurrences
for num in arr:
count[num - min_val] += 1
# Cumulative count
for i in range(1, range_size):
count[i] += count[i - 1]
# Build output array (stable)
for i in range(len(arr) - 1, -1, -1):
output[count[arr[i] - min_val] - 1] = arr[i]
count[arr[i] - min_val] -= 1
return output
Template 6: Topological Sort — LC 207
# Python - Topological Sort (Kahn's Algorithm)
def topologicalSort(numNodes, edges):
# Build graph and in-degree
graph = defaultdict(list)
in_degree = [0] * numNodes
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
# Queue for nodes with no dependencies
queue = deque([i for i in range(numNodes) if in_degree[i] == 0])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# Check for cycle
if len(result) != numNodes:
return [] # Cycle detected
return result
# DFS-based Topological Sort
def topologicalSortDFS(numNodes, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
visited = set()
stack = []
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
stack.append(node)
for i in range(numNodes):
if i not in visited:
dfs(i)
return stack[::-1]
Template 7: Merge Sort as a COUNTER (inversions / smaller-after-self) — LC 315 Priority 5 of 5 — Must know — expect it in almost every loop
Key Idea: the merge step is the only moment where a whole block of “right-half” elements is known to be smaller than a given left-half element. Piggyback a counter on that moment and you count pairs across the array in O(n log n) instead of O(n²).
Recurrence: answer(lo..hi) = answer(left) + answer(right) + cross-pairs counted during merge
Critical detail: you must sort an array of indices, not the values — the answer is reported per original position, and the values get shuffled by the sort.
// java
// LC 315 - Count of Smaller Numbers After Self
// time = O(n log n), space = O(n)
// IDEA: merge sort over an INDEX array. While merging, `moved` = how many
// right-half elements have already been emitted. Every time we emit a
// LEFT element, exactly `moved` smaller elements sat after it → add.
public List<Integer> countSmaller(int[] nums) {
int n = nums.length;
int[] count = new int[n];
int[] idx = new int[n];
for (int i = 0; i < n; i++) idx[i] = i;
sortCount(nums, idx, new int[n], 0, n - 1, count);
List<Integer> res = new ArrayList<>();
for (int c : count) res.add(c);
return res;
}
private void sortCount(int[] nums, int[] idx, int[] tmp, int lo, int hi, int[] count) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
sortCount(nums, idx, tmp, lo, mid, count);
sortCount(nums, idx, tmp, mid + 1, hi, count);
int i = lo, j = mid + 1, k = lo;
int moved = 0; // # of right-half elems already merged
while (i <= mid && j <= hi) {
if (nums[idx[j]] < nums[idx[i]]) { // strict `<` keeps the sort stable
moved++;
tmp[k++] = idx[j++];
} else {
count[idx[i]] += moved; // <-- the whole trick
tmp[k++] = idx[i++];
}
}
while (i <= mid) { count[idx[i]] += moved; tmp[k++] = idx[i++]; }
while (j <= hi) { tmp[k++] = idx[j++]; }
for (int t = lo; t <= hi; t++) idx[t] = tmp[t];
}
# python
# LC 315 - Count of Smaller Numbers After Self
# time = O(n log n), space = O(n)
# IDEA: merge sort the INDEX list; `moved` counts right-half elements already
# emitted, which are exactly the smaller-and-to-the-right ones.
class Solution:
def countSmaller(self, nums):
n = len(nums)
count = [0] * n
idx = list(range(n))
def sort_count(lo, hi):
if lo >= hi:
return
mid = (lo + hi) // 2
sort_count(lo, mid)
sort_count(mid + 1, hi)
i, j, moved, tmp = lo, mid + 1, 0, []
while i <= mid and j <= hi:
if nums[idx[j]] < nums[idx[i]]: # right elem is smaller
moved += 1
tmp.append(idx[j]); j += 1
else:
count[idx[i]] += moved # <-- the whole trick
tmp.append(idx[i]); i += 1
while i <= mid:
count[idx[i]] += moved
tmp.append(idx[i]); i += 1
while j <= hi:
tmp.append(idx[j]); j += 1
idx[lo:hi + 1] = tmp
sort_count(0, n - 1)
return count
Visual trace — nums = [5,2,6,1], final merge of [2,5] (idx 1,0) and [1,6] (idx 3,2):
left = [2(i1), 5(i0)] right = [1(i3), 6(i2)] moved = 0
step 1: 1 < 2 -> emit right, moved = 1
step 2: 2 <= 6 -> emit left , count[1] += 1 -> count[1] = 1
step 3: 5 <= 6 -> emit left , count[0] += 1 -> count[0] = 2 (1 came from the earlier level)
step 4: drain right
answer = [2,1,1,0]
Same skeleton, different counting predicate:
| Problem | LC # | What you count during merge |
|---|---|---|
| Count of Smaller Numbers After Self | 315 | right elems < left elem |
| Reverse Pairs | 493 | pairs with left > 2 * right (extra pre-scan before merging) |
| Count of Range Sum | 327 | prefix-sum pairs whose difference lands in [lower, upper] |
Template 8: Bucket Sort by Value Range (bucket = value / width) — LC 220 Priority 4 of 5 — High value — a gap here costs you rounds
Key Idea: when the question is “do two values differ by at most t?”, make buckets of width t + 1. Then:
- two values in the same bucket always differ by ≤
t→ instant hit - values more than
tapart can only be in adjacent buckets → you only ever checkid-1,id,id+1
That turns an O(n log k) balanced-BST / sliding-window-sort solution into O(n).
// java
// LC 220 - Contains Duplicate III
// time = O(n), space = O(min(n, indexDiff))
// IDEA: bucket width = valueDiff + 1, so "same bucket" == "within valueDiff".
// Keep only the last `indexDiff` elements as a sliding window of buckets.
public boolean containsNearbyAlmostDuplicate(int[] nums, int indexDiff, int valueDiff) {
if (indexDiff <= 0 || valueDiff < 0) return false;
long w = (long) valueDiff + 1; // NOTE: long, valueDiff can be MAX_VALUE
Map<Long, Long> bucket = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
long id = bucketId(nums[i], w);
if (bucket.containsKey(id)) return true; // same bucket
if (bucket.containsKey(id - 1) && nums[i] - bucket.get(id - 1) <= valueDiff) return true;
if (bucket.containsKey(id + 1) && bucket.get(id + 1) - nums[i] <= valueDiff) return true;
bucket.put(id, (long) nums[i]); // at most 1 value per bucket in the window
if (i >= indexDiff) bucket.remove(bucketId(nums[i - indexDiff], w)); // slide
}
return false;
}
// KEY TRAP: Java integer division truncates toward zero, so -1/3 == 0 == 2/3.
// Negative values need an explicit floor.
private long bucketId(long x, long w) {
return x < 0 ? (x + 1) / w - 1 : x / w;
}
# python
# LC 220 - Contains Duplicate III
# time = O(n), space = O(min(n, indexDiff))
# IDEA: bucket width = valueDiff + 1; check own bucket + 2 neighbours only.
# Python's // already floors, so negatives need no special case.
class Solution:
def containsNearbyAlmostDuplicate(self, nums, indexDiff, valueDiff):
if indexDiff <= 0 or valueDiff < 0:
return False
w = valueDiff + 1
bucket = {}
for i, x in enumerate(nums):
bid = x // w
if bid in bucket:
return True
if bid - 1 in bucket and x - bucket[bid - 1] <= valueDiff:
return True
if bid + 1 in bucket and bucket[bid + 1] - x <= valueDiff:
return True
bucket[bid] = x
if i >= indexDiff: # slide the window
bucket.pop(nums[i - indexDiff] // w, None)
return False
Bucket-width design cheat sheet (this is the reusable part):
| Goal | Bucket width | Why |
|---|---|---|
“two values within t” (LC 220) |
t + 1 |
same bucket ⇒ diff ≤ t; only neighbours can also qualify |
| “max gap between consecutive sorted values” (LC 164) | (max-min)/(n-1) |
pigeonhole ⇒ the max gap must be between buckets, so intra-bucket order is irrelevant |
| “top K by frequency” (LC 347) | freq as index, 1..n |
frequency range is bounded by n ⇒ index directly |
Template 9: Sort by a DERIVED Key to Unlock Greedy / DP — LC 354 Priority 5 of 5 — Must know — expect it in almost every loop
Key Idea: many “2-D” problems collapse to a solved 1-D problem once you pick the right sort order. The sort itself is the algorithm; the tie-break rule is where the interview is won or lost.
Pattern: sort on the first dimension ascending, and on ties sort the second dimension descending — the descending tie-break makes equal-first-dimension items mutually non-chainable, so a plain strictly-increasing scan on dimension 2 is automatically correct.
// java
// LC 354 - Russian Doll Envelopes
// time = O(n log n), space = O(n)
// IDEA: sort width ASC, height DESC on ties -> answer = LIS over heights.
// WHY height DESC? envelopes [3,5] and [3,7] must never both be chosen.
// With height DESC they appear as 7 then 5 (decreasing), so no increasing
// subsequence can pick both. With height ASC you'd wrongly nest them.
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes, (a, b) -> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]);
int[] tails = new int[envelopes.length]; // tails[l] = min tail of an LIS of length l+1
int len = 0;
for (int[] e : envelopes) {
int i = Arrays.binarySearch(tails, 0, len, e[1]);
if (i < 0) i = -(i + 1); // insertion point = lower_bound
tails[i] = e[1];
if (i == len) len++;
}
return len;
}
# python
# LC 354 - Russian Doll Envelopes
# time = O(n log n), space = O(n)
# IDEA: sort (w ASC, h DESC) -> strictly-increasing LIS over heights
import bisect
class Solution:
def maxEnvelopes(self, envelopes):
# NOTE: -e[1] is the whole trick (blocks same-width nesting)
envelopes.sort(key=lambda e: (e[0], -e[1]))
tails = []
for _, h in envelopes:
i = bisect.bisect_left(tails, h) # bisect_left => STRICTLY increasing
if i == len(tails):
tails.append(h)
else:
tails[i] = h
return len(tails)
Variation — sort by derived key, then INSERT greedily — LC 406
Twist: sort tallest-first so that everyone already placed is ≥ the current person; then k is literally the index to insert at, because shorter people inserted later never disturb an earlier person’s count.
// java
// LC 406 - Queue Reconstruction by Height
// time = O(n^2) (list insert), space = O(n)
// IDEA: height DESC, k ASC. Insert person at index k — everyone already in
// the list is taller/equal, so exactly k of them end up in front.
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, (a, b) -> a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]);
List<int[]> res = new ArrayList<>();
for (int[] p : people) res.add(p[1], p);
return res.toArray(new int[0][]);
}
# python
# LC 406 - Queue Reconstruction by Height
# time = O(n^2), space = O(n)
# IDEA: sort (-h, k) then insert each person at index k
class Solution:
def reconstructQueue(self, people):
people.sort(key=lambda p: (-p[0], p[1]))
res = []
for p in people:
res.insert(p[1], p)
return res
Variation — sort by derived key, then DP over predecessors — LC 1048
Twist: sort by length so that every possible predecessor of a word is guaranteed to be processed before it — the DP then needs no recursion or memo ordering logic at all.
// java
// LC 1048 - Longest String Chain
// time = O(n * L^2), space = O(n * L) (L = max word length)
// IDEA: sort by length -> predecessors always come first. For each word try
// deleting each char and look the shorter word up in the dp map.
public int longestStrChain(String[] words) {
Arrays.sort(words, (a, b) -> a.length() - b.length());
Map<String, Integer> dp = new HashMap<>();
int best = 0;
for (String w : words) {
int cur = 1;
for (int i = 0; i < w.length(); i++) {
String pre = w.substring(0, i) + w.substring(i + 1);
cur = Math.max(cur, dp.getOrDefault(pre, 0) + 1);
}
dp.put(w, cur);
best = Math.max(best, cur);
}
return best;
}
# python
# LC 1048 - Longest String Chain
# time = O(n * L^2), space = O(n * L)
# IDEA: sort by length so predecessors are already in dp when we reach a word
class Solution:
def longestStrChain(self, words):
words.sort(key=len)
dp = {}
best = 0
for w in words:
cur = 1
for i in range(len(w)):
cur = max(cur, dp.get(w[:i] + w[i + 1:], 0) + 1)
dp[w] = cur
best = max(best, cur)
return best
Derived-key sort order — quick decision table:
| Situation | Sort order | Unlocked by |
|---|---|---|
| 2-D strict nesting (LC 354) | dim1 ASC, dim2 DESC on ties | LIS on dim2 |
| Place items where “count of bigger” matters (LC 406) | height DESC, k ASC | insert at index k |
| DP where predecessor is “smaller” (LC 1048) | by size/length ASC | forward DP, no memo |
| Compare-by-concatenation (LC 179) | custom a+b vs b+a |
direct join |
⚠️ Comparator transitivity trap: a custom comparator must be a total order —
compare(a,b) > 0 && compare(b,c) > 0must implycompare(a,c) > 0. Java’s TimSort throwsIllegalArgumentException: Comparison method violates its general contract!when it isn’t. LC 179’sa+bvsb+arule is provably transitive; ad-hoc rules like “sort by whichever field is nonzero” usually are not. Also preferInteger.compare(a, b)overa - b(overflow on large/negative values).
Template 10: Cyclic Sort (values are a permutation of 1..n) — LC 645 Priority 4 of 5 — High value — a gap here costs you rounds
Key Idea: when the values themselves are the indices (1..n or 0..n-1), you don’t need a comparison sort at all — repeatedly swap each value home to index = value - 1. Every swap places one value permanently, so the total work is O(n) with O(1) space. Afterwards, any index whose value is wrong pinpoints the missing/duplicate/misplaced element.
// java
// LC 645 - Set Mismatch
// time = O(n), space = O(1)
// IDEA: cyclic sort — send nums[i] to index nums[i]-1.
// LOOP CONDITION: use `nums[i] != nums[nums[i]-1]` (compare VALUES), not
// `nums[i] != i+1`. With duplicates the "home" slot is already taken, and
// comparing values is what stops the swap loop from spinning forever.
public int[] findErrorNums(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
while (nums[i] != nums[nums[i] - 1]) {
int t = nums[i];
nums[i] = nums[t - 1];
nums[t - 1] = t;
}
}
// now nums[i] should be i+1; the one that isn't gives both answers
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) return new int[]{nums[i], i + 1}; // {duplicated, missing}
}
return new int[]{-1, -1};
}
# python
# LC 645 - Set Mismatch
# time = O(n), space = O(1)
# IDEA: cyclic sort, then scan for the index whose value is not i+1
class Solution:
def findErrorNums(self, nums):
n = len(nums)
for i in range(n):
# NOTE: compare VALUES (not nums[i] != i+1) so duplicates terminate
while nums[i] != nums[nums[i] - 1]:
t = nums[i]
nums[i], nums[t - 1] = nums[t - 1], nums[i]
for i in range(n):
if nums[i] != i + 1:
return [nums[i], i + 1] # [duplicated, missing]
return [-1, -1]
When to reach for cyclic sort: the array length is n and the values are constrained to 1..n (or 0..n-1), and the follow-up asks for O(n) time / O(1) space (so no HashSet, no counting array). The post-sort scan is what varies — “first index that’s wrong” answers missing/duplicate/first-missing-positive style questions.
Template 11: Offline Queries — sort the QUERIES too, then sweep — LC 1847 Priority 4 of 5 — High value — a gap here costs you rounds
When: every query is answered independently, all of them are given up front, and answering one
in isolation costs too much. Sorting the queries alongside the data turns q independent searches
into one sweep — but only if you remember to put the answers back in the original order.
online : answer query i the moment it arrives (must handle any order)
offline : you have all q queries -> reorder them freely (this template)
The recipe is always these four steps:
- Sort the data by the dimension the queries filter on.
- Sort the query indices by the same dimension — keep the index, that is the whole trick.
- Sweep: a single moving pointer feeds items into a searchable structure as the threshold relaxes.
- Write each answer into
ans[originalIndex].
LC 1847 Closest Room: each query is (preferred id, minSize) — among rooms with
size >= minSize, return the id closest to preferred, ties going to the smaller id.
- Sort rooms by size descending and queries by
minSizedescending. AsminSizerelaxes, rooms only ever get added — never removed. A monotone threshold is what makes one pointer enough. - The structure must answer “nearest value to
x”, so it needs order: aTreeSetin Java (floor/ceiling), aSortedListin Python.
// java
// LC 1847 - Closest Room
// IDEA: offline. Rooms by size DESC, queries by minSize DESC; one pointer adds rooms into
// a TreeSet as the size requirement relaxes, then floor/ceiling gives the nearest id.
// time = O(n log n + q log q + q log n), space = O(n + q)
public int[] closestRoom(int[][] rooms, int[][] queries) {
int n = rooms.length, q = queries.length;
Arrays.sort(rooms, (a, b) -> b[1] - a[1]); // size descending
Integer[] order = new Integer[q];
for (int i = 0; i < q; i++) order[i] = i;
// NOTE !!! sort the INDICES, not the queries — ans must go back in the caller's order
Arrays.sort(order, (a, b) -> queries[b][1] - queries[a][1]);
TreeSet<Integer> ids = new TreeSet<>();
int[] ans = new int[q];
int j = 0;
for (int qi : order) {
int preferred = queries[qi][0], minSize = queries[qi][1];
while (j < n && rooms[j][1] >= minSize) ids.add(rooms[j++][0]); // monotone: add only
Integer lo = ids.floor(preferred), hi = ids.ceiling(preferred);
if (lo == null && hi == null) ans[qi] = -1;
else if (lo == null) ans[qi] = hi;
else if (hi == null) ans[qi] = lo;
// tie -> smaller id, so `<=` favours the floor
else ans[qi] = (preferred - lo <= hi - preferred) ? lo : hi;
}
return ans;
}
# python
# LC 1847 - Closest Room
# IDEA: same sweep. `ids` is kept sorted so bisect finds the two candidates around
# `preferred`; insort keeps this to the standard library, at O(n) per insert.
# time = O(n^2 + q log q + q log n), space = O(n + q)
from bisect import bisect_left, insort
def closestRoom(rooms, queries):
rooms = sorted(rooms, key=lambda r: -r[1]) # size descending
order = sorted(range(len(queries)), key=lambda i: -queries[i][1])
ids = []
ans = [-1] * len(queries)
j = 0
for qi in order:
preferred, min_size = queries[qi]
while j < len(rooms) and rooms[j][1] >= min_size:
insort(ids, rooms[j][0]) # O(n) memmove per insert
j += 1
if not ids:
continue # no room is big enough
k = bisect_left(ids, preferred)
best = None
for cand in (ids[k - 1] if k > 0 else None, ids[k] if k < len(ids) else None):
if cand is None:
continue
# strict `<` keeps the smaller id on a tie, because the floor is seen first
if best is None or abs(cand - preferred) < abs(best - preferred):
best = cand
ans[qi] = best
return ans
insortvs a real balanced set.bisect.insortfinds the slot inO(log n)but shifts the tail, so each insert isO(n)and the sweep isO(n^2)— fine for LC 1847’sn <= 10^5because the shift is amemmove, and it needs nothing outside the standard library. TheO(n log n)version isSortedListfrom the third-partysortedcontainerspackage (pre-installed on LeetCode, not in the standard library, sopip install sortedcontainersto run it here): swapids = []forids = SortedList(),insort(ids, x)forids.add(x), andbisect_left(ids, x)forids.bisect_left(x). Java’sTreeSetabove is the genuinely logarithmic structure and needs no such caveat.
Common mistakes
- Sorting the queries themselves. The answers then come out in sorted order and the judge sees a permutation of the right array. Sort an index array, or carry the index in the tuple.
- A non-monotone threshold. If items had to be removed as the sweep advances, one pointer is not enough — you need a structure that supports deletion, or two sweeps.
- Only checking
ceiling. The nearest value can be on either side; both neighbours must be compared, and the tie rule read off the statement.
Where else this template shows up
| Problem | Sort by | Structure swept into |
|---|---|---|
| LC 1847 Closest Room | room size / minSize, descending |
TreeSet of ids |
| LC 1697 Checking Existence of Edge Length Limited Paths | edge weight / query limit, ascending | union-find |
| LC 2070 Most Beautiful Item for Each Query | price, ascending | prefix max over prices |
| LC 1146 Snapshot Array | — | online, so binary search per key instead |
Offline vs online is a real interview signal. “You are given all the queries in an array” is permission to reorder them; “implement a class with a
query()method” is not. Say which one you are assuming out loud — it changes the achievable complexity.
Problems by Pattern
Pattern-Based Problem Tables
Custom Comparator Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Largest Number | 179 | String comparison | Medium |
| Custom Sort String | 791 | Character order | Medium |
| Reorder Data in Log Files | 937 | Multi-key sort | Easy |
| Two City Scheduling | 1029 | Cost difference | Medium |
| Rank Teams by Votes | 1366 | Vote counting | Medium |
| Sort Array by Parity | 905 | Even/odd separation | Easy |
| Relative Sort Array | 1122 | Custom order | Easy |
Topological Sort Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Course Schedule | 207 | Cycle detection | Medium |
| Course Schedule II | 210 | Ordering with dependencies | Medium |
| Alien Dictionary | 269 | Character ordering | Hard |
| Minimum Height Trees | 310 | Tree centroid | Medium |
| Parallel Courses | 1136 | Level-based processing | Medium |
| Sequence Reconstruction | 444 | Unique ordering | Medium |
Interval Sorting Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Merge Intervals | 56 | Sort and merge | Medium |
| Insert Interval | 57 | Binary search insertion | Medium |
| Meeting Rooms | 252 | Overlap check | Easy |
| Meeting Rooms II | 253 | Sweep line | Medium |
| Non-overlapping Intervals | 435 | Greedy removal | Medium |
| Minimum Number of Arrows | 452 | Interval intersection | Medium |
K-th Element Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Kth Largest Element | 215 | Quick select | Medium |
| Top K Frequent Elements | 347 | Bucket sort | Medium |
| Kth Smallest in Matrix | 378 | Binary search | Medium |
| Find K Closest Elements | 658 | Two pointers | Medium |
| K Closest Points to Origin | 973 | Quick select | Medium |
| Kth Largest in Stream | 703 | Min heap | Easy |
Counting/Bucket Sort Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Maximum Gap | 164 | Bucket sort | Hard |
| H-Index | 274 | Counting sort | Medium |
| Sort Characters By Frequency | 451 | Frequency buckets | Medium |
| Relative Sort Array | 1122 | Counting sort | Easy |
| Sort Array by Frequency | 1636 | Custom comparator | Easy |
Merge Sort Application Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Merge k Sorted Lists | 23 | K-way merge | Hard |
| Sort List | 148 | Linked list merge sort | Medium |
| Count of Smaller Numbers | 315 | Merge sort with count | Hard |
| Count of Range Sum | 327 | Merge sort | Hard |
| Reverse Pairs | 493 | Modified merge sort | Hard |
Greedy Pairing Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Minimize Maximum Pair Sum | 1877 | Sort + two pointers (min+max pairs) | Medium |
| Array Partition | 561 | Sort + pair adjacent elements | Easy |
| Boats to Save People | 881 | Sort + greedy two pointers | Medium |
| Divide Players Into Teams | 2491 | Sort + pair smallest with largest | Medium |
Sorting as a One-Line Preprocessing Step (no new template needed)
| Problem | LC # | Sort/Count trick | Difficulty |
|---|---|---|---|
| Group Anagrams | 49 | sorted(word) (or a 26-length count tuple) as the hash key | Medium |
| Valid Anagram | 242 | sort both strings, or compare frequency maps | Easy |
| Contains Duplicate | 217 | sort then check adjacent pairs (HashSet is better) | Easy |
| Minimum Increment to Make Array Unique | 945 | sort, then push each value to max(v, prev+1) |
Medium |
| Least Number of Unique Integers after K Removals | 1481 | count freq, sort freqs ASC, remove the rarest first | Medium |
Pattern Selection Strategy
Problem Analysis Flowchart:
1. Need custom ordering rules?
├── YES → Custom Comparator
│ ├── Multiple criteria → Tuple comparison
│ └── Complex logic → Comparison function
└── NO → Continue to 2
2. Dealing with dependencies?
├── YES → Topological Sort
│ ├── Detect cycle → Kahn's algorithm
│ └── Find ordering → DFS approach
└── NO → Continue to 3
3. Working with intervals?
├── YES → Sort by start/end
│ ├── Merge overlapping → Greedy merge
│ └── Find conflicts → Sweep line
└── NO → Continue to 4
4. Finding k-th element?
├── YES → Quick Select or Heap
│ ├── One-time query → Quick select O(n)
│ └── Multiple queries → Heap O(n log k)
└── NO → Continue to 5
5. Limited value range?
├── YES → Counting/Bucket Sort
│ ├── Integers → Counting sort
│ └── With precision → Bucket sort
└── NO → Use standard sorting
Summary & Quick Reference
Complexity Quick Reference
| Algorithm | Best Case | Average | Worst Case | Space | Stable |
|---|---|---|---|---|---|
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | No |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | No |
| Tim Sort | O(n) | O(n log n) | O(n log n) | O(n) | Yes |
| Counting Sort | O(n+k) | O(n+k) | O(n+k) | O(k) | Yes |
| Radix Sort | O(nk) | O(nk) | O(nk) | O(n+k) | Yes |
Template Quick Reference
| Template | Pattern | Key Code |
|---|---|---|
| Quick Sort | Partition | pivot; partition; recurse |
| Merge Sort | Divide & merge | mid; merge(left, right) |
| Custom Sort | Comparator | key=lambda x: criteria |
| Quick Select | K-th element | partition until k |
| Counting Sort | Value as index | count[val]++ |
| Topological | Dependencies | in_degree; queue |
Common Patterns & Tricks
Python Sorting Tricks
# Sort with multiple keys
items.sort(key=lambda x: (x[0], -x[1], x[2]))
# Sort by custom class
items.sort(key=lambda x: x.priority, reverse=True)
# Stable sort in multiple passes
items.sort(key=lambda x: x.secondary) # First
items.sort(key=lambda x: x.primary) # Then primary
# In-place vs new list
arr.sort() # In-place
sorted_arr = sorted(arr) # New list
Java Sorting Tricks
// Lambda comparator
Arrays.sort(arr, (a, b) -> a - b);
// Method reference
Arrays.sort(arr, Integer::compare);
// Comparator chaining
Arrays.sort(items, Comparator
.comparing(Item::getPriority)
.thenComparing(Item::getName));
// Reverse order
Arrays.sort(arr, Collections.reverseOrder());
Problem-Solving Steps
-
Identify Sorting Need
- Is sorting necessary?
- Can we use partial sorting?
- Do we need stability?
-
Choose Algorithm
- Dataset size
- Value range
- Memory constraints
- Stability requirement
-
Define Comparison
- Single or multiple keys?
- Ascending or descending?
- Special cases handling
-
Optimize if Needed
- Quick select for k-th element
- Counting sort for limited range
- Bucket sort for uniform distribution
Common Mistakes & Tips
🚫 Common Mistakes:
- Modifying array during custom comparison
- Integer overflow in comparator (a - b)
- Not handling equal elements in comparator
- Using unstable sort when stability needed
- O(n²) algorithms for large datasets
✅ Best Practices:
- Use built-in sort for most cases
- Prefer Integer.compare() over subtraction
- Test with duplicates and edge cases
- Consider partial sorting for k elements
- Use stable sort for equal element ordering
Interview Tips
-
Algorithm Choice
- Start with built-in sort
- Optimize only if needed
- Explain time/space trade-offs
-
Custom Comparator
- Handle all comparison cases
- Avoid integer overflow
- Maintain transitivity
-
Common Questions
- “Why Quick Sort over Merge Sort?”
- “How to make Quick Sort stable?”
- “When to use Counting Sort?”
-
Follow-up Optimizations
- Sort only k elements
- External sorting for large data
- Parallel sorting
Advanced Techniques
Hybrid Sorting
- Tim Sort: Merge + Insertion
- Intro Sort: Quick + Heap + Insertion
- Used in Python and Java standard libraries
External Sorting
- K-way merge for disk-based data
- Used in databases and big data
Parallel Sorting
- Divide data among processors
- Parallel merge or sample sort
Related Topics
- Heap: Priority queue, k-th element
- Binary Search: On sorted arrays
- Divide & Conquer: Merge sort pattern
- Greedy: Interval scheduling
- Graph: Topological ordering
LC Example
2-1) Pancake Sorting — LC 969
# python
# LC 969 Pancake Sorting
# V0
# IDEA : pankcake sort + while loop
# IDEA : 3 STEPS
# -> step 1) Find the maximum number in arr
# -> step 2) Reverse from 0 to max_idx
# -> step 3) Reverse whole list
# https://github.com/yennanliu/CS_basics/blob/master/algorithm/python/pancake_sort.py
class Solution(object):
def pancakeSort(self, arr):
"""Sort Array with Pancake Sort.
:param arr: Collection containing comparable items
:return: Collection ordered in ascending order of items
Examples:
>>> pancake_sort([0, 5, 3, 2, 2])
[0, 2, 2, 3, 5]
>>> pancake_sort([])
[]
>>> pancake_sort([-2, -5, -45])
[-45, -5, -2]
"""
cur = len(arr)
res = []
while cur > 1:
# step 1) Find the maximum number in arr
max_idx = arr.index(max(arr[0:cur]))
res = res + [max_idx+1, cur] # idx is 1 based
# step 2) Reverse from 0 to max_idx
# NOTE: `arr[:max_idx][::-1]` EXCLUDES arr[max_idx]; the commented
# `arr[max_idx::-1]` INCLUDES it — they are NOT equivalent.
# Prefer the commented form to keep the pivot element.
#arr = arr[max_idx::-1] + arr[max_idx + 1 : len(arr)] # includes pivot
arr = arr[:max_idx][::-1] + arr[max_idx + 1 : len(arr)]
# step 3) Reverse whole list
#arr = arr[cur - 1 :: -1] + arr[cur : len(arr)] # this is OK as well
#arr = arr[:cur - 1][::-1] + arr[cur : len(arr)] # this is OK as well
tmp = arr[::-1]
arr = tmp
cur -= 1
print ("arr = " + str(arr))
return res
# V1
# https://leetcode.com/problems/pancake-sorting/discuss/817978/Python-O(n2)-by-simulation-w-Comment
# https://leetcode.com/problems/pancake-sorting/discuss/330990/Python
class Solution:
def pancakeSort(self, A):
res = []
for x in range(len(A), 1, -1):
# Carry out pancake-sort from largest number n to smallest number 1
# find the index of x
i = A.index(x)
# flip first i+1 elements to put x on A[0]
# flip first x elements to put x on A[x-1]
# now, x is on its corresponding position A[x-1] on ascending order
#
"""
# array extend
In [10]: x = [1,2,3]
In [11]: x.extend([4])
In [12]: x
Out[12]: [1, 2, 3, 4]
In [13]: x = [1,2,3]
In [14]: x = x + [4]
In [15]: x
Out[15]: [1, 2, 3, 4]
"""
#res.extend([i + 1, x])
res = res + [i + 1, x]
# update A
"""
https://stackoverflow.com/questions/509211/understanding-slice-notation
a[::-1] # all items in the array, reversed
a[1::-1] # the first two items, reversed
a[:-3:-1] # the last two items, reversed
a[-3::-1] # everything except the last two items, reversed
-> A[:i:-1] : last i items, reversed
"""
A = A[:i:-1] + A[:i]
#print ("res = " + str(res))
return res
# V1
# IDEA : RECURSIVE
# https://leetcode.com/problems/pancake-sorting/discuss/553116/My-python-solution
# https://leetcode.com/problems/pancake-sorting/discuss/274921/PythonDetailed-Explanation-for-This-Problem
class Solution:
def pancakeSort(self, A):
pointer = len(A)
result = []
while pointer > 1:
idx = A.index(pointer)
result.append(idx + 1)
A = A[idx::-1] + A[idx + 1:]
result.append(pointer)
A = A[pointer - 1::-1] + A[pointer:]
pointer -= 1
return result
// java
// aAlgorithm book (labu) p. 347
// record reverse op array
LinkedList<Integer> res = new LinkedList<>();
List<Integer> pancakeSort(int[] cakes){
sort(cakes, cakes.length);
return res;
}
// order first N pancakes
void sort(int[] cakes, int n){
// base case
if (n == 1) return;
// find max index
int maxCake = 0;
int maxCakeIndex = 0;
for (int i = 0; i < n; i ++){
if (cakes[i] > maxCake){
maxCakeIndex = i;
maxCake = cakes[i];
}
}
// after 1st flip, put max pancake to the 1st layer
reverse(cakes, 0, maxCakeIndex);
// record this flip
res.add(maxCakeIndex+1);
// 2nd flip, make max pancake to the bottom (last layer)
reverse(cakes, 0, n-1);
// record this flop
res.add(n);
// recursive call : flip the remaining pancakes
sort(cakes, n-1);
}
/** flip arr[i..j] elements */
void reverse(int[] arr, int i, int j){
while (i < j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
i++;
j--;
}
}
2-2) Reorder Data in Log Files — LC 937
# LC 937. Reorder Data in Log Files
# V0
# IDEA : SORT BY KEY
class Solution:
def reorderLogFiles(self, logs):
def f(log):
id_, rest = log.split(" ", 1)
"""
NOTE !!!
2 cases:
1) case 1: rest[0].isalpha() => sort by rest, id_
2) case 2: rest[0] is digit => DO NOTHING (keep original order)
syntax:
if condition:
return key1, key2, key3 ....
"""
if rest[0].isalpha():
return 0, rest, id_
else:
return 1, None, None
#return 100, None, None # since we need to put Digit-logs behind of Letter-logs, so first key should be ANY DIGIT BIGGER THAN 0
logs.sort(key = lambda x : f(x))
return logs
# V1
# IDEA : SORT BY keys
# https://leetcode.com/problems/reorder-data-in-log-files/solution/
class Solution:
def reorderLogFiles(self, logs):
def get_key(log):
_id, rest = log.split(" ", maxsplit=1)
"""
NOTE !!!
2 cases:
1) case 1: rest[0].isalpha() => sort by rest, id_
2) case 2: rest[0] is digit => DO NOTHING (keep original order)
"""
return (0, rest, _id) if rest[0].isalpha() else (1, )
return sorted(logs, key=get_key)
2-3) Meeting Rooms — LC 252
# LC 252. Meeting Rooms
# V0
class Solution:
def canAttendMeetings(self, intervals):
"""
NOTE this
"""
intervals.sort(key=lambda x: x[0])
for i in range(1, len(intervals)):
"""
NOTE this :
-> we compare ntervals[i][0] and ntervals[i-1][1]
"""
if intervals[i][0] < intervals[i-1][1]:
return False
return True
2-4) Custom Sort String — LC 791
# LC 791. Custom Sort String
# V0
# IDEA : COUNTER
from collections import Counter
class Solution(object):
def customSortString(self, order, s):
s_map = Counter(s)
res = ""
for o in order:
if o in s_map:
res += (o * s_map[o])
del s_map[o]
for s in s_map:
res += s * s_map[s]
return res
2-5) Find K Closest Elements — LC 658
# LC 658. Find K Closest Elements
# NOTE : there is also stack, binary search.. approaches
# V0'
# IDEA : SORTING
class Solution:
def findClosestElements(self, arr, k, x):
# Sort using custom comparator
sorted_arr = sorted(arr, key = lambda num: abs(x - num))
# Only take k elements
result = []
for i in range(k):
result.append(sorted_arr[i])
# Sort again to have output in ascending order
return sorted(result)
2-6) Largest Number — LC 179
# LC 179. Largest Number
# V0
# IDEA : Sorting via Custom Comparator
class compare(str):
# __lt__ defines ">" operator in python
def __lt__(x, y):
return x+y > y+x
class Solution:
def largestNumber(self, nums):
largest = sorted([str(v) for v in nums], key=compare)
largest = ''.join(largest)
return '0' if largest[0] == '0' else largest
2-7) Permutation in String — LC 567
# LC 567
# V0
# IDEA : collections + sliding window
from collections import Counter
class Solution(object):
def checkInclusion(self, s1, s2):
if len(s1) > len(s2):
return False
l = 0
tmp = ""
_s1 = Counter(s1)
_s2 = Counter()
for i, item in enumerate(s2):
### NOTE : we need to append new element first, then compare
_s2[item] += 1
tmp = s2[l:i+1]
if _s2 == _s1 and len(tmp) > 0:
return True
if len(tmp) >= len(s1):
_s2[tmp[0]] -= 1
if _s2[tmp[0]] == 0:
del _s2[tmp[0]]
l += 1
return False
// java
// LC 567
// V2
// IDEA : SORTING
// https://leetcode.com/problems/permutation-in-string/editorial/
public boolean checkInclusion_3(String s1, String s2) {
s1 = sort(s1);
for (int i = 0; i <= s2.length() - s1.length(); i++) {
if (s1.equals(sort(s2.substring(i, i + s1.length()))))
return true;
}
return false;
}
public String sort(String s) {
char[] t = s.toCharArray();
Arrays.sort(t);
return new String(t);
}
2-8) Car Fleet — LC 853
// java
// LC 853. Car Fleet
// V0
// IDEA: pair position and speed, sorting (gpt)
/**
* IDEA :
*
* The approach involves sorting the cars by their starting positions
* (from farthest to nearest to the target)
* and computing their time to reach the target.
* We then iterate through these times to count the number of distinct fleets.
*
*
*
* Steps in the Code:
* 1. Pair Cars with Their Speeds:
* • Combine position and speed into a 2D array cars for easier sorting and access.
* 2. Sort Cars by Position Descending:
* • Use Arrays.sort with a custom comparator to sort cars from farthest to nearest relative to the target.
* 3. Calculate Arrival Times:
* • Compute the time each car takes to reach the target using the formula:
*
* time = (target - position) / speed
*
* 4. Count Fleets:
* • Iterate through the times array:
* • If the current car’s arrival time is greater than the lastTime (time of the last fleet), it forms a new fleet.
* • Update lastTime to the current car’s time.
* 5. Return Fleet Count:
* • The number of distinct times that exceed lastTime corresponds to the number of fleets.
*
*/
public int carFleet(int target, int[] position, int[] speed) {
int n = position.length;
// Pair positions with speeds and `sort by position in descending order`
// cars : [position][speed]
int[][] cars = new int[n][2];
for (int i = 0; i < n; i++) {
cars[i][0] = position[i];
cars[i][1] = speed[i];
}
/**
* NOTE !!!
*
* Sort by position descending (simulate the "car arriving" process
*/
Arrays.sort(cars, (a, b) -> b[0] - a[0]); // Sort by position descending
// Calculate arrival times
double[] times = new double[n];
for (int i = 0; i < n; i++) {
times[i] = (double) (target - cars[i][0]) / cars[i][1];
}
// Count fleets
int fleets = 0;
double lastTime = 0;
for (double time : times) {
/**
* 4. Count Fleets:
* • Iterate through the times array:
* • If the current car’s arrival time is greater than the lastTime (time of the last fleet), it forms a new fleet.
* • Update lastTime to the current car’s time.
*/
// If current car's time is greater than the last fleet's time, it forms a new fleet
if (time > lastTime) {
fleets++;
lastTime = time;
}
}
return fleets;
}
2-9) Minimize Maximum Pair Sum in Array — LC 1877
// java
// LC 1877. Minimize Maximum Pair Sum in Array
// Pattern: Greedy Pairing — Sort + Two Pointers
// Core idea:
// 1. Sort the array
// 2. Pair smallest with largest (two pointers from both ends)
// 3. Track the maximum pair sum across all pairs
//
// Why it works: pairing large+large inflates the max unnecessarily;
// pairing min+max balances every pair sum and minimizes the maximum.
public int minPairSum(int[] nums) {
Arrays.sort(nums);
int left = 0, right = nums.length - 1;
int ans = 0;
while (left < right) {
ans = Math.max(ans, nums[left] + nums[right]);
left++;
right--;
}
return ans;
}
Similar problems using the same Greedy Pairing pattern:
| Problem | LC # | Twist |
|---|---|---|
| Array Partition | 561 | Maximize sum of pair minimums → pair adjacent after sort |
| Boats to Save People | 881 | Minimize boats → greedy two pointers with weight limit |
| Divide Players Into Teams | 2491 | Equal skill sum → pair 1st with last |
2-10) TopK Frequent Words — LC 692
// java
// LC 692
// V0-1
// IDEA: Sort on map key set
public List<String> topKFrequent_0_1(String[] words, int k) {
// IDEA: map sorting
HashMap<String, Integer> freq = new HashMap<>();
for (int i = 0; i < words.length; i++) {
freq.put(words[i], freq.getOrDefault(words[i], 0) + 1);
}
List<String> res = new ArrayList(freq.keySet());
/**
* NOTE !!!
*
* we directly sort over map's keySet
* (with the data val, key that read from map)
*
*
* example:
*
* Collections.sort(res,
* (w1, w2) -> freq.get(w1).equals(freq.get(w2)) ? w1.compareTo(w2) : freq.get(w2) - freq.get(w1));
*/
Collections.sort(res, (x, y) -> {
int valDiff = freq.get(y) - freq.get(x); // sort on `value` bigger number first (decreasing order)
if (valDiff == 0){
// Sort on `key ` with `lexicographically` order (increasing order)
//return y.length() - x.length(); // ?
return x.compareTo(y);
}
return valDiff;
});
// get top K result
return res.subList(0, k);
}