Sorting Algorithms
Overview
Sorting is one of the most fundamental operations in computer science. Understanding different sorting algorithms, their trade-offs, and when to use each one is crucial for technical interviews and real-world applications.
Algorithm Comparison Table
| Algorithm | Best Case | Average Case | Worst Case | Space | Stable | In-Place | Notes |
|---|---|---|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | ✓ | ✓ | Educational only |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | ✗ | ✓ | Minimal swaps |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | ✓ | ✓ | Good for small arrays |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | ✓ | ✗ | Consistent performance |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | ✗ | ✓ | Average case excellent |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | ✗ | ✓ | Guaranteed O(n log n) |
| Counting Sort | O(n + k) | O(n + k) | O(n + k) | O(k) | ✓ | ✗ | Integer keys only |
| Radix Sort | O(d(n + k)) | O(d(n + k)) | O(d(n + k)) | O(n + k) | ✓ | ✗ | Fixed-length keys |
| Bucket Sort | O(n + k) | O(n + k) | O(n²) | O(n) | ✓ | ✗ | Uniform distribution |
Basic Sorting Algorithms
1. Bubble Sort
Concept: Repeatedly swap adjacent elements if they're in wrong order
function bubbleSort(arr: number[]): number[] {
const n = arr.length;
const result = [...arr];
for (let i = 0; i < n - 1; i++) {
let swapped = false;
for (let j = 0; j < n - i - 1; j++) {
if (result[j] > result[j + 1]) {
[result[j], result[j + 1]] = [result[j + 1], result[j]];
swapped = true;
}
}
// If no swapping occurred, array is sorted
if (!swapped) break;
}
return result;
}2. Selection Sort
Concept: Find minimum element and place it at the beginning
function selectionSort(arr: number[]): number[] {
const result = [...arr];
const n = result.length;
for (let i = 0; i < n - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < n; j++) {
if (result[j] < result[minIndex]) {
minIndex = j;
}
}
if (minIndex !== i) {
[result[i], result[minIndex]] = [result[minIndex], result[i]];
}
}
return result;
}3. Insertion Sort
Concept: Build sorted array one element at a time
function insertionSort(arr: number[]): number[] {
const result = [...arr];
for (let i = 1; i < result.length; i++) {
const key = result[i];
let j = i - 1;
while (j >= 0 && result[j] > key) {
result[j + 1] = result[j];
j--;
}
result[j + 1] = key;
}
return result;
}Advanced Sorting Algorithms
1. Merge Sort
Concept: Divide and conquer - split array and merge sorted halves
function mergeSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left: number[], right: number[]): number[] {
const result: number[] = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i]);
i++;
} else {
result.push(right[j]);
j++;
}
}
return result.concat(left.slice(i)).concat(right.slice(j));
}2. Quick Sort
Concept: Choose pivot, partition around it, recursively sort partitions
function quickSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter(x => x < pivot);
const middle = arr.filter(x => x === pivot);
const right = arr.filter(x => x > pivot);
return [...quickSort(left), ...middle, ...quickSort(right)];
}
// In-place version
function quickSortInPlace(arr: number[], low = 0, high = arr.length - 1): void {
if (low < high) {
const pivotIndex = partition(arr, low, high);
quickSortInPlace(arr, low, pivotIndex - 1);
quickSortInPlace(arr, pivotIndex + 1, high);
}
}
function partition(arr: number[], low: number, high: number): number {
const pivot = arr[high];
let i = low - 1;
for (let j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
return i + 1;
}3. Heap Sort
Concept: Build max heap, repeatedly extract maximum
function heapSort(arr: number[]): number[] {
const result = [...arr];
const n = result.length;
// Build max heap
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(result, n, i);
}
// Extract elements from heap one by one
for (let i = n - 1; i > 0; i--) {
[result[0], result[i]] = [result[i], result[0]];
heapify(result, i, 0);
}
return result;
}
function heapify(arr: number[], n: number, i: number): void {
let largest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest !== i) {
[arr[i], arr[largest]] = [arr[largest], arr[i]];
heapify(arr, n, largest);
}
}Non-Comparison Based Sorting
1. Counting Sort
Use Case: Integer keys with known range
function countingSort(arr: number[], maxValue: number): number[] {
const count = new Array(maxValue + 1).fill(0);
const result = new Array(arr.length);
// Count occurrences
for (const num of arr) {
count[num]++;
}
// Calculate cumulative count
for (let i = 1; i <= maxValue; i++) {
count[i] += count[i - 1];
}
// Build result array
for (let i = arr.length - 1; i >= 0; i--) {
result[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
return result;
}2. Radix Sort
Use Case: Fixed-length integer or string keys
function radixSort(arr: number[]): number[] {
if (arr.length === 0) return arr;
const max = Math.max(...arr);
const result = [...arr];
for (let exp = 1; Math.floor(max / exp) > 0; exp *= 10) {
countingSortByDigit(result, exp);
}
return result;
}
function countingSortByDigit(arr: number[], exp: number): void {
const count = new Array(10).fill(0);
const output = new Array(arr.length);
// Count occurrences of each digit
for (let i = 0; i < arr.length; i++) {
const digit = Math.floor(arr[i] / exp) % 10;
count[digit]++;
}
// Calculate cumulative count
for (let i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// Build output array
for (let i = arr.length - 1; i >= 0; i--) {
const digit = Math.floor(arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
// Copy output array to original array
for (let i = 0; i < arr.length; i++) {
arr[i] = output[i];
}
}3. Bucket Sort
Use Case: Uniformly distributed floating-point numbers
function bucketSort(arr: number[], bucketCount = 10): number[] {
if (arr.length === 0) return arr;
const min = Math.min(...arr);
const max = Math.max(...arr);
const bucketSize = (max - min) / bucketCount;
const buckets: number[][] = Array(bucketCount).fill(null).map(() => []);
// Distribute elements into buckets
for (const num of arr) {
const bucketIndex = Math.min(
Math.floor((num - min) / bucketSize),
bucketCount - 1
);
buckets[bucketIndex].push(num);
}
// Sort individual buckets and concatenate
const result: number[] = [];
for (const bucket of buckets) {
bucket.sort((a, b) => a - b);
result.push(...bucket);
}
return result;
}Specialized Sorting Techniques
1. Tim Sort (Hybrid)
Concept: Combination of merge sort and insertion sort (used in Python and Java)
function timSort(arr: number[]): number[] {
const MIN_MERGE = 32;
const result = [...arr];
const n = result.length;
// Sort individual subarrays of size MIN_MERGE using insertion sort
for (let i = 0; i < n; i += MIN_MERGE) {
const right = Math.min(i + MIN_MERGE - 1, n - 1);
insertionSortRange(result, i, right);
}
// Start merging from size MIN_MERGE
for (let size = MIN_MERGE; size < n; size *= 2) {
for (let start = 0; start < n; start += size * 2) {
const mid = start + size - 1;
const end = Math.min(start + size * 2 - 1, n - 1);
if (mid < end) {
mergeRange(result, start, mid, end);
}
}
}
return result;
}
function insertionSortRange(arr: number[], left: number, right: number): void {
for (let i = left + 1; i <= right; i++) {
const key = arr[i];
let j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
function mergeRange(arr: number[], left: number, mid: number, right: number): void {
const leftArr = arr.slice(left, mid + 1);
const rightArr = arr.slice(mid + 1, right + 1);
let i = 0, j = 0, k = left;
while (i < leftArr.length && j < rightArr.length) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
while (i < leftArr.length) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < rightArr.length) {
arr[k] = rightArr[j];
j++;
k++;
}
}2. External Sorting
Use Case: Sorting data that doesn't fit in memory
class ExternalSort {
private chunkSize: number;
constructor(chunkSize: number = 1000) {
this.chunkSize = chunkSize;
}
sort(data: number[]): number[] {
if (data.length <= this.chunkSize) {
return data.sort((a, b) => a - b);
}
// Phase 1: Sort chunks and store them
const sortedChunks: number[][] = [];
for (let i = 0; i < data.length; i += this.chunkSize) {
const chunk = data.slice(i, i + this.chunkSize);
sortedChunks.push(chunk.sort((a, b) => a - b));
}
// Phase 2: Merge sorted chunks
return this.mergeChunks(sortedChunks);
}
private mergeChunks(chunks: number[][]): number[] {
const result: number[] = [];
const pointers = new Array(chunks.length).fill(0);
while (true) {
let minValue = Infinity;
let minChunkIndex = -1;
// Find minimum value among all chunks
for (let i = 0; i < chunks.length; i++) {
if (pointers[i] < chunks[i].length && chunks[i][pointers[i]] < minValue) {
minValue = chunks[i][pointers[i]];
minChunkIndex = i;
}
}
if (minChunkIndex === -1) break; // All chunks exhausted
result.push(minValue);
pointers[minChunkIndex]++;
}
return result;
}
}Custom Comparators and Object Sorting
interface Person {
name: string;
age: number;
salary: number;
}
// Sort by multiple criteria
function sortPeople(people: Person[]): Person[] {
return people.sort((a, b) => {
// Primary: by age (ascending)
if (a.age !== b.age) {
return a.age - b.age;
}
// Secondary: by salary (descending)
if (a.salary !== b.salary) {
return b.salary - a.salary;
}
// Tertiary: by name (alphabetical)
return a.name.localeCompare(b.name);
});
}
// Generic comparator function
function createComparator<T>(
keyFn: (item: T) => any,
ascending = true
): (a: T, b: T) => number {
return (a: T, b: T) => {
const aKey = keyFn(a);
const bKey = keyFn(b);
if (aKey < bKey) return ascending ? -1 : 1;
if (aKey > bKey) return ascending ? 1 : -1;
return 0;
};
}
// Usage
const people: Person[] = [
{ name: "Alice", age: 30, salary: 50000 },
{ name: "Bob", age: 25, salary: 60000 },
{ name: "Charlie", age: 30, salary: 55000 }
];
// Sort by age
people.sort(createComparator(p => p.age));
// Sort by salary (descending)
people.sort(createComparator(p => p.salary, false));Interview Problem Patterns
1. Merge Intervals
interface Interval {
start: number;
end: number;
}
function mergeIntervals(intervals: Interval[]): Interval[] {
if (intervals.length <= 1) return intervals;
// Sort by start time
intervals.sort((a, b) => a.start - b.start);
const merged: Interval[] = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const current = intervals[i];
const last = merged[merged.length - 1];
if (current.start <= last.end) {
// Overlapping intervals, merge them
last.end = Math.max(last.end, current.end);
} else {
// Non-overlapping interval
merged.push(current);
}
}
return merged;
}2. Top K Elements
function topKFrequent(nums: number[], k: number): number[] {
const frequencyMap = new Map<number, number>();
// Count frequencies
for (const num of nums) {
frequencyMap.set(num, (frequencyMap.get(num) || 0) + 1);
}
// Sort by frequency
const sortedByFreq = Array.from(frequencyMap.entries())
.sort((a, b) => b[1] - a[1]);
return sortedByFreq.slice(0, k).map(([num]) => num);
}3. Meeting Rooms
function canAttendMeetings(intervals: Interval[]): boolean {
intervals.sort((a, b) => a.start - b.start);
for (let i = 1; i < intervals.length; i++) {
if (intervals[i].start < intervals[i - 1].end) {
return false;
}
}
return true;
}
function minMeetingRooms(intervals: Interval[]): number {
if (intervals.length === 0) return 0;
const starts = intervals.map(i => i.start).sort((a, b) => a - b);
const ends = intervals.map(i => i.end).sort((a, b) => a - b);
let rooms = 0;
let endPointer = 0;
for (let i = 0; i < starts.length; i++) {
if (starts[i] >= ends[endPointer]) {
endPointer++;
} else {
rooms++;
}
}
return rooms;
}When to Use Each Algorithm
Choose Based on:
- Data Size: Small (insertion), Large (merge/quick)
- Memory Constraints: In-place (quick/heap) vs Extra space (merge)
- Stability Required: Stable (merge/counting) vs Unstable (quick/heap)
- Data Type: Integers (counting/radix) vs General (comparison-based)
- Performance Guarantees: Worst-case (merge/heap) vs Average-case (quick)
Real-World Applications
- Database Indexing: B-tree sorting
- Graphics: Z-order sorting for rendering
- Networking: Packet scheduling
- File Systems: Directory listing
- Search Engines: Result ranking
Problem Implementations
This directory contains the following problem solutions:
TypeScript Solutions
- Almost Sorted - Determine minimum swaps to sort an array
- Fraudulent Activity Notifications - Detect fraudulent transactions using sorting
- Lily's Homework - Find minimum swaps for beautiful array
- The Full Counting Sort - Stable counting sort implementation
Practice Problems
Easy
- Sort Colors (Dutch Flag)
- Merge Sorted Array
- Intersection of Two Arrays
- Relative Sort Array
Medium
- Merge Intervals
- Top K Frequent Elements
- Sort Characters By Frequency
- Meeting Rooms II
- Largest Number
Hard
- Merge k Sorted Lists
- Count of Smaller Numbers After Self
- Reverse Pairs
- Maximum Gap
Performance Tips
- Use Built-in Sort: For most cases, use
Array.sort() - Custom Comparators: Write efficient comparison functions
- Preprocessing: Sometimes transforming data helps
- Hybrid Approaches: Combine algorithms for better performance
- Memory Management: Consider cache locality and memory usage
Remember: The best sorting algorithm depends on your specific use case, data characteristics, and constraints!