13.10. Mergesort¶
In Section 13.7, we saw a simple sorting algorithm that turns out not to be very efficient. In order to sort \(n\) items, it has to traverse the vector \(n\) times, and each traversal takes an amount of time that is proportional to \(n\). The total time, therefore, is proportional to \(n^2\).
In this section I will sketch a more efficient algorithm called mergesort. To sort \(n\) items, mergesort takes time proportional to \(n \log n\). That may not seem impressive, but as \(n\) gets big, the difference between \(n^2\) and \(n \log n\) can be enormous. Try out a few values of \(n\) and see.
The basic idea behind mergesort is this: if you have two subdecks, each of which has been sorted, it is easy (and fast) to merge them into a single, sorted deck. Try this out with a deck of cards:
Form two subdecks with about 10 cards each and sort them so that when they are face up the lowest cards are on top. Place both decks face up in front of you.
Compare the top card from each deck and choose the lower one. Flip it over and add it to the merged deck.
Repeat step two until one of the decks is empty. Then take the remaining cards and add them to the merged deck.
The result should be a single sorted deck. Here’s what this looks like In pseudocode:
card_deck merge (const card_deck& d1, const card_deck& d2) {
// create a new deck big enough for all the cards
card_deck result (d1.cards.size() + d2.cards.size());
// use the index i to keep track of where we are in
// the first deck, and the index j for the second deck
std::size_t i = 0;
std::size_t j = 0;
// the index k traverses the result deck
for (std::size_t k = 0; k<result.cards.size(); k++) {
// if d1 is empty, d2 wins; if d2 is empty, d1 wins;
// otherwise, compare the two cards
// add the winner to the new deck
}
return result;
}
I chose to make merge a nonmember function because the two arguments
are symmetric.
The best way to test merge is to build and shuffle a deck, use
subdeck to form two (small) hands, and then use the sort routine from
the previous chapter to sort the two halves. Then you can pass the two
halves to merge to see if it works.
If you can get that working, try a simple implementation of
merge_sort:
card_deck card_deck::merge_sort () const {
// find the midpoint of the deck
// divide the deck into two subdecks
// sort the subdecks using sort
// merge the two halves and return the result
}
Notice that the current object is declared const because
merge_sort does not modify it. Instead, it creates and returns a new
card_deck object.
If you get that version working, the real fun begins! The magical thing
about mergesort is that it is recursive. At the point where you sort the
subdecks, why should you invoke the old, slow version of sort? Why
not invoke the spiffy new merge_sort you are in the process of
writing?
Not only is that a good idea, it is necessary in order to achieve the
performance advantage I promised. In order to make it work, though, you
have to add a base case so that it doesn’t recurse forever. A simple
base case is a subdeck with 0 or 1 cards. If mergesort receives such
a small subdeck, it can return it unmodified, since it is already
sorted.
The recursive version of mergesort should look something like this:
card_deck card_deck::merge_sort (card_deck deck) const {
// if the deck is 0 or 1 cards, return it
// find the midpoint of the deck
// divide the deck into two subdecks
// sort the subdecks using mergesort
// merge the two halves and return the result
}
As usual, there are two ways to think about recursive programs: you can think through the entire flow of execution, or you can make the “leap of faith.” I have deliberately constructed this example to encourage you to make the leap of faith.
When you were using sort to sort the subdecks, you didn’t feel
compelled to follow the flow of execution, right? You just assumed that
the sort function would work because you already debugged it. Well,
all you did to make merge_sort recursive was replace one sort
algorithm with another. There is no reason to read the program
differently.
Well, actually you have to give some thought to getting the base case right and making sure that you reach it eventually, but other than that, writing the recursive version should be no problem. Good luck!
The efficiency of a simple sorting algorithm is __________. The efficiency of mergesort is __________. Mergesort is __________ than the simple sorting algorithm.
Write your implementation of merge in the commented area of the active
code below. Read the comments in main to see how we'll test if your
merge function works. If you get stuck, you can reveal the extra problem
at the end for help.
1#include <stdexcept>
2#include <iterator>
3#include <cstddef>
4#include <random>
5#include <iostream>
6#include <string>
7#include <vector>
8using std::cout;
9
10enum card_suit { clubs, diamonds, hearts, spades };
11
12enum card_rank { ace=1, two, three, four, five, six, seven, eight, nine,
13ten, jack, queen, king };
14
15std::size_t random_int(std::size_t low, std::size_t high);
16
17struct playing_card {
18 card_rank rank;
19 card_suit suit;
20 playing_card ();
21 playing_card (card_suit s, card_rank r);
22 void print () const;
23 bool is_greater (const playing_card& c2) const;
24 bool equals (const playing_card& c2) const;
25};
26
27struct card_deck {
28 std::vector<playing_card> cards;
29 card_deck ();
30 card_deck (std::size_t n);
31 void print () const;
32 void swap_cards (std::size_t index1, std::size_t index2);
33 std::size_t find_lowest_card (std::size_t index);
34 void shuffle_deck ();
35 void sort_deck ();
36 card_deck subdeck (std::ptrdiff_t low, std::ptrdiff_t high) const;
37};
38
39std::ptrdiff_t find_bisect (card_deck subdeck, playing_card card);
40
41card_deck merge (const card_deck& d1, const card_deck& d2) {
42 // ``merge`` should merge d1 with d2 and return
43 // a merged deck. Follow the pseudocode above,
44 // delete the existing code, and write your
45 // implementation here.
46 card_deck deck(0); return deck;
47}
48
49int main() {
50 card_deck deck;
51
52 // Shuffle a deck of cards and split it in half
53 deck.shuffle_deck();
54 card_deck d1 = deck.subdeck(0, 25);
55 card_deck d2 = deck.subdeck(26, 51);
56
57 // Sort each half
58 d1.sort_deck();
59 d2.sort_deck();
60 cout << "Sorted first half:" << std::endl;
61 d1.print();
62 cout << std::endl;
63 cout << "Sorted second half:" << std::endl;
64 d2.print();
65 cout << std::endl;
66
67 // Merge sorted decks together
68 card_deck finished = merge(d1, d2);
69
70 // We should see a sorted standard deck of 52 cards
71 cout << "Merged sorted full deck:" << std::endl;
72 finished.print();
73}
merge Help
First, let's write the code for the merge function. merge should take two decks as parameters and return a deck with the deck merged.
-
else { result.cards[k] = d1.cards[i]; ++i; } } -
} return result; } -
card_deck merge (const card_deck& d1, const card_deck& d2) { -
card_deck result (d1.cards.size() + d2.cards.size()); -
else if (d1.cards.empty()) { result.cards[k] = d2.cards[j]; ++j; } -
else if (d2.cards.empty()) { result.cards[k] = d1.cards[i]; ++i; } -
else if (i >= d1.cards.size() || d1.cards[i].is_greater(d2.cards[j])) { result.cards[k] = d2.cards[j]; ++j; } -
else { -
for (std::size_t k = 0; k < result.cards.size(); ++k) { -
if (d1.cards.empty()) { result.cards[k] = d1.cards[i]; ++i; } -
if (d1.cards.empty()) { result.cards[k] = d2.cards[j]; ++j; } -
if (j >= d2.cards.size()) { result.cards[k] = d1.cards[i]; ++i; } -
std::size_t i = 0; std::size_t j = 0; -
void merge (const card_deck& d1, const card_deck& d2) {
Now that we've written merge, it's time to write the merge_sort function. Try writing
the non-recursive version of merge_sort first before writing the recursive version. Follow the
comments in main to test your functions. If done correctly, the program should output a sorted
deck of cards. If you get stuck, you can reveal the extra problems at the end for help.
1#include <stdexcept>
2#include <iterator>
3#include <cstddef>
4#include <random>
5#include <iostream>
6#include <string>
7#include <vector>
8
9enum card_suit { clubs, diamonds, hearts, spades };
10
11enum card_rank { ace=1, two, three, four, five, six, seven, eight, nine,
12ten, jack, queen, king };
13
14std::size_t random_int(std::size_t low, std::size_t high);
15
16struct playing_card {
17 card_rank rank;
18 card_suit suit;
19 playing_card ();
20 playing_card (card_suit s, card_rank r);
21 void print () const;
22 bool is_greater (const playing_card& c2) const;
23 bool equals (const playing_card& c2) const;
24};
25
26struct card_deck {
27 std::vector<playing_card> cards;
28 card_deck ();
29 card_deck (std::size_t n);
30 void print () const;
31 void swap_cards (std::size_t index1, std::size_t index2);
32 std::size_t find_lowest_card (std::size_t index);
33 void shuffle_deck ();
34 void sort_deck ();
35 card_deck subdeck (std::ptrdiff_t low, std::ptrdiff_t high) const;
36 card_deck merge_sort () const;
37 card_deck merge_sort (card_deck deck) const;
38};
39
40std::ptrdiff_t find_bisect (card_deck subdeck, playing_card card);
41card_deck merge (const card_deck& d1, const card_deck& d2);
42
43card_deck card_deck::merge_sort () const {
44 // This version of ``merge_sort`` is the non-recursive version.
45 // Follow the pseudocode above delete the existing code,
46 // and write your implementation here.
47 card_deck deck(0); return deck;
48}
49
50card_deck card_deck::merge_sort (card_deck deck) const {
51 // This version of ``merge_sort`` is the recursive version.
52 // Follow the pseudocode above delete the existing code,
53 // and write your implementation here.
54 card_deck deck1(0); return deck;
55}
56
57int main() {
58 card_deck deck1;
59 deck1.shuffle_deck();
60 card_deck sorted1 = deck1.merge_sort();
61 sorted1.print();
62
63 // Once you get the above code to work, comment it
64 // out and uncomment the code below to test the
65 // recursive version of ``merge_sort``.
66
67 /*
68 card_deck deck2;
69 deck2.shuffle_deck();
70 card_deck sorted2 = deck2.merge_sort(deck2);
71 sorted2.print();
72 */
73}
merge_sort Help
Let's write the code for the merge_sort function. merge_sort should be a card_deck member function that returns a sorted deck.
-
return merge(d1, d2); } -
card_deck card_deck::merge_sort () const { -
card_deck d1 = subdeck(0, mid - 1); card_deck d2 = subdeck(mid, std::ssize(cards) - 1); -
card_deck merge_sort () { -
d1.sort_deck(); d2.sort_deck(); -
std::ptrdiff_t mid = std::ssize(cards) / 2;
merge_sort Recursion Help
Let's take it one step further and rewrite merge_sort as a
recursive function.
-
return merge(merged1, merged2); } -
card_deck card_deck::merge_sort (card_deck deck) const { -
card_deck d1 = subdeck(0, mid - 1); card_deck d2 = subdeck(mid, std::ssize(deck.cards) - 1); -
card_deck merged1 = d1.merge_sort(d1); card_deck merged2 = d2.merge_sort(d2); -
if (deck.cards.size() == 0 || deck.cards.size() == 1) { return deck; } -
std::ptrdiff_t mid = std::ssize(deck.cards) / 2;