Sorting Algorithms: Comparison, Non-Comparison, Stability, and Hybrids
Sorting rearranges a collection of items into a monotonic order (ascending or descending). It is a fundamental building block for binary search, database indexing (B-Tree splits), join processing (Sort-Merge Joins), and data deduplication.
⚡ Quick Dive
Master Sorting Algorithms Comparison Matrix
| Algorithm | Best Time | Average Time | Worst Time | Space Overhead | Stable? | In-Place? | Paradigm |
|---|---|---|---|---|---|---|---|
| Insertion Sort | $O(n)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | ✅ | ✅ | Incremental (Adaptive) |
| Selection Sort | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | ❌ | ✅ | Selection |
| Bubble Sort | $O(n)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | ✅ | ✅ | Exchange |
| Merge Sort | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(n)$ | ✅ | ❌ | Divide & Conquer |
| QuickSort | $O(n \log n)$ | $O(n \log n)$ | $O(n^2)$ | $O(\log n)$ (Stack) | ❌ | ✅ | Divide & Conquer |
| HeapSort | $O(n \log n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(1)$ | ❌ | ✅ | Selection (Binary Heap) |
| Counting Sort | $O(n + k)$ | $O(n + k)$ | $O(n + k)$ | $O(k)$ | ✅ | ❌ | Non-Comparison (Integer keys) |
| Radix Sort | $O(d \cdot (n + k))$ | $O(d \cdot (n + k))$ | $O(d \cdot (n + k))$ | $O(n + k)$ | ✅ | ❌ | Non-Comparison (Digits) |
| Timsort (Hybrid) | $O(n)$ | $O(n \log n)$ | $O(n \log n)$ | $O(n)$ | ✅ | ❌ | Insertion + Merge (Production) |
📖 Extended Guide
1. Stability in Sorting
A sorting algorithm is stable if two objects with equal keys appear in the same relative order in sorted output as they appeared in the input array.
Input: [ (Alice, Grade: B), (Bob, Grade: A), (Charlie, Grade: B) ]
▲ ▲
Stable Sort by Grade:
Output: [ (Bob, A), (Alice, B), (Charlie, B) ] ◄── Relative order of Alice and Charlie preserved
Unstable Sort by Grade:
Output: [ (Bob, A), (Charlie, B), (Alice, B) ] ◄── Relative order swapped
Stability is critical in multi-key database sorting (e.g. sort by Date, then sort by Department).
2. High-Performance QuickSort: Hoare vs. Lomuto Partitioning
QuickSort operates by choosing a pivot, partitioning the array around the pivot, and recursively sorting subarrays.
- Lomuto Partitioning: Simpler to code; scans with single forward pointer. Performs more swaps and degrades to $O(n^2)$ on arrays with all equal keys.
- Hoare Partitioning: Scans inward from both ends; performs three times fewer swaps on average and handles duplicate keys efficiently.
package main
import "fmt"
// QuickSort using Hoare Partition Scheme
func QuickSort(arr []int, low, high int) {
if low < high {
p := partition(arr, low, high)
QuickSort(arr, low, p)
QuickSort(arr, p+1, high)
}
}
func partition(arr []int, low, high int) int {
pivot := arr[low + (high-low)/2] // Avoid worst-case on sorted inputs
i := low - 1
j := high + 1
for {
for {
i++
if arr[i] >= pivot {
break
}
}
for {
j--
if arr[j] <= pivot {
break
}
}
if i >= j {
return j
}
arr[i], arr[j] = arr[j], arr[i]
}
}
func main() {
nums := []int{38, 27, 43, 3, 9, 82, 10}
QuickSort(nums, 0, len(nums)-1)
fmt.Println("Sorted:", nums)
}
3. Merge Sort (Guaranteed $O(n \log n)$ Stable Sorting)
Merge Sort divides the array into halves, recursively sorts them, and merges the sorted halves:
Divide: [ 38, 27, 43, 3 ]
/ \
[ 38, 27 ] [ 43, 3 ]
/ \ / \
[ 38 ] [ 27 ] [ 43 ] [ 3 ]
Merge: \ / \ /
[ 27, 38 ] [ 3, 43 ]
\ /
[ 3, 27, 38, 43 ]
Merge Sort is ideal for sorting Linked Lists (because merging linked lists requires $O(1)$ auxiliary memory without array copying) and External Sorting (sorting terabytes of data stored on disks that don't fit in RAM).
4. Non-Comparison Linear Sorts: Counting Sort
When input keys are integers bounded within range $[0, k]$, Counting Sort achieves $O(n + k)$ linear time by tallying frequencies:
func CountingSort(arr []int, k int) []int {
count := make([]int, k+1)
output := make([]int, len(arr))
// 1. Store frequencies
for _, v := range arr {
count[v]++
}
// 2. Accumulate prefix sums (positions)
for i := 1; i <= k; i++ {
count[i] += count[i-1]
}
// 3. Build output backward to maintain stability
for i := len(arr) - 1; i >= 0; i-- {
val := arr[i]
output[count[val]-1] = val
count[val]--
}
return output
}
5. Production Hybrid Sorts: Timsort & IntroSort
Modern programming language standard libraries do not use pure theoretical sorting algorithms:
- Timsort (Python
list.sort(), JavaArrays.sort(), Rust): Scans for existing natural sorted runs, extends short runs with Insertion Sort, and merges them using a balanced stack. Achieves $O(n)$ best-case time on real-world partially sorted data while maintaining $O(n \log n)$ worst-case and stability. - IntroSort (C++
std::sort()): Begins with QuickSort for raw speed, monitors recursion depth, and switches to HeapSort if depth exceeds $2\log_2 n$ (preventing $O(n^2)$ worst cases), switching to Insertion Sort for tiny subarrays ($n < 16$).