skip to content
Site header image Minh Ton

Non-Comparison-Based Sorting Algorithms - DSA #7

How can Non-Comparison-Based Sorting Algorithms, such as Counting Sort and Radix Sort, so performant with just linear time complexity?

Last Updated:

👋
Hey there! Welcome to my series of notes on Data Structures and Algorithms!


Efficient sorting algorithms, as discussed in my previous note (Advanced Sorting Algorithms - DSA #6), share several similarities. They rely heavily on comparison operations and cannot achieve a runtime faster than linearithmic time. As a result, the lower bound complexity for these algorithms is O(nlogn)\mathcal{O}(n \log n).

In contrast, non-comparison-based sorting algorithms can achieve linear time complexity. However, this speed comes at a cost—they require additional information about the elements in the array. This introduces several limitations, such as significant extra memory usage due to auxiliary variables and applicability only to integers.

In this note, we’ll dive into these non-comparison-based sorting algorithms - Counting Sort, Radix Sort, and Counting Radix Sort - by discovering how these algorithm work and analyze the efficiency of each algorithm.

The Stability of a Sorting Algorithm

Before getting into these sorting algorithms, we first need to know the stability of a sorting algorithm, as these non-comparison-based sorting algorithms are stable ones. Here’s the definition:

The stability of a sorting algorithm is the property that the elements that compare to be equal preserve their original order after sorting.

A sorting algorithm is stable if elements that are of the same value do not change their order after the sorting.

For example, assume that we have two identical number in an array. After sorting, if a number that appears first in the input array also appears first in the output array, and a number that appears last in the input array also appears last in the output array, the sorting algorithm is considered stable.

Counting Sort

Basic Concept of Counting Sort

The idea of Counting Sort is pretty simple. For each element of an array to be sorted, the total number of elements smaller than this element will indicate the position of the element in the sorted array. The sorted order this approach determines is based on counting, therefore it’s called Counting Sort!

Let’s say we pick an element from our array. If the number of elements which are smaller than our selected element is 55, our selected element will definitely be in the 6th6^{th} position in the sorted array.

How can we implement this idea?

A Silly Approach

One may propose a simple implementation as follows (assume that our array contains no duplicate values):

Assume that we have an unsorted array of 55 elements:

a1a2a3a4a579136\begin{array}{|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 \\ \hline 7 & 9 & 1 & 3 & 6 \\ \hline \end{array}

We’ll define a new array, countcount, to store the number of elements which are smaller than the current element. For example, there are 33 elements smaller than a1=7a_1 = 7 (1,3,61, 3, 6).

a1a2a3a4a5a79136count34012\begin{array}{|c|c|c|c|c|c|} \hline & a_1 & a_2 & a_3 & a_4 & a_5 \\ \hline a & 7 & 9 & 1 & 3 & 6 \\ \hline count & 3 & 4 & 0 & 1 & 2 \\ \hline \end{array}
count[1..n] = 0;
for (i = 1; i ≤ n - 1; i++)
	for (j = i + 1; j ≤ n; j++)
		if (a[i] < a[j])
			count[j]++;
		else
			count[i]++;

Finally, we’ll use the values of the countcount array to determine the final position of each element. We’re going to use an auxiliary array bb to store the elements and copy the sorted elements back to the aa array later on.

a1a2a3a4a5a79136count34012b1b2b3b4b513679\begin{array}{|c|c|c|c|c|c|} \hline & a_1 & a_2 & a_3 & a_4 & a_5 \\ \hline a & 7 & 9 & 1 & 3 & 6 \\ \hline count & 3 & 4 & 0 & 1 & 2 \\ \hline \end{array} \\ \\[20pt] \begin{array}{|c|c|c|c|c|c|} \hline b_1 & b_2 & b_3 & b_4 & b_5 \\ \hline 1 & 3 & 6 & 7 & 9 \\ \hline \end{array}
for (i = 1; i ≤ n; i++)
	b[count[i] + 1] = a[i];
a[1..n] = b[1..n];

However, this silly approach has a time complexity of O(n2)\mathcal{O}(n^2) since we’re using two nested ‘for’ loops in order to collect the information of the elements. Moreover, the space complexity is twice larger, as we have to initialize two auxiliary arrays, countcount and bb. Therefore, this approach is super inefficient!

A More Efficient Approach

We want this algorithm to run in linear time, so we need to find a way to collect the information of the elements without using nested ‘for’ loops. We’re going to determine the range that all of the elements in the array belong to, and take advantage of this range to count the number of elements smaller than the selected element.

We’re going to label this range [l,u][l, u], where ll and uu is the lower bound and upper bound of this range, respectively. For simplicity, we just assume l=0l = 0.

a1a2a3a4a5a6a73588183\begin{array}{|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 \\ \hline 3 & 5 & 8 & 8 & 1 & 8 & 3 \\ \hline \end{array}

It’s easy to determine this range using a linear search to find the largest value. In this sample array, the range is [0,8][0, 8]. Let’s initialize an array f[0..u]f[0..u] with zeroes as our counting array.

At first, we compute the frequency of the value of each element in array aa and store them in the array ff:

Sample array:a1a2a3a4a5a6a73588183Initialize f[0..u]:f0f1f2f3f4f5f6f7f8000000000Compute the frequency:f0f1f2f3f4f5f6f7f8010201003\text{Sample array}: \begin{array}{|c|c|c|c|c|c|c|}\hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 \\\hline3 & 5 & 8 & 8 & 1 & 8 & 3 \\\hline\end{array} \\ \\[20pt] \text{Initialize } f[0..u]: \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\\hline\end{array} \\ \\[20pt] \text{Compute the frequency}: \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 1 & 0 & 2 & 0 & 1 & 0 & 0 & 3 \\\hline \end{array}
f[0..u] = 0;
for (i = 1; i ≤ n; i++) 
	f[a[i]]++;

Let’s assume that j=f[i]j = f[i], in this case, jj is the number of times the value ii appears in the array.

Next, we will iterate through our array ff starting from element f[1]f[1], and on each iteration we will compute f[i]=f[i1]+f[i]f[i] = f[i - 1] + f[i]. Since such accumulated sums of frequencies are called a distribution in statistics, the method itself is known as distribution counting.

f[0..u]:f0f1f2f3f4f5f6f7f8010201003f[i]=f[i1]+f[i]:f0f1f2f3f4f5f6f7f8011334447f[0..u]: \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 1 & 0 & 2 & 0 & 1 & 0 & 0 & 3 \\\hline \end{array} \\ \\[20pt] f[i] = f[i - 1] + f[i]: \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 1 & 1 & 3 & 3 & 4 & 4 & 4 & 7 \\\hline \end{array}
for (i = 1; i ≤ u; i++)
	f[i] = f[i - 1] + f[i];

After this step, j=f[i]j = f[i] is no longer represents just the count of occurrences of ii in the array. Instead, j=f[i]j = f[i] now holds the number of elements which are less than or equal to ii.

For instance, f[6]=4f[6] = 4 indicates that there are 44 elements in the input array that are smaller or equal to 66. Getting these distribution values is what we wanted, and it can be done without using nested ‘for’ loops!

Moreover, these distribution values also help in placing elements correctly while constructing the sorted array. Specifically, f[a[i]]f[a[i]] provides the correct index for the last occurrence of a[i]a[i] in the sorted array. Thus, we populate the output array bb by assigning b[f[a[i]]]=a[i]b[f[a[i]]] = a[i]. After placing an element, we decrement f[a[i]]f[a[i]] by 11. This ensures that if a[i]a[i] appears again in the input, its next occurrence is placed in the correct position in bb.

for (i = n; i ≥ 1; i--) {
	b[f[a[i]]] = a[i];
	f[a[i]]--; 
}
a[1..n] = b[1..n];

Lastly, we copy the sorted elements back to the aa array, and the original array aa is sorted!

With this in mind, let’s arrange the elements of aa to the correct positions in bb:

a1a2a3a4a5a6a73588183f0f1f2f3f4f5f6f7f8011334447\begin{array}{|c|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 & a_7 \\ \hline 3 & 5 & 8 & 8 & 1 & 8 & 3 \\ \hline \end{array} \\ \\[10pt]\\ \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 1 & 1 & 3 & 3 & 4 & 4 & 4 & 7 \\\hline \end{array}

Case i=7i = 7:

f[a[i]]=f[a[7]]=f[3]=3b[3]=a[7]=3b1b2b3b4b5b6b73f[3]=f[3]1=31=2f0f1f2f3f4f5f6f7f8011234447f[a[i]] = f[a[7]] = f[3] = 3 \\\rightarrow b[3] = a[7] = 3\\ \begin{array}{|c|c|c|c|c|c|c|}\hline b_1 & b_2 & b_3 & b_4 & b_5 & b_6 & b_7 \\\hline & & 3 \\\hline \end{array} \\ \\[5pt]\\ f[3] = f[3] - 1 = 3 - 1 = 2 \\ \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 1 & 1 & 2 & 3 & 4 & 4 & 4 & 7 \\\hline \end{array}

Case i=6i = 6:

f[a[i]]=f[a[6]]=f[8]=7b[7]=a[6]=8b1b2b3b4b5b6b738f[8]=f[8]1=71=6f0f1f2f3f4f5f6f7f8011234446f[a[i]] = f[a[6]] = f[8] = 7 \\\rightarrow b[7] = a[6] = 8\\ \begin{array}{|c|c|c|c|c|c|c|}\hline b_1 & b_2 & b_3 & b_4 & b_5 & b_6 & b_7 \\\hline & & 3 & & & & 8\\\hline \end{array} \\ \\[5pt]\\ f[8] = f[8] - 1 = 7 - 1 = 6 \\ \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 1 & 1 & 2 & 3 & 4 & 4 & 4 & 6 \\\hline \end{array}

Case i=5i = 5:

f[a[i]]=f[a[5]]=f[1]=1b[1]=a[5]=1b1b2b3b4b5b6b7138f[1]=f[1]1=11=0f0f1f2f3f4f5f6f7f8001234446f[a[i]] = f[a[5]] = f[1] = 1 \\\rightarrow b[1] = a[5] = 1\\ \begin{array}{|c|c|c|c|c|c|c|}\hline b_1 & b_2 & b_3 & b_4 & b_5 & b_6 & b_7 \\\hline 1 & & 3 & & & & 8\\\hline \end{array} \\ \\[5pt]\\ f[1] = f[1] - 1 = 1 - 1 = 0 \\ \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 0 & 1 & 2 & 3 & 4 & 4 & 4 & 6 \\\hline \end{array}

Case i=4i = 4:

f[a[i]]=f[a[4]]=f[8]=6b[6]=a[4]=8b1b2b3b4b5b6b71388f[8]=f[8]1=61=5f0f1f2f3f4f5f6f7f8001234445f[a[i]] = f[a[4]] = f[8] = 6 \\\rightarrow b[6] = a[4] = 8\\ \begin{array}{|c|c|c|c|c|c|c|}\hline b_1 & b_2 & b_3 & b_4 & b_5 & b_6 & b_7 \\\hline 1 & & 3 & & & 8 & 8\\\hline \end{array} \\ \\[5pt]\\ f[8] = f[8] - 1 = 6 - 1 = 5 \\ \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 0 & 1 & 2 & 3 & 4 & 4 & 4 & 5 \\\hline \end{array}

Case i=3i = 3:

f[a[i]]=f[a[3]]=f[8]=5b[5]=a[3]=8b1b2b3b4b5b6b713888f[8]=f[8]1=51=4f0f1f2f3f4f5f6f7f8001234444f[a[i]] = f[a[3]] = f[8] = 5 \\\rightarrow b[5] = a[3] = 8\\ \begin{array}{|c|c|c|c|c|c|c|}\hline b_1 & b_2 & b_3 & b_4 & b_5 & b_6 & b_7 \\\hline 1 & & 3 & & 8 & 8 & 8\\\hline \end{array} \\ \\[5pt]\\ f[8] = f[8] - 1 = 5 - 1 = 4 \\ \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 0 & 1 & 2 & 3 & 4 & 4 & 4 & 4 \\\hline \end{array}

Case i=2i = 2:

f[a[i]]=f[a[2]]=f[5]=4b[4]=a[2]=5b1b2b3b4b5b6b7135888f[5]=f[5]1=41=3f0f1f2f3f4f5f6f7f8001233444f[a[i]] = f[a[2]] = f[5] = 4 \\\rightarrow b[4] = a[2] = 5\\ \begin{array}{|c|c|c|c|c|c|c|}\hline b_1 & b_2 & b_3 & b_4 & b_5 & b_6 & b_7 \\\hline 1 & & 3 & 5 & 8 & 8 & 8\\\hline \end{array} \\ \\[5pt]\\ f[5] = f[5] - 1 = 4 - 1 = 3 \\ \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 0 & 1 & 2 & 3 & 3 & 4 & 4 & 4 \\\hline \end{array}

Case i=1i = 1:

f[a[i]]=f[a[1]]=f[3]=2b[2]=a[1]=3b1b2b3b4b5b6b71335888f[3]=f[3]1=21=1f0f1f2f3f4f5f6f7f8001133444f[a[i]] = f[a[1]] = f[3] = 2 \\\rightarrow b[2] = a[1] = 3\\ \begin{array}{|c|c|c|c|c|c|c|}\hline b_1 & b_2 & b_3 & b_4 & b_5 & b_6 & b_7 \\\hline 1 & 3 & 3 & 5 & 8 & 8 & 8\\\hline \end{array} \\ \\[5pt]\\ f[3] = f[3] - 1 = 2 - 1 = 1 \\ \begin{array}{|c|c|c|c|c|c|c|c|c|}\hline f_0 & f_1 & f_2 & f_3 & f_4 & f_5 & f_6 & f_7 & f_8 \\\hline0 & 0 & 1 & 1 & 3 & 3 & 4 & 4 & 4 \\\hline \end{array}

The illustration also shows that Counting Sort is a stable sorting algorithm. When placing elements into the array bb, the algorithm processes the input array aa in reverse order. The distribution value f[a[i]]f[a[i]] ensures that later occurrences of the same value are placed after earlier ones, thereby preserving their relative order in the sorted array.

Determine the Correct Range

In fact, we can make Counting Sort even more efficient by determining the correct range of [l,u][l, u] instead of [0,u][0, u]! We can achieve this by determining the lower bound value ll and adjust the indices by subtracting ll when counting each element.

range = u - l + 1;
f[0..range] = 0;

for (i = 1; i ≤ n; i++) 
	f[a[i] - l]++;
	
for (i = 1; i < u; i++)
	f[i] = f[i - 1] + f[i];
	
for (i = n; i ≥ 1; i--) {
	b[f[a[i] - l]] = a[i];
	f[a[i] - l]--; 
}

a[1..n] = b[1..n];

Performance Analysis of Counting Sort

This is a linear algorithm because it makes just two consecutive passes through its input array. This is better in terms of time complexity than that of the efficient sorting algorithms such as Merge Sort, Heap Sort, and Quick Sort. To be more specific, there are a total of six loops with the equivalent complexity of linear time, Θ(n)\Theta(n) and Θ(u)\Theta(u), resulting in the complexity of linear time Θ(n+u)\Theta(n + u).

Counting Sort needs two auxiliary arrays of size nn and uu for the storing the distribution values and the elements in the sorted order, hence the space complexity of O(n)\mathcal{O}(n). In practice, this algorithm should be used when unu ≤ n. It would be a disaster if unu \gg n, for example a[1..3]=[109,1,10]a[1..3] = [10^9, 1, 10] would require the ff array to have a total of 10910^9 elements!

Radix Sort

Basic Concept of Radix Sort

Radix Sort, in principle, is a sorting algorithm that works with whole numbers in all bases. If we let dd be the number of digits of the largest element of an input array, the algorithm only runs dd passes to complete the sorting process!

For simplicity, we’ll assume that the numbers to be sorted are in decimal base. As an auxiliary data structure, the algorithm also uses ten bins to store numbers. These bins are always emptied before each of the dd passes.

Radix Sort can sort the array using either the least significant digit (LSD) or the most significant digit (MSD). The LSD of a number is the digit with the lowest exponent value, located in the rightmost position (and vice-versa for the MSD). For example, the LSD of 12341234 is 44.

In this note, we’ll be focusing on Least Significant Digit Radix Sort!

A Simple Implementation of Radix Sort

Radix Sort sorts the array on the LSD first:

  • In this step, a number with the LSD ii is stored in the ii bin. All numbers are then combined into an array.
  • The numbers from bin 0 come first, followed by those in bin 1, then bin 2, and so on.

The algorithm continue to sort the array on the second-least significant digit and recombine the array afterwards.

The process continues until the numbers have been sorted on all dd digits.

Let’s visualize LSD Radix Sort using a sample array:

a1a2a3a4a5a6281435507911220\begin{array}{|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 \\ \hline 28 & 143 & 550 & 7 & 911 & 220 \\ \hline \end{array}

The largest element of this array has d=3d = 3 digits, so this array needs 33 passes to complete the sorting process. As mentioned above, we’ll create ten bins to store the numbers:

1st passbin0123456789\begin{array}{|c|c|c|c|c|c|c|c|c|c|c|} \hline 1^{st} \text{ pass} \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array}

In the 1st1^{st} pass, we’ll traverse through the array aa, put the numbers to their respective bins using the LSD marked in red, and reconstruct the array aa:

a[1]=281st pass28bin0123456789a[2]=1431st pass14328bin0123456789a[3]=5501st pass55014328bin0123456789a[4]=71st pass550143728bin0123456789a[5]=9111st pass550911143728bin0123456789a[6]=2201st pass220550911143728bin0123456789Reconstructed Arraya1a2a3a4a5a6550220911143728a[1] = 28 \\ \begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 1^{st} \text{ pass} & & & & & & & & & 2\color{red}8 & \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ a[2] = 143\\ \begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 1^{st} \text{ pass} & & & & 14\color{red}3 & & & & & 2\color{red}8 & \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ a[3] = 550\\ \begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 1^{st} \text{ pass} & 55\color{red}0 & & & 14\color{red}3 & & & & & 2\color{red}8 & \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ a[4] = 7\\ \begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 1^{st} \text{ pass} & 55\color{red}0 & & & 14\color{red}3 & & & & \color{red}7 & 2\color{red}8 & \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ a[5] = 911\\ \begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 1^{st} \text{ pass} & 55\color{red}0 & 91\color{red}1 & & 14\color{red}3 & & & & \color{red}7 & 2\color{red}8 & \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ a[6] = 220\\ \begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 1^{st} \text{ pass} & 22\color{red}0 \\ & 55\color{red}0 & 91\color{red}1 & & 14\color{red}3 & & & & \color{red}7 & 2\color{red}8 & \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ \text{Reconstructed Array}\\ \begin{array}{|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 \\ \hline 550 & 220 & 911 & 143 & 7 & 28 \\ \hline \end{array}

In the 2nd2^{nd} pass:

2nd passbin01234567892nd pass2807911220143550bin0123456789Reconstructed Arraya1a2a3a4a5a6791122028143550\begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 2^{nd} \text{ pass} \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ \begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 2^{nd} \text{ pass} & & & {\color{red}{2}}8 \\ & {\color{red}{0}}7 & 9{\color{red}{1}}1 & 2{\color{red}{2}}0 & & 1{\color{red}{4}}3 & 5{\color{red}{5}}0 \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ \text{Reconstructed Array}\\ \begin{array}{|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 \\ \hline 7 & 911 & 220 & 28 & 143 & 550 \\ \hline \end{array}

Remember, the bins need to be emptied before each pass. Let’s continue with the 3rd3^{rd} pass:

3rd passbin01234567893rd pass028007143220550911bin0123456789Reconstructed Arraya1a2a3a4a5a6728143220550911\begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 3^{rd} \text{ pass} \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 \\ \hline \end{array} \\ \\[10pt]\\ \begin{array}{|l|l|l|l|l|l|l|l|l|l|l|} \hline 3^{rd} \text{ pass} & {\color{red}{0}}28 & & \\ & {\color{red}{0}}07 & {\color{red}{1}}43 & {\color{red}{2}}20 & & & {\color{red}{5}}50 & & & & {\color{red}{9}}11 \\ \hline \text{bin} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9\\ \hline \end{array} \\ \\[10pt]\\ \text{Reconstructed Array}\\ \begin{array}{|c|c|c|c|c|c|} \hline a_1 & a_2 & a_3 & a_4 & a_5 & a_6 \\ \hline 7 & 28 & 143 & 220 & 550 & 911 \\ \hline \end{array}

The pictorial illustration also shows that Radix Sort is a stable sorting algorithm. This is because elements are inserted into the bins in their original order, therefore that order for equal keys remain unchanged after reconstructing the array.

Let’s implement the above idea of Radix Sort, using static arrays.

We’ll be using three arrays: a[1..n] as our input array, b[10][n] as our bins, and counts[10] to keep track how many elements are in each bin.

RadixSort(a[1..n], n) {
	int bins[10][n];
	int counts[10];

	int maxValue = a[1];
	for (i = 2; i ≤ n; i++) 
		// Find the largest element of the array
		if (a[i] > maxValue) maxValue = a[i];

	// Loop process each digit     
	for (exp = 1; maxValue / exp > 0; exp *= 10) {
		// Instead of resetting the bins,
		// we reset the element count of each bin.
		for (bin = 0; bin < 10; bin++) counts[bin] = 0;
		
		for (i = 1; i ≤ n; i++) {
			// Extract the LSD
			digit = (a[i] / exp) % 10;
			// Put the element into its respective bin
			bins[digit][counts[digit]] = a[i];
			counts[digit]++; // Update the element count
		}
		
		index = 1;
		for (bin = 0; bin < 10; bin++) {
		  for (i = 0; i < counts[bin]; i++) {
			  // Reconstruct the array
		    a[index++] = bins[bin][i];
	    }
	  }
	}
}

This is definitely good enough. But can we improve it further by dynamically allocating the memory for the arrays?

Radix Sort Using Dynamic Memory

The arrays will be dynamically allocated like so: *a, *counts, and **bins. After each pass, the *counts and the **bins arrays are deallocated.

RadixSort(int* &a, n) {
	int maxValue = a[1];
	for (i = 2; i ≤ n; i++) 
		if (a[i] > maxValue) maxValue = a[i];

	for (exp = 1; maxValue / exp > 0; exp *= 10) {
		// Initialize the counts array with 0s
		int* counts = new int[10]();
	
		// Count occurences of each digit
		for (i = 1; i ≤ n; i++) {
			digit = (a[i] / exp) % 10;
			counts[digit]++;
		}
	
		int** bins = new int*[10];
		for (bin = 0; bin < 10; bin++)
			if (counts[bin] > 0) 
				bins[bin] = new int[counts[bin]];
		
		for (bin = 0; bin < 10; bin++) {
			index = 0; // Element counter for current bin
			for (i = 1; i ≤ n; i++) {
				if (((a[i] / exp) % 10) == bin)
					bins[bin][index++] = a[i];
			}
		}
		
		index = 1;
		for (bin = 0; bin < 10; bin++) {
		  for (i = 0; i < counts[bin]; i++)
		    a[index++] = bins[bin][i];
		  delete[] bins[bin];
		}
	
		// After each pass we deallocate the memory
		delete[] bins;
		delete[] counts;
	}
}

Implementing this approach is more challenging than the previous one, right?

Although theoretically the code should use less memory as it only allocates memory for each bin based on the digit counts, its execution time could possibly be slower due to the overhead of the repeated allocation and deallocation of memory.

Radix Sort Using Singly Linked List

This implementation of Radix Sort is particularly interesting because it operates directly on a singly linked list. Instead of using arrays, sorting process rely entirely on linked lists, making the algorithm more efficient in terms of memory.

We will use this singly linked list definition for the implementation of the algorithm:

typedef struct Node* Ref;
struct Node {
	int value;
	Node* next;
};

Next, we’ll define two arrays, b[10] and t[10], each with ten elements, to serve as bins for the Radix Sort. Each element in b points to the head of a linked list representing a bin for a specific digit, while each element in t points to the tail of the corresponding list.

Ref b[10] = { NULL };
Ref t[10] = { NULL };

The only difference in this algorithm is how it stores the numbers in the bins:

  • While traversing through the array in each pass, extract the LSD from a[i] and store the value of a[i] in a new node.
    • If the bin pointer b[i] is NULL, store the node address in both b[i] and t[i]. This is because the linked list of this bin only contains one element, thus this element servers as both the head and the tail of the linked list.
    • Otherwise, insert the node at the end of the linked list. This can be done by utilizing the pointer t[i].
  • After each pass, set the bin pointers to NULL (resetting the bins).
RadixSort(a[1..n], n) {
	int maxValue = a[1];
	for (i = 2; i ≤ n; i++) 
		if (a[i] > maxValue) maxValue = a[i];
	
	for (exp = 1; maxValue / exp > 0; exp *= 10) {
		// Resetting the bin pointers to NULL
		for (int i = 0; i < 10; i++) b[i] = t[i] = NULL;
		
		for (i = 1; i ≤ n; i++) {
		  digit = (a[i] / exp) % 10;
		  // Create a new node to store a[i]
		  Ref p = new Node{ a[i], NULL };
		  if (b[digit] == NULL) {
			  // Store the node address 
			  // in both b and t
			  b[digit] = t[digit] = p;
		  } else {
			  // Add the node to the end 
			  // of the linked list
			  t[digit]->next = p;
			  t[digit] = p;
		  }
		}
		
		index = 1;
		// Reconstruct the array
		for (bin = 0; bin < 10; bin++) {
		  Ref p = b[bin];
		  while (p != NULL) {
			  a[index++] = p->value;
			  p = p->next;
		  }
	  }
	}
}

Performance Analysis of Radix Sort

Radix Sort has a time complexity of Θ(dn)\Theta(dn) (or Θ(n)\Theta(n) when ndn \gg d), where dd is the number of digits of the largest element and nn is the number of the elements of the input. In fact, in algorithm analysis, it actually runs in linearithmic time!

Radix Sort uses two auxiliary arrays of about size nn, bins and counts. Therefore it has a space complexity of O(n)\mathcal{O}(n).

Counting Radix Sort

Basic Concept of Counting Radix Sort

The above implementations for Radix Sort share a similarity - they’re all using bins to sort the numbers by digits. Counting Radix Sort is an alternative implementation of Radix Sort that avoids using bins.

Once again, the algorithm needs to run dd passes to complete the sorting process. In the ithi^{th} pass, it uses an efficient sorting algorithm to sort the numbers on the ithi^{th} least significant digit!

Regarding the efficient sorting algorithm, what should we choose to achieve the best possible performance? If that algorithm is comparison-based, it cannot run faster than linearithmic time, making the time complexity of our Radix Sort O(dnlogn)\mathcal{O}(dn \log n)! This is not good!

As the name suggests, we’ll use Counting Sort, which runs in linear time.

Implement Counting Radix Sort

// Method to extract the Kth
// digit of an integer
digit(n, k) {
	while (--k && n != 0) n /= 10;
	return n % 10;
}

CountingSort(a[1..n], k) {
	for (i = 0; i < 10; i++) f[i] = 0;
	for (i = 1; i ≤ n; i++) f[digit(a[i], k)]++;
	for (i = 1; i ≤ 9; i++) f[i] += f[i - 1];
	for (i = n; i ≥ 1; i--) {
		j = digit(a[i], k);
		b[f[j]] = a[i];
		f[j]--;
	}
	a[1..n] = b[1..n]
}

CountingRadixSort(a[1..n], d) {
	for (k = 1; k ≤ d; k++) CountingSort(a, k);
}

Additional Resources

This is an amazing article that offer lots of valuable insights on the performance of Radix Sort. Take a look at it yourself!



👏 Thanks for reading!