If comparing the real-world difference between say O(log
N) and O(
N) algorithms is interesting, implement a radix sort (which is O(
N) but with a rather high constant factor, and requires rather large amount of extra memory), and compare it to other sort algorithms. To make it a bit more realistic, use key-value pairs as elements to be sorted, say
double and a pointer on 64-bit architectures.
For very small
N,
insertion sort is the best choice in practice, even though it has O(
N2) time complexity. Most common sorting algorithm is probably
quicksort, even though it has O(
N2) rare worst-case and O(
N log
N) expected time complexity. In practice, radix sort (having O(
N) time complexity), temporarily converting the IEEE-754
double keys so that all finite values sort according to their unsigned integer value by flipping either the sign bit or all other bits, will be fastest for large enough
N. However, the exact point depends heavily on the radix size used (determining the size of the temporary arrays needed), and how well that matches the hardware architecture used; and even then, because it is cache-intensive, it can negatively impact surrounding code as it evicts a lot of data from the caches. The last time I checked this thoroughly, the changeover was
N roughly a couple of million, but this was a decade ago on x86-64.
Brucehoults
post and example code that sorts input lines by reading the data into a (scapegoat) tree, is also an excellent example of how the chosen algorithmic approach affects real-world performance.
In mid-nineties, a C course I took required one to implement a simplified 'sort' command in an Unix environment (Solaris, if I recall correctly). I used a very similar approach, reading lines into a binary search tree, implementing the entire program in about 300 lines, a third of which were comments.
This was the era of spinning disk hard drives, and I/O speeds were much slower than they are today with SSD drives and multi-gigabyte RAM sizes (making many workloads completely cacheable in RAM). Reading the input was the clear bottleneck. If you first read the input to memory, then sort them, then you wait for all I/O to complete before you start computation (sorting). If you read the lines into a sorting data structure like a tree, you essentially do the computation while I/O is still underway; and your sort is basically complete, when I/O completes. This means that the read-then-sort takes much longer, using real-world wall-clock measurement, than reading the lines into a tree; even if the read-to-tree uses somewhat more CPU time.
(Today, if the input is in a file, it is likely cached, and the I/O is essentially free. You need to use a slow pipe (a network connection or similar), or a slow(ish) generator, to see the difference.)
Just like comparing algorithms based on their
big-O notation, even terms like
fast must be carefully qualified, to convey real-world applicable information. When we say "fast", we can refer to CPU time, or wall clock time (or, if we are stupid, our gut feeling about the algorithm).
For us humans, interactive tasks' speed should be measured in real-world wall clock time, but batch and background jobs in CPU time. Similarly, asymptotic time behaviour is only really indicative of the big-
N behaviour, and does not tell us anything about the constant factors.
Even comparing algorithm behaviour via real-world testing must be categorized into at least two sets:
microbenchmarks that ignore everything except the task at hand, in an effort to compare apples to apples; and true benchmarks, where tasks similar to or simulating real-world computing tasks are compared to each other.
This means that to be able to sort in a
neat way, one should have several different sorting algorithms and approaches in their toolbox ready to be used. The neat part, then, is picking the correct ones to match the use cases and user preferences for the kind of tasks.