It turns out that within each round of radix sort, we can take advantage of parallelism. We need to reorder the keys (in a stable manner) according to the relevant bit. The simplest to do this in parallel would be as follows:
/* perform one round of radix-sort on the given input
* sequence, returning a new sequence reordered according
* to the kth bit */
function radixRound(input, k):
front = filter(input, kth bit is zero)
back = filter(input, kth bit is one)
return concatenate(front, back)
In this approach, on each round, we "filter" the sequence twice. The first filter selects the subsequence of elements whose kth bit is zero. The second filter similarly selects the elements whose kth bit is one. We then complete the round by returning the concatenation of these two sequences. Here's a trace of your small example:
round 0:
input = [101, 110, 011, 001, 111]
front = [110]
back = [101, 011, 001, 111]
round 1:
input = [110, 101, 011, 001, 111]
front = [101, 001]
back = [110, 011, 111]
round 2:
input = [101, 001, 110, 011, 111]
front = [001, 011]
back = [101, 110, 111]
round 3:
input = [001, 011, 101, 110, 111]
(done)
Now all we have to do is explain how to do filter
and concatenate
in parallel. Assuming sequences are just implemented as arrays, concatenate
is pretty simple. All we have to do is allocate an output array of the appropriate size and then, in parallel, write all the elements out to this new array:
function concatenate(a, b):
n = length(a)
m = length(b)
result = allocate(n+m)
for i from 0 to n+m do in parallel:
if i < n:
result[i] = a[i]
else:
result[i] = b[i-n]
return result
Implementing filter
in parallel is not so immediately obvious. The basic idea which is typically used in practice is to do a parallel prefix sum to count, at each position, how many elements satisfy the predicate preceding that position. This gives you the index of each surviving element of the output, so that in parallel you can write them into an output array.
With this implementation, you can do each round of radix sort in $O(n)$ work and $O(\log n)$ parallel time. This gives you a work-efficient radix sort with total work $O(wn)$ and parallel time $O(w \log(n))$, assuming a bit width of $w$. In practice, you would want to optimize this implementation to not allocate too many intermediate arrays, and to hopefully only make a single "pass" over the input on each round.