Edit: A follow up question can be found here.
My first try at mergesort, without resorting to any references:
#include <iostream>
#include <vector>
std::vector<int> Merge(std::vector<int>, std::vector<int>);
std::vector<int> MergeSort(std::vector<int>);
int main()
{
// No need to review this, it was just for testing purposes
std::vector<int> a;
for (int i = 0; i < 100; i++) {
a.push_back(6000.0 * rand() / RAND_MAX);
}
for (int i = 0; i < a.size(); i++) {
std::cout << a[i] << " ";
}
std::cout << "\n";
a = MergeSort(a);
for (int i = 0; i < a.size(); i++) {
std::cout << a[i] << " ";
}
std::cout << "\n";
return 0;
}
std::vector<int> Merge(std::vector<int> arr1, std::vector<int> arr2) {
int size1 = arr1.size();
int size2 = arr2.size();
std::vector<int> res(size1 + size2, 0);
int i1 = 0;
int i2 = 0;
while (i1 < size1 && i2 < size2) {
if (arr1[i1] < arr2[i2]) {
res[i1 + i2] = arr1[i1];
i1++;
}
else {
res[i1 + i2] = arr2[i2];
i2++;
}
}
while (i1 < size1) {
res[i1 + i2] = arr1[i1];
i1++;
}
while (i2 < size2) {
res[i1 + i2] = arr2[i2];
i2++;
}
return res;
}
std::vector<int> MergeSort(std::vector<int> arr) {
if (arr.size() < 2) {
return arr;
}
else if (arr.size() == 2) {
// If array is of size 2, return it sorted
if (arr[0] > arr[1]) {
arr[1] += arr[0];
arr[0] = arr[1] - arr[0];
arr[1] -= arr[0];
}
return arr;
}
else {
// Recursively split in halves until all vectors are size 1 or 2, then merge
return Merge(
MergeSort(std::vector<int>(arr.begin(), arr.begin() + arr.size() / 2)),
MergeSort(std::vector<int>(arr.begin() + arr.size() / 2, arr.end()))
);
}
}
A few questions:
- Have I implemented a merge sort correctly? Or is it some other sort?
- How can I make it work for multiple data types rather than just
int
? - I imagine it's not very efficient (I don't know much about
std::vector<T>.begin
orstd::vector<T>.end
), how could I make it more efficient?
4 Answers 4
Your big mistake is not using iterators.
As a result you pass your vector
s by value, which causes the vectors to be copied a lot.
std::vector<int> MergeSort(std::vector<int> arr)
^^^ A copy of the array is made when you call this function.
std::vector<int> Merge(std::vector<int> arr1, std::vector<int> arr2)
^^^^ Copy ^^^^ Copy
Now, you could mitigate this by passing the values by reference. But I think a better solution is to pass iterators into the container. That way you also allow for other container types.
Luckily for you, a return
is a move, not a copy - so that is not as big a problem as a copy.
template<typename I>
void MergeSort(I begin, I end)
template<typename I,
void Merge(I src1Begin, I src1End, I src2Begin, I src2End, I dstBegin);
-
\$\begingroup\$ See codereview.stackexchange.com/questions/169048/… for follow up \$\endgroup\$user122352– user1223522017年07月12日 11:56:45 +00:00Commented Jul 12, 2017 at 11:56
Yes, it's a mergesort, switching to something else for small ranges is a common refinement.
Be aware your implementation is not stable, though it doesn't matter for
int
on your machine.
You can easily address that at no extra cost.Change:
if (arr1[i1] < arr2[i2])
intoif (arr1[i1] <= arr2[i2])
Use templates, for the inputs type and an optional comparison-predicate.
Though you should first change it to work with iterators, modifying the passed range, to increase flexibility and reduce the number of instantiations needed.The most important thing you can do to make it more efficient is reducing the number of allocations and deallocations. It should suffice to allocate just one auxiliary array over the runtime of the whole algorithm.
Use
using std::swap; swap(a, b);
if you need to swap two objects, don't roll your own. Thus your code will be:- More idiomatic and readable.
- Possibly more efficient.
- Avoiding common pitfalls. Bit-hacks often fail on self-swap (which is not an issue for you) or invoke undefined behavior if carelessly implemented (signed integer-overflow is UB).
Don't bother with an else-branch if the conditional block ends with a return/break/... . Best to minimize nesting.
Use move-semantics (C++11) to avoid costly copies.
If you want to stream a single character, don't use a length-1-string but a character. It's more efficient.
return 0;
is implicit formain()
.As an aside, I really don't like single-statements-blocks. Others differ.
You know that
rand()
is a really poor RNG? Look into<random>
(C++11) for better choices. Andrand()
should be seeded (once and only once) withsrand()
unless you need repeatability. So, your use is actually defensible, though you should call it out in a comment, maybe with// srand(1);
.Many of your
while
-loops should befor
-loops. Using the proper loop makes compreheending the code easier.
-
\$\begingroup\$ Have you measured the performance difference between streaming a character and streaming a one-character string? It's probably hard to justify your recommendation solely on performance grounds (even though I do support it on the grounds of clarity, and (at least on this keyboard) reduced typing). \$\endgroup\$Toby Speight– Toby Speight2017年07月12日 09:33:15 +00:00Commented Jul 12, 2017 at 9:33
-
\$\begingroup\$ @TobySpeight: If there are two choices, which differ in nothing but a marginal amount of efficiency, efficiency is the argument to go for. It's also more comfortable to use the more efficient one on your keyboard? Well, that's also an argument. \$\endgroup\$Deduplicator– Deduplicator2017年07月12日 09:52:15 +00:00Commented Jul 12, 2017 at 9:52
Some optimizations:
- You are calling
arr.size()
inMergeSort(..)
many times, you can store it in a local variable.
-
\$\begingroup\$ Thank you for correcting me, it doesn't make sense for built-in types \$\endgroup\$Bheema Reddy– Bheema Reddy2017年07月12日 11:23:26 +00:00Commented Jul 12, 2017 at 11:23
-
\$\begingroup\$ See codereview.stackexchange.com/questions/169048/… for follow up \$\endgroup\$user122352– user1223522017年07月12日 11:57:16 +00:00Commented Jul 12, 2017 at 11:57
It's a merge sort al right.
templates,
template<T> std::vector<T> Merge(std::vector<T> arr1, std::vector<T> arr2) template<T> std::vector<T> MergeSort(std::vector<T> arr)
and then replace every usage of
int
that isn't for size or index withT
Having said that you can also create a version that takes 2 iterators the begin and end)
You can make it more efficient by not allocating as much. Every time you fill a vector it needs to allocate a buffer to put the values in. Allocation is not free.
Instead you can provide the target buffer. However that means having to keep track of which way to copy the data and tends to get complicated.
I prefer the bottom up merge sort where separate the input into chunks of length n
then merge 2 chunks pairwise then multiply n
by 2 and repeat.
-
\$\begingroup\$ See codereview.stackexchange.com/questions/169048/… for follow up \$\endgroup\$user122352– user1223522017年07月12日 11:56:58 +00:00Commented Jul 12, 2017 at 11:56
x ^= y; y ^= x; x ^= y;
\$\endgroup\$