Hello everyone! This is part 7.0 of my Sorting series.
So, anyway, here we are at 7.0 – and I’m wondering about how long I can drag this whole thing out…
Merge Sort: The Algorithm Under the Hood
Merge Sort is another divide and conquer algorithm, much like Quick Sort.
One big difference from Quick Sort is that Merge Sort doesn’t pick any special pivot. Rather, Merge Sort splits the array-to-sort arr
right down the middle. Then, it uses recursion to Merge Sort the left = arr[0..k]
and right = arr[k+1..n-1]
.
It 'll continue splitting each piece of the array-to-sort arr
into smaller and smaller pieces, until the pieces are of size two or less.
That’s the wonder of divide and conquer: take a daunting problem and split it into a lot of smaller, more manageable tasks.
So, we get to very small arrays that essentially follow this pattern: arr[a..b]
, where 0 <= a <= n - 2
, 1 <= b <= n - 1
, and b - a <= 1
, meaning that the array piece arr[a..b]
has a length of two or less.
Okay, so now we have a bunch of arrays that are two-or-less elements in length. Two or less elements can mean one of three things:
- There are no elements in the array, so we return it move on to the next piece.
- The array has one element, so we return that and move on.
- The array has two elements. Here, we swap the elements if they’re out of order, and then return it.
Once we have sorted the two or lesses, we need to MERGE each pair of two or lesses into arrays of size four (or less).
Take a look at the picture above again. Do you understand what I mean by merging? Did you bring your head right up to the screen to examine the second line closely? Regardless of your answer, look at it again and imagine merging the -two_-size arrays into the four-size arrays above them.
If the first element in the left array is smaller than the first of the right array, put the left’s first element in the first spot of the four array, now look at the second element of the left array, but keep looking at the right. Put the smaller one in. Now you’re looking at the last element of each two array. Put the smaller one in, that’s 5
on the left side of the diagram. Now you have no more values to merge in that left two array, so tack on the rest of that right two array if it still has values to be merged.
You keep merging the pieces until you have the entire array. [Take a look at this video for a visual]https://youtu.be/ZRPoEKHXTJg).
Note that it does the left side first before doing anything for the right side. This is because of how recursion is used. All of the mergesort(left)
s will be executed before it ever gets to a mergesort(right)
on the same level.
Time Complexity
Merge Sort has an O(n * log n )
worst-and best-case time complexity.
Ruby Implementation
class Array
def mergesort
return self if length <= 1
if length == 2 # we swap if the length is two and the "two array" is unsorted still
if (self[0] > self[1])
self[0],self[1] = self[1],self[0]
end
return self
end
left = self[0...self.length/2].mergesort
right = self[self.length/2...self.length].mergesort
arr = []
while(!left.empty? && !right.empty?)
if left.first < right.first
arr << left.first
left.shift #i remove the first element from left after it gets merged
# it's so i can keep comparing using left/right.first
else
arr << right.first
right.shift # remove first element from right after merging it
end
end
if !left.empty?
arr += left # if the left array is not yet fully merged, tack on the remaining values
elsif !right.empty?
arr += right # if the right array is not yet fully merged, tack on remaining values
end
return arr
end
end
The first few lines of mergesort()
are exit conditions, since we use recursion in mergesort()
.
In this implementation, I extended Ruby’s Array
class to have a mergesort()
method. That’s why I call it with dot-notation.
If the array we are mergesort
ing has more than two elements, we call mergesort
on its left and right halves right away. No merging takes place until the array-to-sort self
gets cut down into little two or less pieces.
I make an empty array arr
to hold the result. Then, I say while left and right both have elements, merge. The merging takes place in that while
loop. I do it by comparing the smallest element of left
to that of right
. The smaller element of the two gets “merged” by appending it to arr
. Then, we delete that element from left
or right
(where it came from) by calling Array#shift
.
After either left
or right
is emptied out, we append the non-empty array. Note: only one array will have remaining elements at this point, and it’ll be guaranteed that the result is sorted.
We then return the new array, arr
.
C++ Version
void mergesort(vector<char>& vec) {
int n = vec.size();
// recursion terminator
if (n <= 1) return;
else if (n == 2) {
if (vec[0] > vec[1]) {
char tmp = vec[0];
vec[0] = vec[1];
vec[1] = tmp;
}
return;
}
vector<char> left(vec.begin(), vec.begin() + n / 2);
vector<char> right(vec.begin() + n / 2, vec.end());
vec.erase(vec.begin(), vec.end()); // because we have copied the values to the partitions (left and right)
mergesort(left); mergesort(right);
int itr = 0;
while(!left.empty() && !right.empty() && itr < n) {
if (left[0] < right[0]) {
vec.push_back(left.front());
left.erase(left.begin());
} else {
vec.push_back(right.front());
right.erase(right.begin());
}
itr++;
}
if (!left.empty()) {
vec.insert(vec.end(), left.begin(), left.end());
} else if (!right.empty()) {
vec.insert(vec.end(), right.begin(), right.end());
}
}
We do fundamentally the same thing, except that I didn’t extend vector
as I extended Array
in the Ruby version. It’s just a regular old function. Use the annotations I have provided above; otherwise, I’ve done all I can to explain Merge Sort.
Conclusion
That’s it for this article. The implementations above are great on time, but a little heavier on memory than I’d like, so Merge Sort may make a return to NB in the future, a little wiser, a little more optimized.
Again, stay tuned for the optimization(s) of Quick Sort to come.
Comment below if you have a special sorting algorithm you’d like included in my series.
Stay sorted,
oaktree