Advanced Sorting Algorithms Visualized
Explore non-comparison sorting algorithms and selection algorithms including QuickSelect, Counting Sort, and Lexicographic Counting Sort.
Beyond the classic comparison-based sorting algorithms, there are specialized algorithms that can achieve better performance for specific data types or solve related problems like finding the k-th smallest element.
Why Non-Comparison Sorts?
Comparison-based sorting algorithms have a theoretical lower bound of . Non-comparison sorts like Counting Sort can break this barrier by exploiting properties of the data itself!
When you know your data consists of integers within a limited range, or when you need to sort by specific digit positions, these algorithms can significantly outperform traditional comparison-based approaches.
Selection Algorithms
QuickSelect
QuickSelect is a selection algorithm that finds the k-th smallest element in an unordered list. It's related to QuickSort but only recurses into the partition containing the target element, making it much faster on average.
How it works:
- Choose a pivot element (similar to QuickSort)
- Partition the array around the pivot
- If pivot's final position equals k, we found our answer
- If k < pivot position, recurse on left partition
- If k > pivot position, recurse on right partition
QuickSelect is the algorithm behind many "find median" or "find top-k" implementations. It's used in numpy's partition function and various database query optimizers.
Use cases:
- Finding the median of an array
- Finding the k largest/smallest elements
- Order statistics problems
Non-Comparison Sorting
Counting Sort
Counting Sort works by counting the occurrences of each distinct element and using arithmetic to determine positions. It's extremely efficient when the range of input values (k) is not significantly larger than the number of elements (n).
How it works:
- Create a count array of size (max value + 1)
- Count occurrences of each element
- Compute cumulative counts (prefix sums)
- Place elements in output array using counts as indices
- Decrement counts to handle duplicates
Counting Sort is stable, meaning equal elements maintain their relative order. This property is crucial for Radix Sort and LexCounting Sort, which use Counting Sort as a subroutine!
Limitations:
- Only works with non-negative integers (or requires mapping)
- Inefficient when range k >> n
- Requires additional space
Lexicographic Counting Sort (LexCounting Sort)
LexCounting Sort is a variant of Radix Sort that sorts elements by processing digit positions from least significant to most significant, using Counting Sort as a stable sorting subroutine for each position.
Pseudocode:
S := X_1, X_2, ..., X_n
For i := k, ..., 1:
S := CountingSort(S) by coordinate i
Return SHow it works:
- Determine the maximum number of digits (k) in the largest element
- For each digit position from rightmost (1) to leftmost (k):
- Apply Counting Sort based on that digit only
- The stability of Counting Sort preserves previous ordering
- After processing all digits, the array is fully sorted
Where:
- d = number of digits (or coordinates)
- n = number of elements
- k = range of each digit (0-9 for decimal)
Why process from least to most significant?
Processing from most to least significant would require recursive subdivision. The LSD (Least Significant Digit) approach works because Counting Sort is stable—when we sort by a more significant digit, elements with the same digit remain in their previously sorted order.
Example walkthrough:
Consider sorting: [170, 45, 75, 90, 2, 24, 802, 66]
After digit 1 (ones): [170, 90, 2, 802, 24, 45, 75, 66]
After digit 2 (tens): [2, 802, 24, 45, 66, 170, 75, 90]
After digit 3 (hundreds): [2, 24, 45, 66, 75, 90, 170, 802]
Algorithm Comparison
| Algorithm | Time | Space | Comparison-based | Stable | Best Use Case |
|---|---|---|---|---|---|
| QuickSelect | avg | Yes | N/A | Finding k-th element | |
| Counting Sort | No | Yes | Small range integers | ||
| LexCounting | No | Yes | Fixed-width integers |
Code Examples
QuickSelect
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], arr[j]);
}
}
swap(arr[i + 1], arr[high]);
return i + 1;
}
int quickSelect(int arr[], int low, int high, int k) {
if (low <= high) {
int pivotIdx = partition(arr, low, high);
if (pivotIdx == k) return arr[pivotIdx];
if (pivotIdx > k) return quickSelect(arr, low, pivotIdx - 1, k);
return quickSelect(arr, pivotIdx + 1, high, k);
}
return -1;
}Counting Sort
void countingSort(int arr[], int n) {
int maxVal = *max_element(arr, arr + n);
int count[maxVal + 1] = {0};
int output[n];
for (int i = 0; i < n; i++) count[arr[i]]++;
for (int i = 1; i <= maxVal; i++) count[i] += count[i - 1];
for (int i = n - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
for (int i = 0; i < n; i++) arr[i] = output[i];
}LexCounting Sort (Radix Sort LSD)
void countingSortByDigit(int arr[], int n, int exp) {
int output[n], count[10] = {0};
for (int i = 0; i < n; i++) count[(arr[i] / 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] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
for (int i = 0; i < n; i++) arr[i] = output[i];
}
void lexCountingSort(int arr[], int n) {
int maxVal = *max_element(arr, arr + n);
for (int exp = 1; maxVal / exp > 0; exp *= 10) {
countingSortByDigit(arr, n, exp);
}
}Try It Yourself
Use the interactive visualizer below to see how each algorithm works step by step. Try different array sizes and observe how the count array evolves during Counting Sort, or how LexCounting Sort processes each digit position!
Interactive Visualizer
Related Tutorials
A visual guide to understanding common sorting algorithms including Bubble Sort, Selection Sort, Insertion Sort, Quick Sort, and Merge Sort.
What Big O actually measures, how each growth class behaves as input grows, and when complexity decides your architecture versus when it is the wrong thing to optimize.