2

The standard library sorting functions I'm familiar with in C and Java accept a user-defined comparator, which takes two elements a and b and returns a <=> b, their order with respect to each other.

The subsequent swap operation for unordered elements happens internally inside the library function. This API design makes it inconvenient to sort data stored in non-standard containers, for example, to sort key-value pairs which happen to be arranged in two separate arrays.

It seems to me that a standard library sort would be more flexible if it accepted a user-defined "compare-and-swap" function, which accepts two indices and sorts the two elements. (No relation to the atomic operation in multithreaded programming.)

In this case, does the sorting algorithm even need to know the result of the comparison? Or can the compare-and-swap be completely opaque, maybe not even bothering to compute an explicit comparison? For example:

compareAndSwap(i, j):
    a = array[i]
    b = array[j]
    array[i] = min(a, b)
    array[j] = max(a, b)

Do some sorting algorithms need to know the result of the comparison and others don't? If so, which common sorts are in which category? Is there anything deeper that they have in common?

Boann
  • 123
  • 3

3 Answers3

3

What you're describing is called sorting network.
Such compare&swap systems become optimal in massively parallel context, but single thread can make use of the feedback for more efficiency.

NooneAtAll3
  • 280
  • 1
  • 8
3

Given the constraint of just having a compare-and-swap callback, the sort algorithm has no real use for the array that it is given, other than its size, as it cannot compare elements "itself". All the array access will therefore only need to happen via the callback. As this callback does not have a return value (is opaque), the sequence of calls does not depend on the array contents, but only on its size. This sequence is what sorting networks can provide. If the callback has a constant time complexity, the time complexity of the sort function will not have a distinct worst or best case: all cases are the same.

The arguments given to the sorting function could be limited to n and the callback function.

Simplistic O(n²) algorithms

Sorting algorithms such as bubble sort, selection sort, and insertion sort can have a naive implementation with such opaque compare-and-swap callback. I say naive in the sense that they (obviously) cannot exit one of their loops early based on some comparison, as you would do in their standard definitions. Using Python syntax here:

def bubble_sort(n, compare_and_swap):
    for i in range(n, 1, -1):  # i is size of unsorted prefix
        for j in range(1, i):
            compare_and_swap(j - 1, j)

def selection_sort(n, compare_and_swap): for i in range(n - 1): # i is size of sorted prefix for j in range(i + 1, n): compare_and_swap(i, j)

def insertion_sort(n, compare_and_swap): for i in range(1, n): # i is size of sorted prefix for j in range(i, 0, -1): # backwards compare_and_swap(j - 1, j)

More efficient algorithms

On the other hand, several of the more efficient algorithms like merge sort, quick sort and heap sort are out as their logic heavily depends on the outcome of comparisons:

  • merge sort needs to know which of the two sorted partitions has the least value, as it is that partition that will "shorten" as it yields that least value. With the opaque callback, we can know the least value, but not which partition it came from.

  • quick sort needs to track the position of the pivot (as we can only call the callback with indices, not with values), but after calling the callback with this index we don't know where the pivot is after that call.

  • heap sort needs to decide which side to trickle down to based on comparison outcomes.

Sorting networks

As stated earlier, with these constraints the sequence of comparisons is only determined by the size of the input, not the array values. So for a given , that sequence would be a fixed one.

Sorting networks can be produced in O(log²) time, which is also the volume of index pairs they produce.

Here is Batcher odd–even mergesort implemented in Python for the given constraints:

def batcher_odd_even_sort(n, compare_and_swap):
    p = 1
    while p < n:
        k = p
        while k >= 1:
            for j in range(k % p, n - k, 2*k):
                for i in range(j, j + min(k, n - j - k)):
                    if i // (2*p) == (i + k) // (2*p):
                        compare_and_swap(i, i + k)
            k >>= 1
        p <<= 1

This would be one of the more efficient implementations of a sorting algorithm that only gets these arguments as input.

trincot
  • 193
  • 5
2

Some algorithms require something more than a compare-and-swap, and some don't (well actually only bubble sort can be written with only compare-and-swap among the most classic sorting algorithms).

Bubble sort

This one is a comparison sort that can be written using compare-and-swap:

Input: array t of length n
for i = 2 to n
    for j = 0 to n - i
        compare_and_swap(t, j, j + 1)

This is actually quite efficient for small arrays, because it is branchless.

Comb sort

This is a generalization of bubble sort, but more efficient. I think this is the best candidate to use a compare-and-swap.

Insertion sort

While it would be possible to create an insertion-like sort using compare-and-swap, it would lose the advantage of being linear in the case of sorting an already sorted array, because you'd need to do "insertion" to the beginning of the array – unless, of course, you allow a test if array[i] < array[j] before the call to compare-and-swap, but that is not the idea behind compare-and-swap.

Quicksort

This one is weird, because of the partition operation, where each element is compared to the pivot. Using Lomuto scheme, you need to make a swap only if the element is smaller than the pivot. Using Hoare scheme, you modify indices using comparisons to the pivot, then do a swap without comparing the two elements. In both cases, a compare-and-swap cannot work

Mergesort

Again, compare-and-swap cannot be used, because during the merge operation, you compare two elements, and place one of them in a third position, without swapping the other.

Heapsort

While compare-and-swap can be used during an insertion in a heap (you'd need to make the bottom-up operation up to the root, but that's still log-time in worst case), you'd need to compare elements without swapping during the extraction in the heap: when trying to make the new root go down the tree, you need to compare its children and make a compare-and-swap with the smallest of the two.

Nathaniel
  • 18,309
  • 2
  • 30
  • 58