Naming
sub_merge
is weird, as it doesn't match up at all with the meaning of the english word "submerge". I get its origin ("subroutine merge"), but why not simply call it merge
instead?
Algorithm
This seems like a straightforward top-down implementation of mergesort. However, there are some issues:
Stability
sub_merge
doesn't preserve the relative order of elements that compare equal. However, this can easily be fixed by changing the comparison operator in if(*left_iter < *right_iter)
from <
to <=
.
Allocations + copies
Since each sub_merge
call creates its own scratch space, there are \$2n\$ allocations in total. This could be replaced by creating one scratch buffer at the highest level instead.
A benefit of doing so is the reduction in the number of necessary copies. Currently, sub_merge
has to copy each element into the scratch space and then back into the original range. One of those copies could be skipped using a preallocated scratch buffer and alternating between it and the original range on different recursion levels, at the cost of one fixed copy when creating the scratch buffer.
Implementation
- Why not use
std::vector<std::iterator_traits<RandIter>::value_type>
instead of manually managing the memory via those std::unique_ptr
s?
Iterator categories
You are correct that a ForwardIterator
would technically work for merge sort. However, using those would mean that you'd need 1.5 extra passes over the range for each midpoint calculation (1 pass for the std::distance
call, and half a pass to find the mid
iterator).
This wouldn't raise the runtime complexity into a worse category, but there are more efficient sorting strategies for those cases that don't require that many passes (especially since those passes usually aren't that cheap in data structures that only have ForwardIterator
s or BidirectionalIterator
s).
That's why RandomAccessIterator
s are preferred for merge sort.
However, a lot of code would need to be changed for that to work, as ForwardIterator
s only need to support a small subset of the operations a RandomAccessIterator
has to provide (i.e. they only have operators *
, ->
, ++
, ==
and !=
, so no comparisons using <
or iteration using -
).
So, why won't InputIterator
s work? Because merge sort requires multiple passes over the input/output range, and InputIterator
s don't guarantee that they can do so.
Improved version
With some improvements, the implementation could look something like this:
#include <vector>
#include <type_traits>
template<typename Iter>
constexpr bool is_forward_iterator_v = std::is_base_of_v<std::forward_iterator_tag, typename std::iterator_traits<Iter>::iterator_category>;
template<typename ForwardIter>
ForwardIter middle_iterator(ForwardIter first, ForwardIter last) {
static_assert(is_forward_iterator_v<ForwardIter>, "middle_iterator requires at least a ForwardIterator to work properly");
auto dist = std::distance(first, last);
if(dist == 0) return first;
return std::next(first, (dist + 1) / 2);
}
template<typename InIter1, typename InIter2, typename OutIter>
OutIter merge(InIter1 left, InIter1 left_end, InIter2 right, InIter2 right_end, OutIter out) {
while(left != left_end && right != right_end) {
if(*left <= *right) {
*out++ = *left++;
} else {
*out++ = *right++;
}
}
if(left != left_end) return std::copy(left, left_end, out);
else return std::copy(right, right_end, out);
}
template<typename Iter, typename Iter2>
void merge_sort(Iter first, Iter last, Iter2 scratch, Iter2 scratch_end) {
const auto mid = middle_iterator(first, last);
if(mid != last) {
const auto scratch_mid = middle_iterator(scratch, scratch_end);
merge_sort(scratch, scratch_mid, first, mid);
merge_sort(scratch_mid, scratch_end, mid, last);
merge(scratch, scratch_mid, scratch_mid, scratch_end, first);
}
}
template<typename Iter>
void merge_sort(Iter first, Iter last) {
auto scratch_buffer = std::vector<typename std::iterator_traits<Iter>::value_type>(first, last);
merge_sort(first, last, scratch_buffer.begin(), scratch_buffer.end());
}
To Do:
Better assert iterator requirements
Add noexcept
specifications where possible
Maybe add support for move-only types?
std::merge
? Otherwise, I don't see the point in writing your own loop like that. \$\endgroup\$