tl;dr: Is it is possible to implement quicksort on a doubly linked list efficiently? My understanding before thinking about it was, no, its not.
The other day I had an opportunity to consider the iterator requirements for the basic sorting algorithms. The basic O(N²) ones are fairly straightforward.
Quicksort
The introsort_loop in std::sort (as in the gnu standard library/ hp(1994) / silicon graphics(1996) ) requires it to be random_access.
__introsort_loop(_RandomAccessIterator __first,
_RandomAccessIterator __last,
_Size __depth_limit, _Compare __comp)
As I have come to expect.
Now upon closer inspection I cant find the real reason to require this for quicksort. The only thing that explicitly requires random_access_iterators is the std::__median
call that requires the middle element to be calculated. The regular, vanilla quicksort does not calculate the median.
The partitioning consists of a check
if (!(__first < __last))
return __first;
Not really a useful check for bidirectionals. However one should be able to replace this with a check in the previous partitioning travel (from left to right/ right to left) with a simple condition of
if ( __first == __last ) this_partitioning_is_done = true;
Is it possible to implement quicksort fairly efficiently using only bidirectional iterators? The recursive depth can still be guarded.
NB. I have yet not attempted an actual implementation.
tl;dr: Yes
As you say, the problem is to find the pivot element, which is the element in the middle, finding this with random access takes O(1), finding it with bidirectional iterators takes O(n) (n/2 operations, to be precise). However, in each step you have to create to sub containers, the left and the right containing smaller and bigger numbers respectively. This is where the main work of quick sort takes place, right?
Now, when building the sub containers (for the recursion step) my approach would be to create an iterator h
pointing to their respective front element. Now whenever you choose a next element to go to the sub container, simply advance h
every second time. This will have h
point to the pivot element once you are ready to descend to the new recursion step.
You only have to find the first pivot which does not matter really, because O(n log n + n/2) = O(n log n).
Actually this is just a runtime optimisation, but has no impact on the complexity, because whether you iterate over the list once (to put each value in the respective sub container) or twice (to find the pivot and then put each value in the respective sub container) is all the same: O(2n) = O(n).
It's simply a question of execution time (not complexity).