Following the review of my old merge sort implementation here, it appears that the biggest improvement to make was to use iterators instead of copying the vector
s. Having never used iterators before, I'd like a review of the new code following that review:
#include <iostream>
#include <vector>
#include <algorithm>
template<typename T>
void Merge(T, T, T, T);
template<typename T>
void MergeSort(T, T);
int main()
{
// As last time, this is just test code and does not need to be reviewed, but
// is included for completeness
std::vector<int> a;
for (int i = 0; i < 10; i++) {
a.push_back(10.0 * rand() / RAND_MAX);
}
for (int i = 0; i < a.size(); i++) {
std::cout << a[i] << " ";
}
std::cout << "\n";
// Note this swap is now in place, previously it returned a new vector
MergeSort(a.begin(), a.end());
for (int i = 0; i < a.size(); i++) {
std::cout << a[i] << " ";
}
std::cout << "\n";
}
template<typename T>
void Merge(T arr1Begin, T arr1End, T arr2Begin, T arr2End) {
T i1 = arr1Begin;
T i2 = arr2Begin;
int i = 0;
std::vector<int> res(arr2End - arr1Begin, 0);
while (i1 < arr1End && i2 < arr2End) {
if (*(i1) < *(i2)) {
res[i++] = *(i1);
std::advance(i1, 1);
}
else {
res[i++] = *(i2);
std::advance(i2, 1);
}
}
while (i1 < arr1End) {
res[i++] = *(i1);
std::advance(i1, 1);
}
while (i2 < arr2End) {
res[i++] = *(i2);
std::advance(i2, 1);
}
std::copy(res.begin(), res.end(), arr1Begin);
}
template<typename T>
void MergeSort(T arrBegin, T arrEnd) {
int size = arrEnd - arrBegin;
if (size < 2) {
return;
}
if (size == 2) {
if (*arrBegin > *(arrEnd - 1)) {
std::swap(*arrBegin, *(arrEnd - 1));
}
return;
}
MergeSort(arrBegin, arrBegin + size / 2);
MergeSort(arrBegin + size / 2, arrEnd);
return Merge(
arrBegin, arrBegin + size / 2,
arrBegin + size / 2, arrEnd
);
}
My questions this time:
- I'm dereferencing a lot of iterators in order to swap and assign values, is there a better way to be doing this?
- Is this implementation better than my last implementation? I'm looking for answers that think about stability, efficiency in terms of speed/memory, etc. (Possibly an obvious question)
- Also, I know nothing about templates, is there anything more to it than adding the line
template<typename T>
then line before the function and then usingT
within it? - I can't figure out how to get rid of
std::vector<int> res(arr2End - arr1Begin, 0);
. Obviously this goes against the templating, how should I store the results while I'm doing the merge? (Edit: since looking back at the previous accepted answer, I figure I should create a new vector outside the original call toMergeSort
(insidemain
) and pass the.begin()
of that array as the destination, rather than defining a new array inside eachMerge()
call and usingstd::copy
. Is this correct?)
Edit: Thanks to @ratchetfreak for pointing out a bug whereby I was overwriting elements of arr1
without storing the overwritten element elsewhere beforehand (essentially losing it).
3 Answers 3
Overview
Its an improvement over the first version, but still some work to go. I think most of my comments below are relatively minor and the worst ones come from assuming the type being sorted is an int
.
The other major flaw is still efficiency (though it has improved considerably). First prefer move to copy. But you also allocate a res
vector on each recursive call down MergeSortMerge()
. That can be made more efficient by only doing it once at the top level:
mergeSort(begin, end)
Allocate temp array here
mergeSortDo(begin, end, tBegin); // you can
mergeSortDo(beg, end, tBeg)
Your Merge in here just pass sections of the temp forward.
mergeSortDo(beg, mid, tBeg);
mergeSortDo(mid, end, tMid);
mergeSortMerge(begin, mid, end, tBeg)
mergeSortMerge(beg, mid, end, tBeg)
Your MergeSortMereHere (with already allocated space.
Code Review
Please specify parameters.
Please provide parameter names even in declarations. It helps understanding what your code does and is part of the "Self Documenting Code" principle.
template<typename T>
void Merge(T, T, T, T);
template<typename T>
void MergeSort(T, T);
Range Based For
If you are looping over a whole container use the range based for:
for (int i = 0; i < a.size(); i++) {
std::cout << a[i] << " ";
}
// This is easier to write:
for (auto const& value: a) {
std::cout << value << " ";
}
Not all iterators are Random Accesses
There are several categories of iterators.
- Input
- Forward: (Is a Input with a few more properties)
- BiDirectional: (Is a Forward with a few more properties)
- RandomAccess: (Is a BiDirectional with a few more properties)
- Contiguous: (Is a RandomAccess with a few more properties)
Now you should be able to perform a sort on any iterator that is a BiDirectional iterator. But BiDirectional iterators don't support all the features of RandomAccess
iterator.
The trouble is here:
int size = arrEnd - arrBegin;
Subtracting two iterators requires that they are at least Random Access
iterators. But the standard comes to your help here:
int size = std::distance(arrBegin, arrEnd);
This returns the distance between two iterators (from the same sequence).
Standard Swap usage
For this to work:
std::swap(*arrBegin, *(arrEnd - 1));
The two object need to be a type that the standard understands. So currently you can only swap types that are in the namespace std
. std::swap will fallback to using a temporary and assignment but this is not always as efficient as possible and lots of types define their own swap to make it much more efficient (but that will not happen here). What you want to do is:
using std::swap;
swap(*arrBegin, *(arrEnd - 1));
This says bring std::swap
into the current scope so we can use it automatically as a backup. Now when you call swap()
it does Koenig lookup
(or ADL). This means it checks the types of the arguments and sees what namespace they are in; it then looks for a swap()
function in that namespace (because if you define a type in a namespace you also define the swap function in the same namespace). If it finds this type specific swap()
it will use that but if it does not find it then it will fallback to using swap()
in the current context (in this case std::swap()).
There is a shortcut for the above.
std::iter_swap(arrBegin, arrEnd - 1);
Do you really want a return
here?
return Merge(
arrBegin, arrBegin + size / 2,
arrBegin + size / 2, arrEnd
);
Don't assume iterators point integer containers.
Here you assume the type is int
. Iterators can point at any type. The other thing you assume is that the value can be constructed with a defautl value (not true of all types).
std::vector<int> res(arr2End - arr1Begin, 0);
The type the iterator points too is: std::iterator_traits<T>::value_type
.
In this better to create an empty array and reserve the size you want (so it does not need to realloc a bigger chunk of memory)
using ValueType = std::iterator_traits<T>::value_type;
std::vector<ValueType> res;
res.reserve(arr2End - arr1Begin); // You will now need to use `emplace_back()`
Stable Sort
A very nice property of a sort is when it is stable (its a maths term go look it up).
Change this line:
if (*(i1) < *(i2)) {
// Into
if (*(i1) <= *(i2)) {
// Basically if the items are equal you will choose from the
// left sub container rather than right. This way values that
// are equal retain their relative order to each other.
Prefer move to copy.
This performs a copy.
res[i++] = *(i1);
When the type is not an integer (say you are sorting strings). This is not a very efficient way to do it. In C++11 we introduced move semantics which allows an object to be moved (in the case of a string that means moving its pointers and not the string). As you can imagine this makes it much more efficient than a copy.
res[i++] = std::move(*i1);
// If you have updated your array as above then you would use.
res.emplace_back(std::move(*i1));
Prefer Algorithms to loops.
while (i1 < arr1End) {
res[i++] = *(i1);
std::advance(i1, 1);
}
This is basically a copy from one container to another. There is an algorithm for that.
i1 = std::copy(i1, arr1End, &res[i]);
But we don't want to copy all the memebers. We want to move them. And there is an algorithm for that:
i1 = std::move(i1, arr1End, &res[i]);
I think I mentioned we want to move rather than copy.
std::copy(res.begin(), res.end(), arr1Begin);
-
\$\begingroup\$ Shouldn't the third
mergeSortDo
in the first outline be replaced withmergeSortMerge
...? \$\endgroup\$CiaPan– CiaPan2017年07月12日 20:32:22 +00:00Commented Jul 12, 2017 at 20:32 -
\$\begingroup\$ @CiaPan Fixed. Shows how much I rely on the compiler. \$\endgroup\$Loki Astari– Loki Astari2017年07月12日 20:59:24 +00:00Commented Jul 12, 2017 at 20:59
Well, the first point is that you are still allocating memory for every single merge. That's expensive.
Second, you are always copying all elements. That's inefficient too.
Third, prefer to move instead of copying. Some types are move-only, others just far more efficiently moved.
Try something along these lines:
#include <iterator>
#include <algorithm>
#include <utility>
#include <memory>
template<class It>
void MergeSortMerge(It begin, It mid,
typename std::iterator_traits<It>::value_type* aux)
{
while(begin != mid && !(*mid < *begin))
++begin;
if(begin != mid)
std::move(begin, mid, aux);
for(; begin != mid; ++begin) {
if(*mid < *aux) {
*begin = std::move(*mid);
++mid;
} else {
*begin = std::move(*aux);
++aux;
}
}
}
template<class It>
void MergeSortImpl(It begin, It end,
typename std::iterator_traits<It>::value_type* aux)
{
const auto size = std::distance(begin, end);
if(size < 2)
return;
const auto mid = std::next(begin, size / 2);
MergeSortImpl(begin, mid, aux);
MergeSortImpl(mid, end, aux);
MergeSortMerge(begin, mid, aux);
}
template<class It>
void MergeSort(It begin, It end) {
const auto size = std::distance(begin, end);
if(size < 2)
return;
using T = typename std::iterator_traits<It>::value_type;
std::unique_ptr<T[]> aux = new T[size / 2];
MergeSortImpl(begin, end, aux.get());
}
The one style-comment I have is that streaming a single character as a char
is marginally more efficient than as a length-1-string.
You only use arrEnd - 1
in two places, where you could use arrBegin + 1
instead. Changing that allows the use of mutable forward-iterators, whereas before you required mutable bidirectional iterators.
Adressing your extra questions:
- It's better to move than copy where you can.
- Your algorithm is still not stable, change
if (*(i1) < *(i2))
toif (!(*i2 < *i1))
.
You have fewer allocations than before, one for every merge instead of one for every recursive call to sort. That's still far more than the single one needed though. That's the beginning. Next, you have to really think about how to most generically use the type / expressions affected, and to minimize the interface used.
std::iterator_traits
,std::distance
andstd::advance
are your friend there, and it wouldn't be amiss to only use<
and==
out of all relational operators, as those are the ones used by most generic code (especially all of the standard library).- Nearly. Use an extra-level of indirection,
MergeSort
is the public API doing the allocating and calling the recursiveMergeSortImpl
which callsMergeSortMerge
.
-
\$\begingroup\$ what's the difference between using
std::distance(begin, end)
andend - begin
\$\endgroup\$user122352– user1223522017年07月12日 13:28:06 +00:00Commented Jul 12, 2017 at 13:28 -
\$\begingroup\$ The former works for all forward-iterators, the latter only has to work for random-access-iterators. That reminded me that I use Iterator + count a few times... fixed. \$\endgroup\$Deduplicator– Deduplicator2017年07月12日 13:43:14 +00:00Commented Jul 12, 2017 at 13:43
-
1\$\begingroup\$ Instead of
auto mid = begin; std::advance(mid, size / 2);
you can useauto mid = std::next(mid, size / 2);
:) \$\endgroup\$Rakete1111– Rakete11112017年07月12日 16:54:01 +00:00Commented Jul 12, 2017 at 16:54 -
1\$\begingroup\$ @Rakete1111: Yes, that way I can make it
const
and save a line. \$\endgroup\$Deduplicator– Deduplicator2017年07月12日 17:01:54 +00:00Commented Jul 12, 2017 at 17:01
I would make it clearer that you are looking at a single array that is split in the middle in the parameters.
Instead of pre-filling the result vector you can reserve it and push back the values. The value type of the iterator can be discovered by using std::iterator_traits<T>::value_type
. The reserve is not strictly necessary but it will avoid reallocations that aren't needed.
template<typename T>
void Merge(T begin, T mid, T end) {
T arr1Begin = begin;
T arr1End = mid;
T arr2Begin = mid;
T arr2End = end;
T i1 = arr1Begin;
T i2 = arr2Begin;
int i = 0;
std::vector<std::iterator_traits<T>::value_type> res;
res.reserve(arr2End - arr1Begin);
while (i1 < arr1End && i2 < arr2End) {
if (*(i1) < *(i2)) {
res.push_back(*(i1));
std::advance(i1, 1);
}
else {
rres.push_back(*(i2));
std::advance(i2, 1);
}
}
std::copy(i1 , arr1End, arr1Begin+res.size());
std::copy(res.begin(), res.end(), arr1Begin);
}
The final while loops can be replaced by either leaving the rest of i2 in place or copying the rest of i1 to the end of the array.
A further improvement would be to use moves instead of copies.