Know Merge Sort (stable, O(n log n) guaranteed), Quick Sort (in-place, cache-friendly), and when to reach for Counting/Radix Sort to beat O(n log n) entirely.
Sort
Merge Sort — O(n log n), stable, divide & conquer
Merge Sort guarantees O(n log n) in all cases and is stable (equal elements keep their order). It's the algorithm behind LINQ's OrderBy and Array.Sort for reference types.
// ── Merge Sort ────────────────────────────────────────
// Time: O(n log n) | Space: O(n) auxiliary | Stable: yes
static void MergeSort(int[] arr, int lo, int hi)
{
if (lo >= hi) return; // base case: 1 element
int mid = lo + (hi - lo) / 2;
MergeSort(arr, lo, mid); // sort left half
MergeSort(arr, mid + 1, hi); // sort right half
Merge(arr, lo, mid, hi); // merge sorted halves
}
static void Merge(int[] arr, int lo, int mid, int hi)
{
// Copy to temporary arrays
int leftLen = mid - lo + 1;
int rightLen = hi - mid;
var left = arr[lo..(mid + 1)];
var right = arr[(mid + 1)..(hi + 1)];
int i = 0, j = 0, k = lo;
while (i < leftLen && j < rightLen)
arr[k++] = left[i] <= right[j] ? left[i++] : right[j++];
while (i < leftLen) arr[k++] = left[i++];
while (j < rightLen) arr[k++] = right[j++];
}
// ── Usage ─────────────────────────────────────────────
int[] data = [5, 2, 8, 1, 9, 3];
MergeSort(data, 0, data.Length - 1);
// data → [1, 2, 3, 5, 8, 9]
// ── .NET equivalent ───────────────────────────────────
// Array.Sort uses IntroSort (Quicksort + Heapsort + Insertion)
Array.Sort(data);
// For objects — LINQ OrderBy uses stable merge sort internally
var sorted = orders
.OrderBy(o => o.Total)
.ThenBy(o => o.Date)
.ToList();
Sort
Quick Sort — O(n log n) average, in-place
Quick Sort is in-place and cache-friendly — typically faster than Merge Sort in practice. The key is pivot choice: always-first pivot degrades to O(n²) on sorted input; median-of-three avoids this.
// ── Quick Sort with median-of-three pivot ─────────────
// Time: O(n log n) avg, O(n²) worst | Space: O(log n) stack | Stable: no
static void QuickSort(int[] arr, int lo, int hi)
{
if (lo >= hi) return;
int p = Partition(arr, lo, hi);
QuickSort(arr, lo, p - 1);
QuickSort(arr, p + 1, hi);
}
static int Partition(int[] arr, int lo, int hi)
{
// Median-of-three: reduces worst-case probability on sorted input
int mid = lo + (hi - lo) / 2;
if (arr[mid] < arr[lo]) Swap(arr, lo, mid);
if (arr[hi] < arr[lo]) Swap(arr, lo, hi);
if (arr[mid] < arr[hi]) Swap(arr, mid, hi);
// arr[hi] is now the median → use as pivot
int pivot = arr[hi];
int i = lo - 1;
for (int j = lo; j < hi; j++)
if (arr[j] <= pivot) Swap(arr, ++i, j);
Swap(arr, i + 1, hi);
return i + 1;
}
static void Swap(int[] arr, int i, int j) =>
(arr[i], arr[j]) = (arr[j], arr[i]);
// ── Choosing a sort ───────────────────────────────────
// Built-in Array.Sort → IntroSort (QS + HeapSort hybrid, not stable)
// LINQ .OrderBy → stable merge sort, allocates
// For primitives → Array.Sort (fastest, in-place)
// For objects → Array.Sort / List.Sort with IComparer<T>
// For stability needed → LINQ OrderBy or your MergeSort
// ── Span<T> sort (no allocation) ─────────────────────
Span<int> span = stackalloc int[] { 5, 2, 8, 1, 9, 3 };
span.Sort(); // System.MemoryExtensions.Sort — in-place, O(n log n)
Sort
Counting, Radix & when O(n) beats O(n log n)
Comparison-based sorts can't beat O(n log n). But if you know the value domain, Counting Sort and Radix Sort achieve O(n) — crucial for large arrays of bounded integers.
// ── Counting Sort — O(n + k) where k = value range ───
// Stable, O(n + k) time/space. Use when values are small integers.
static int[] CountingSort(int[] arr, int maxVal)
{
var count = new int[maxVal + 1];
var output = new int[arr.Length];
foreach (var x in arr) count[x]++;
for (int i = 1; i <= maxVal; i++) count[i] += count[i - 1];
for (int i = arr.Length - 1; i >= 0; i--) // reverse for stability
output[--count[arr[i]]] = arr[i];
return output;
}
// Example: sort ages 0–120
int[] ages = [25, 17, 42, 25, 8, 17];
var sortedAges = CountingSort(ages, 120);
// ── Radix Sort — O(d * n) where d = number of digits ─
// Sort by least-significant digit first using stable counting sort
static int[] RadixSort(int[] arr)
{
int max = arr.Max();
for (int exp = 1; max / exp > 0; exp *= 10)
arr = CountingSortByDigit(arr, exp);
return arr;
}
static int[] CountingSortByDigit(int[] arr, int exp)
{
var output = new int[arr.Length];
var count = new int[10];
foreach (var x in arr) count[(x / exp) % 10]++;
for (int i = 1; i < 10; i++) count[i] += count[i - 1];
for (int i = arr.Length - 1; i >= 0; i--)
{
int digit = (arr[i] / exp) % 10;
output[--count[digit]] = arr[i];
}
return output;
}
// ── When to use which sort ────────────────────────────
// General purpose, unknown range → Array.Sort (IntroSort, O(n log n))
// Stability required → LINQ OrderBy (MergeSort, O(n log n))
// Integers, small range (0–k) → Counting Sort (O(n + k))
// Integers, large range, few digits → Radix Sort (O(d * n))