Decks, Shuffling, Selection Sort, and Merge Sort

A Deck class whose instance variable is an array of Cards, pseudocode as a way of discovering which helper methods you need, shuffling by repeated swaps, selection sort and why it is quadratic, and merge sort — which needs subdecks and a merge before it can be written at all. Follows Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.1-13.6, pp. 217-224, cross-referenced against Java SE 21 API — java.util.Collections (shuffle, sort).

Subject: Java · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Decks, Shuffling, Selection Sort, and Merge Sort

Title

Think Java 2e · Chapter 13 · Objects of Arrays

Sections 13.1-13.6 · pp. 217-224

2. What you will be able to do

Objectives

This lesson follows Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.1-13.6, pp. 217-224. Everything on these slides can be checked against those pages.

1. Write a class whose instance variable is an array, with more than one constructor.

2. Convert a static method into an instance method and explain why the code gets shorter.

3. Use pseudocode to design an algorithm and discover the helper methods it needs.

4. Explain what a helper method is and why helper methods are usually private.

5. Describe selection sort and explain why its cost is proportional to n².

6. Explain why merge sort's n log₂ n cost matters, and write subdeck without an off-by-one error.

3. Retrieve before you read

Warm-up

Chapter 13 picks up exactly where Chapter 12 stopped.

Discussion prompt

From Lesson 12b: what does new Card[52] create, and what does it not create? And from Lesson 12a: why is it safe for two different collections to hold references to the same Card object?

Hint: One array, no cards. And one keyword per instance variable.

Answer:

new Card[52] creates an array of fifty-two null references and no Card objects at all. And sharing cards is safe because Card is immutable — with final instance variables and no setters, nobody can change a card out from under anybody else.

Both facts are load-bearing in this chapter. In the previous chapter we defined a class to represent cards and used an array of Card objects to represent a deck. In this chapter we take additional steps toward object-oriented programming — starting by giving that array a class of its own.

4. Objects of arrays

Concept

Chapter 12 was arrays of objects — an array whose elements are Cards. Chapter 13 is objects of arrays: an object whose instance variable is an array. Wrapping the array in a class gives every deck operation a place to live.

Figure (svg): A Deck object containing a private cards array which holds references to Card objects

Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.1-13.6, pp. 217-224 — Chapter 13 opens on printed page 217.

5. The Deck class

Section

Section 13.1

6. A class that encapsulates an array

Concept

Here is the beginning of a Deck class that encapsulates an array of Card objects. The instance variable is an array, and it is private — the same information-hiding decision as Lesson 11a.

public class Deck {
    private Card[] cards;

    public Deck(int n) {
        this.cards = new Card[n];
    }

    public Card[] getCards() {
        return this.cards;
    }
}
memberkindpurpose
cardsprivate instance variablethe array of Cards
Deck(int n)constructoran empty deck of n slots
getCards()getterlets other classes read the array

encapsulate — To wrap data inside a class so that the methods operating on it live alongside it and clients cannot reach it directly.

The constructor initialises the instance variable with an array of n cards, but it doesn't create any Card objects. That is Lesson 12b's null array, now hidden one level deeper.

7. An unpopulated Deck

Picture it

A Deck exists. Its array exists. There is not a single Card anywhere.

Figure (svg): A Deck object whose cards array holds only null references, representing an unpopulated deck

Two objects have been created — the Deck and the array — and the reference in each slot is null. This constructor is for building a deck you intend to fill yourself, which is exactly what subdeck will need in Section 13.5.

8. A second constructor for a full deck

Worked example

We'll add another constructor that creates a standard 52-card array and populates it with Card objects. The body is Lesson 12b's nested loop, moved inside the class.

public Deck() {
    this.cards = new Card[52];
    int index = 0;
    for (int suit = 0; suit <= 3; suit++) {
        for (int rank = 1; rank <= 13; rank++) {
            this.cards[index] = new Card(rank, suit);
            index++;
        }
    }
}
constructorargumentresult
Deck(int n)5a deck of 5 nulls
Deck()nonea full, ordered 52-card deck
how Java tells them apartthe parameter listoverloading, from Lesson 11a

Two constructors, same name.

Why: Lesson 11a's overloading — Java picks by the parameter list.

The body is the code from Section 12.6.

Why: This method is similar to the example in Section 12.6; we just turned it into a constructor.

this.cards rather than a local.

Why: The array is stored in the instance variable, so it survives the constructor.

Using it is now one line.

Why: Deck deck = new Deck();

Verify: Create new Deck() and print the first and last cards; expect the Ace of Clubs and the King of Spades.

Why: The nested loop has not changed at all. What changed is where it lives: any program that wants a full deck now writes four characters instead of eight lines, which is the whole point of a constructor.

9. What does new Deck(5) create?

Prediction

Count the objects.

public Deck(int n) {
    this.cards = new Card[n];
}
Deck sub = new Deck(5);
thinghow many
Deck objects1
arrays1
Card objects?

Predict first

How many Card objects exist?

  • Zero — the array holds five nulls
  • Five
  • One
  • Fifty-two

Correct: Zero — the array holds five nulls

Why: The constructor creates the array but never calls new Card, so every element is null. That is exactly what subdeck wants in Section 13.5 — an empty container it can fill with references to cards that already exist.

10. printDeck becomes print

Concept

Now that we have a Deck class, we have a logical place to put methods that pertain to decks. Converting a static method to an instance method is Lesson 11b's three edits again.

public void print() {
    for (Card card : this.cards) {
        System.out.println(card);
    }
}
as a static methodas an instance method
declarationstatic void printDeck(Card[] cards)void print()
the arraya parameterthis.cards
invocationprintDeck(deck.getCards())deck.print()

Notice that when we transform a static method into an instance method, the code is shorter. The parameter disappears because the object already carries the data — which is the argument for instance methods in one line.

11. A getter that hands out the array

Trap

The trap

getCards returns the reference, not a copy.

public Card[] getCards() {
    return this.cards;
}

// so a client can do this:
Card[] c = deck.getCards();
c[0] = null;              // the deck's own array is now damaged
the array isconsequence
privateclients cannot name it
returned by a getterclients can reach it anyway
mutableand can change it

The private keyword protected the variable, not the object it refers to. This is Lesson 10a's aliasing, arriving through a method that looks perfectly innocent.

The fix

Know what a getter gives away, and decide deliberately.

// the book's choice - Pile.addDeck needs to iterate the cards
public Card[] getCards() {
    return this.cards;
}

// the defensive alternative
public Card[] getCards() {
    return Arrays.copyOf(this.cards, this.cards.length);
}
approachclients can readclients can damage the deck
return the arrayyesyes
return a copyyesno
the Cards themselvesreadable either wayno — Card is immutable

The last row is the saving grace. The Cards cannot be damaged whatever happens to the array, which is Lesson 12a's immutability paying off again — but the array's arrangement is still exposed.

12. Which constructor for which job?

Definition probe

Two constructors, two different needs.

Sort into buckets

Sort each situation.

Deck()
starting a game with a full deck; shuffling and dealing a standard deck
Deck(int n)
making room for a subdeck of 26 cards; building the result of a merge
full
You want fifty-two real cards in order, so the populating constructor does all the work.
empty
You want a container of a particular size that you will fill with references to cards that already exist — creating new Cards would be wrong as well as wasteful.

13. Write the instance method

Fill the middle

print traverses the deck's own array.

Fill in the blanks

public void print() this}.cards) card});
}
}

Why: As an instance method it reads this.cards rather than taking the array as a parameter, which is why the converted method is shorter than the static one. The enhanced for loop from Lesson 7a gives each element in turn, and println calls Card's toString automatically.

14. Why is Deck a better home for print?

Explain it to yourself

The static version worked fine.

Discussion prompt

printDeck(Card[] cards) was a perfectly good static method. What does moving it into Deck actually improve?

Hint: Where does someone look for it?

Answer:

Discoverability. Someone with a Deck types deck. and sees every operation a deck supports. A loose static method in some other class has to be known about in advance.

And cohesion: the data and the code that operates on it sit together, so changing how a deck is stored means changing one file. The methods stop being scattered around the program.

Think Java's phrasing is we have a logical place to put methods that pertain to decks. The class is the place — which is most of what object-oriented programming means in practice.

15. Shuffling and pseudocode

Section

Section 13.2

16. Write the algorithm before you write the Java

Concept

For most card games you have to shuffle the deck; that is, put the cards in a random order. Section 7.6 showed how to generate random numbers, but it is not obvious how to use them to shuffle a deck — so start with a sketch.

public void shuffle() {
    for each index i {
        // choose a random number between i and length - 1
        // swap the ith card and the randomly-chosen card
    }
}
linestatus
public void shuffle() {real Java
for each index i {English standing in for a for loop
// choose a random number...a comment describing a step
// swap the ith card...another step, not yet written

pseudocode — A combination of Java statements and English comments used to outline an algorithm before writing it.

To outline this algorithm we'll use a combination of Java statements and English comments. This technique is sometimes called pseudocode. It compiles into nothing and it is still the most valuable thing you can write at this stage.

17. Why not shuffle like a human?

Notation

The obvious approach is to imitate people, and Think Java rejects it for a reason that is worth reading twice.

Annotate

  • A human shuffle is random because it is sloppy. The imprecision is doing the work.
  • A program is precise, so the same operation repeated is a fixed permutation — and applying a fixed permutation eight times can land you exactly where you started.
  • That is the faro shuffle, and eight of them restore a 52-card deck perfectly. A shuffling method with a period of eight is not a shuffle.
  • Simulating a physical process is not the same as achieving its result. The physical process had a property — imprecision — that the simulation drops.
  • The better algorithm abandons the metaphor: traverse the deck one card at a time and swap each card with a randomly chosen one.

This is a general trap worth naming. Copying how humans do something can copy the mechanics and lose the point — and here it produces code that is deterministic while looking random.

18. Pseudocode reveals the helper methods

Worked example

The nice thing about pseudocode is that it often makes clear what other methods you are going to need. Read the two comments and the methods fall out.

private static int randomInt(int low, int high) {
    // return a random number between low and high,
    // including both
}

private void swapCards(int i, int j) {
    // swap the ith and the jth cards in the array
}
the comment saidso we needkind
choose a random number between i and length - 1randomInt(low, high)static
swap the ith card and the chosen cardswapCards(i, j)instance

Read each comment as a request for a method.

Why: We need a method that chooses a random integer in a given range and a method that takes two indexes and swaps the cards at those positions.

Give each one a signature.

Why: Naming the parameters forces you to decide what it needs to know.

Leave the bodies empty for now.

Why: The design is settled before any of it is written.

Notice one is static and one is not.

Why: randomInt is a class method and swapCards is an instance method. Do you understand why?

Verify: Ask yourself the book's question before reading on: which of the two touches this.cards?

Why: swapCards rearranges the deck's own array, so it needs an object. randomInt just does arithmetic on two ints and touches no instance variable at all — so it does not need an object, and should not require one. That is the rule Section 13.8 will state outright.

19. Why not simulate a human shuffle?

Prediction

A program can divide and interleave perfectly.

// perfect riffle shuffle, applied eight times
// to a 52-card deck
shufflesdeck order
1permuted
8back to the original

Predict first

What goes wrong?

  • A perfect shuffle is deterministic — eight of them restore the original order
  • It is too slow for 52 cards
  • It needs more random numbers than are available
  • Nothing — it is the recommended algorithm

Correct: A perfect shuffle is deterministic — eight of them restore the original order

Why: Human shuffling works because humans are imprecise; a program's precision turns the same operation into a fixed permutation whose period happens to be eight. The randomness in the human version came from the sloppiness, and the simulation drops exactly that.

20. Helper methods and top-down design

Concept

Methods like randomInt and swapCards are called helper methods, because they help you solve parts of the problem.

helper method — A method that solves part of a larger problem, usually private because only methods in the same class need it.

top-down design — Writing pseudocode first, then writing the helper methods that make it work.

propertyreason
usually privateused only by methods in the class
smalleach does one step of the outline
discovered, not plannedthe pseudocode asked for them
testable aloneswapCards can be checked without shuffle

The process of writing pseudocode first and then writing helper methods to make it work is a kind of top-down design. It is an alternative to incremental development and encapsulation and generalization, the other design processes in this book — Lessons 4b and 5b respectively. Three processes, and you choose.

21. Writing the whole method at once

Trap

The trap

Straight to Java, with every detail at once.

public void shuffle() {
    for (int i = 0; i < cards.length; i++) {
        int j = i + (int) (Math.random() * (cards.length - i));
        Card temp = cards[i];
        cards[i] = cards[j];
        cards[j] = temp;
    }
}
problemwhy it hurts
the range arithmetic is inlinehard to check, easy to get wrong
the swap is inlinethree lines of noise inside the real idea
nothing is reusableselection sort needs a swap too
if it is wrongyou do not know which part

It may well work. But the structure of the algorithm — traverse, choose, swap — is buried under arithmetic, and the swap that selection sort is about to need has to be written again.

The fix

Outline first; each comment becomes a method.

public void shuffle() {
    for (int i = 0; i < cards.length; i++) {
        int j = randomInt(i, cards.length - 1);
        swapCards(i, j);
    }
}
benefitdetail
shuffle reads as its own descriptiontraverse, choose, swap
each helper is testable alonecheck randomInt's range separately
swapCards is reusedselection sort needs exactly this
a bug has an addressyou know which method to look in

The finished method looks like the pseudocode — which is the sign that the decomposition was right. And swapCards is about to be reused by selection sort without a line being changed.

22. Static or instance?

Definition probe

The deciding question is whether the method touches this.cards.

Sort into buckets

Sort each helper.

static
randomInt(low, high)
instance
swapCards(i, j); print(); indexLowest(low, high)
st
It reads and writes no instance variable — it just computes from its arguments — so it needs no object and should not demand one.
in
It reads or writes this.cards, so it needs a particular deck to operate on.

23. Finish the shuffle

Fill the middle

Choose a random index and swap.

Fill in the blanks

for (int i = 0; i < cards.length; i++) randomInt}(i, cards.length - 1);
swapCards(i, j);
}

Why: The finished method is the pseudocode with each comment replaced by a call, which is the sign that the decomposition was right. Note the range: from i to the last index, so a card can be swapped with itself — excluding that possibility would actually make the shuffle less random.

24. Why should helper methods be private?

Socratic

They work perfectly well as public methods.

Discussion prompt

Think Java says helper methods are often private. What does making swapCards private buy, given that a public version would behave identically?

Hint: What is a class promising with its public methods?

Answer:

A class's public methods are a promise: they are what other code is allowed to depend on. swapCards is a step in an algorithm, not a service the class offers.

Making it private means you can rename it, change its parameters, or delete it when the algorithm changes — without breaking anyone, because nobody outside could have called it.

This is Lesson 11a's argument again: the smaller the public surface, the more freedom you keep. Private is not about secrecy; it is about what you are allowed to change later.

25. Selection sort

Section

Section 13.3

26. Repeatedly select the lowest remaining card

Concept

Now that we have shuffled the deck, we need a way to put it back in order. There is an algorithm for sorting that is ironically similar to the algorithm for shuffling — it also traverses and swaps.

public void selectionSort() {
    for each index i {
        // find the lowest card at or to the right of i
        // swap the ith card and the lowest card found
    }
}

private int indexLowest(int low, int high) {
    // find the lowest card between low and high
}
iterationsearchesswaps into
0the whole arrayposition 0
1positions 1 to the endposition 1
2positions 2 to the endposition 2
ipositions i to the endposition i

selection sort — A sorting algorithm that traverses the array repeatedly, selecting the lowest remaining element each time and swapping it into place.

It's called selection sort because it works by traversing the array repeatedly and selecting the lowest (or highest) remaining card each time. During the ith iteration we find the lowest card to the right of i and swap it with the ith card.

27. The sorted region grows from the left

Picture it

After each pass, one more position is permanently correct — and everything to its left will never move again.

Figure (svg): A row of array positions showing the sorted prefix growing one element per pass while the unsorted suffix shrinks

The searched region shrinks by one each pass, so the passes are not all the same length: the first examines n elements, the last examines two. That detail is what makes the total cost what it is.

28. Reusing swapCards

Worked example

Again, the pseudocode helps with the design of the helper methods. And this time one of them already exists.

// from Section 13.2, unchanged:
private void swapCards(int i, int j) { ... }

// the one new helper this algorithm needs:
private int indexLowest(int low, int high) {
    // find the lowest card between low and high
}
helperwritten forreused by
swapCardsshuffleselectionSort
indexLowestselectionSort—
randomIntshuffle—

Read the pseudocode's two comments.

Why: Find the lowest card at or to the right of i, then swap.

Notice the second one is already solved.

Why: We can reuse swapCards from the previous section, so we need only a method to find the lowest card.

Name the new one.

Why: indexLowest(int low, int high) — it returns an index, like the searches in Lesson 12b.

Notice it uses compareTo.

Why: Finding the lowest card means comparing cards, which is the method Lesson 12a wrote.

Verify: Shuffle a deck, sort it, and print it: expect a new deck's order — Clubs first, Aces low.

Why: The reuse is the payoff of the previous section's decomposition. Had the swap been written inline inside shuffle, it would now be written a second time inside selectionSort — and there would be two places for the same bug to live.

29. What is the lowest card after one pass?

Prediction

Selection sort, first iteration, on a shuffled deck.

int lowest = indexLowest(0, cards.length - 1);
swapCards(0, lowest);
stepeffect
indexLowest(0, 51)the index of the lowest card
swapCards(0, that)it moves to position 0

Predict first

What is true after the first pass?

  • cards[0] holds the lowest card in the whole deck, permanently
  • The deck is sorted
  • cards[0] holds the lowest card of the first half
  • Nothing has moved

Correct: cards[0] holds the lowest card in the whole deck, permanently

Why: The first pass searches the entire array, so what it finds is the overall lowest, and swapping it into position 0 puts it where it belongs for good. That is the invariant the algorithm maintains: after pass i, positions 0 through i are final and never searched again.

30. Why selection sort is quadratic

Concept

Selection sort is a simple algorithm, but it is not very efficient. The cost follows directly from the shape of the two loops.

// to sort n items:
//   pass 1 examines n     elements
//   pass 2 examines n - 1
//   ...
//   pass n examines 1
//
// total ≈ n * n / 2, which is proportional to n squared
nroughly n² / 2 comparisons
1050
521,350
1,000500,000
1,000,000500,000,000,000

To sort n items it has to traverse the array n − 1 times. Each traversal takes an amount of time proportional to n. The total time, therefore, is proportional to n². Read the last row: half a trillion comparisons to sort a million cards.

31. Searching the whole array every pass

Trap

The trap

Restarting the search from zero each time.

for (int i = 0; i < cards.length; i++) {
    int lowest = indexLowest(0, cards.length - 1);   // always from 0
    swapCards(i, lowest);
}
passfindsswaps intoresult
0the overall lowestposition 0correct
1the overall lowest — againposition 1undoes pass 0
2the same card once moreposition 2worse

The lowest card gets dragged rightwards forever. The search must start at i, not at 0 — the region to the left is finished and must not be touched again.

The fix

Search only the unsorted region.

for (int i = 0; i < cards.length; i++) {
    int lowest = indexLowest(i, cards.length - 1);   // from i
    swapCards(i, lowest);
}
passsearchespositions now final
00 to 510
11 to 510, 1
22 to 510, 1, 2

The invariant is that everything to the left of i is sorted and final. Naming that invariant is what makes the low bound obviously i — the same reasoning Lesson 8b used for accumulator loops.

32. How many comparisons to sort 1000 items?

Prediction

Selection sort, worst case, roughly.

// n - 1 traversals, each proportional to n
// total proportional to n squared
nn²roughly n² / 2
1,0001,000,000500,000

Predict first

Roughly how many comparisons?

  • About half a million
  • About a thousand
  • About ten
  • About twenty

Correct: About half a million

Why: Each of about a thousand passes examines up to a thousand elements, giving a million comparisons if you count generously and about half that once you account for the shrinking region. Either way the cost is proportional to n², which is why doubling the input roughly quadruples the work.

33. Match the helper to its job

Matching

Three helpers across two algorithms.

Match the pairs

  • a. randomInt
  • b. swapCards
  • c. indexLowest
  • d. print
  • r1. a number in a given range
  • r2. exchange two positions
  • r3. the position of the smallest card in a range
  • r4. display every card

Why: swapCards is the one used by both shuffle and selectionSort — which is exactly the reuse that writing helper methods rather than inline code buys. Note that indexLowest returns a position, not a card, for the same reason Lesson 12b's searches did.

34. Is selection sort ever the right choice?

Counterexample

Quadratic sounds like a verdict.

Discussion prompt

Merge sort is asymptotically much better. Name a situation where selection sort is nevertheless a reasonable thing to use.

Hint: How big is a hand of cards?

Answer:

When n is small. For a hand of five cards the difference is nothing, and selection sort is shorter, simpler and easier to get right.

And when swaps are expensive but comparisons are cheap — selection sort performs at most n swaps, which is fewer than most alternatives.

Asymptotic cost is about large inputs, and it is worth remembering it says nothing about small ones. Think Java's own almostMergeSort uses selection sort on the two halves for exactly this reason before recursion replaces it.

35. Merge sort

Section

Section 13.4

36. Merging two sorted decks is fast

Concept

We will develop a more efficient algorithm called merge sort. To sort n items, merge sort takes time proportional to n log₂ n. The idea behind it is one observation: if you have two decks, each of which has already been sorted, you can quickly merge them into a single sorted deck.

// try this with a real deck of cards:
// 1. form two decks of about 10 cards each and sort them,
//    face up with the lowest cards on top
// 2. compare the TOP card from each deck and choose the lower;
//    flip it over onto the merged deck
// 3. repeat until one deck is empty, then add the remaining cards
d1 topd2 toptakemerged so far
2♣3♣2♣2♣
5♣3♣3♣2♣ 3♣
5♣7♣5♣2♣ 3♣ 5♣
9♣7♣7♣2♣ 3♣ 5♣ 7♣

merge sort — A sorting algorithm that splits the collection in half, sorts each half, and merges the sorted halves.

You only ever look at the two top cards, and each comparison places one card permanently. Merging two sorted decks of ten cards takes about twenty comparisons, not two hundred.

37. Why n log n is not a small improvement

Notation

Think Java gives the arithmetic outright, and it is worth reading slowly.

Annotate

  • Twenty million against one trillion — a factor of fifty thousand, for the same input.
  • At a billion operations a second, that is 0.02 seconds against about 17 minutes.
  • The gap widens with n. At ten million items merge sort takes about 230 million steps and selection sort takes a hundred trillion.
  • Both algorithms are correct. Neither has a bug. The difference is entirely in how the work grows.
  • This is the same lesson as Lesson 12b's two searches, one degree up: there, n against log n; here, n² against n log n.

Choosing an algorithm is a bigger decision than any amount of tuning. No amount of making selection sort's inner loop faster closes a factor of fifty thousand.

38. The shape of merge sort

Worked example

Merge sort is three steps, and each one is a method you can write separately.

public Deck almostMergeSort() {
    // divide the deck into two subdecks
    // sort the subdecks using selectionSort
    // merge the subdecks, return the result
}
stepthe method neededsection
dividesubdeck(low, high)13.5
sort each halfselectionSort13.3
combinemerge(d1, d2)13.6

Split the deck in two.

Why: That needs a subdeck method, which is the next section.

Sort each half.

Why: For now, with the selection sort you already have.

Merge the two sorted halves.

Why: That needs a merge method, which is Section 13.6.

Notice this is still not merge sort.

Why: It is still not very efficient, because it uses selectionSort to sort the subdecks.

Verify: Work out why halving helps even with a quadratic sort: two sorts of 26 cards cost about 2 × 26² = 1,352 against 52² = 2,704 for one sort of 52.

Why: Halving alone nearly halves the work — and the obvious next thought is to halve again, which is what makes the method recursive in Lesson 13b. almostMergeSort is a deliberate stepping stone.

39. How many comparisons to merge two sorted 10-card decks?

Prediction

Each comparison places one card.

// 20 cards total
// each comparison moves exactly one card to the result
cards to placecomparisons
20at most 19

Predict first

Roughly how many comparisons does the merge need?

  • About 20
  • About 200
  • About 100
  • About 400

Correct: About 20

Why: Every comparison of the two top cards places exactly one card in the result, so twenty cards need at most nineteen comparisons — and fewer, since once a deck empties the rest are copied straight across. Merging is cheap; that cheapness is the whole basis of the algorithm.

40. The two costs side by side

Concept

Every row of this table is the same input sorted two different ways.

nselection sort ≈ n²merge sort ≈ n log₂ n
1010033
522,704297
1,0001,000,00010,000
1,000,0001,000,000,000,00020,000,000

At n = 10 the difference barely matters. At a million it is the difference between a program that answers and one that does not — which is why the crossover point, not the formula, is what you actually care about in practice.

41. Merging decks that are not sorted

Trap

The trap

Merging assumes both inputs are already in order.

// d1 = 5♣ 2♣ 9♣   (NOT sorted)
// d2 = 3♣ 7♣ J♣

// compare tops: 5♣ vs 3♣ -> take 3♣
// compare tops: 5♣ vs 7♣ -> take 5♣
// compare tops: 2♣ vs 7♣ -> take 2♣   <- out of order!
merged so farsorted?
3♣yes
3♣ 5♣yes
3♣ 5♣ 2♣no

Merging only looks at the two top cards, so it can never discover that something further down is out of place. The output is wrong and nothing complains — Lesson 12b's binary search precondition again.

The fix

Sort each half first — that is what the algorithm is.

// divide the deck into two subdecks
// SORT the subdecks
// merge the subdecks
stepguarantees
sort each subdeckboth inputs to merge are ordered
mergecomparing tops is enough
resulta fully sorted deck

The middle step exists precisely to establish merge's precondition. That is worth seeing clearly, because it is what makes the recursion in Lesson 13b legitimate: whatever sorts the halves, merge only needs them sorted.

42. Which algorithm's cost?

Definition probe

Two growth rates.

Sort into buckets

Sort each statement.

selection sort — n²
doubling n roughly quadruples the work; a million items takes about a trillion steps
merge sort — n log n
a million items takes about 20 million steps; doubling n adds slightly more than double the work
sel
Squaring means doubling the input multiplies the work by four — and at a million items that reaches a trillion steps.
mer
n log n grows barely faster than n itself, because the logarithm creeps up by one each time the input doubles.

43. The three steps

Fill the middle

Merge sort in outline.

Fill in the blanks

// divide the deck into two subdecks
// sort the subdecks
// merge the subdecks, return the result

Why: Divide, sort each half, merge. The middle step is what establishes merge's precondition — merge only compares the top two cards, so it is correct only when both inputs are already in order. Which sorting method the middle step uses is exactly the question Lesson 13b answers with recursion.

44. Why does splitting help at all?

Explain it to yourself

You still have to sort all 52 cards.

Discussion prompt

Even with selection sort on both halves, splitting first is cheaper. Why does dividing the work reduce the total when nothing got faster?

Hint: What does squaring do to a halved input?

Answer:

Because the cost is quadratic. One sort of 52 costs about 52² = 2,704. Two sorts of 26 cost about 2 × 26² = 1,352 — half as much, plus a cheap merge.

Halving the input quarters the cost of a quadratic algorithm, and you do it twice, so you save half overall. The saving comes from the squaring, not from any speed-up.

And the argument repeats. If halving once helps, halving the halves helps again — which is exactly the thought that turns almostMergeSort into a recursive method in Lesson 13b.

45. Subdecks and merging

Section

Sections 13.5-13.6

46. Splitting a deck: the + 1 that matters

Concept

The first step of merge sort is to split the deck into two subdecks, each with about half of the cards. So we need a method that takes a range of indexes and returns a new deck with that subset.

public Deck subdeck(int low, int high) {
    Deck sub = new Deck(high - low + 1);
    for (int i = 0; i < sub.cards.length; i++) {
        sub.cards[i] = this.cards[low + i];
    }
    return sub;
}
lowhighhigh - low + 1cards
045indexes 0,1,2,3,4
02526the first half
265126the second half
331just card 3

The length of the subdeck is high − low + 1, because both the low card and the high card are included. Think Java says this plainly: forgetting the '+ 1' often leads to off-by-one errors, and drawing a picture is usually the best way to avoid them.

47. The subdeck shares its cards

Picture it

The loop copies references, not cards. The result is a hand of five cards that are also still in the original deck.

Figure (svg): A subdeck array whose five references point at the same Card objects as the original deck's first five slots

The result is a hand with five cards that are shared with the original deck; that is, they are aliased. Two decks, one set of cards — and Lesson 10a said that was dangerous.

48. Why the aliasing is safe here

Worked example

Chapter 10 spent a whole section on why aliasing causes bugs. Chapter 13 aliases deliberately, and explains exactly why it is allowed to.

// from Lesson 12a:
public class Card {
    private final int rank;    // final - no method can change it
    private final int suit;
    // getters, no setters
}
questionanswer
can a shared card be changed?no — Card is immutable
so can one deck affect another?no
what does sharing save?creating 26 duplicate Card objects
would this be safe for a mutable class?no

State the general worry.

Why: Aliasing might not be a good idea, because changes to shared cards would be reflected in multiple decks.

Check whether it applies.

Why: But since Card objects are immutable, this kind of aliasing is not a problem.

Note the benefit.

Why: And it saves some memory because we don't create duplicate Card objects.

Note what would change the answer.

Why: If Card had setters, subdeck would have to copy each card — slower, and more code.

Verify: Take subdeck(0, 4), sort the subdeck, and confirm the original deck's first five cards are unchanged.

Why: The subdeck rearranges its own array of references; the Cards never move and never change. This is the design decision from Lesson 12a being cashed in — immutability is what makes sharing free, and it is why this method is four lines rather than fourteen.

49. How many cards in subdeck(0, 25)?

Prediction

Both endpoints are included.

Deck sub = new Deck(high - low + 1);
// low = 0, high = 25
expressionvalue
high - low25
high - low + 126

Predict first

How many cards does the subdeck hold?

  • 26
  • 25
  • 27
  • 52

Correct: 26

Why: Indexes 0 through 25 inclusive is twenty-six positions, because both endpoints count. This is the classic fence-post error, and the reliable check is the smallest case: subdeck(3, 3) must contain one card, and only the + 1 version gives that.

50. The merge algorithm, in pseudocode

Concept

The next helper method we need is merge, which takes two sorted subdecks and returns a new deck containing all cards from both decks, in order.

private static Deck merge(Deck d1, Deck d2) {
    // create a new deck, d3, big enough for all the cards
    // use the index i to keep track of where we are at in
    // the first deck, and the index j for the second deck
    int i = 0;
    int j = 0;

    // the index k traverses the result deck
    for (int k = 0; k < d3.length; k++) {
        // if d1 is empty, use top card from d2
        // if d2 is empty, use top card from d1
        // otherwise, compare the top two cards
        // add lowest card to the new deck at k
        // increment i or j (depending on card)
    }
    // return the new deck
}
indextraverses
ithe first deck, d1
jthe second deck, d2
kthe result deck, d3

Three indexes, one per deck. k advances every iteration because a card is always placed; i and j advance only when a card is taken from their deck — which is the detail that makes merge work, and the reason Think Java warns it's a little tricky, so be sure to test it with different subdecks.

51. Getting subdeck's length wrong

Trap

The trap

high - low loses a card.

Deck sub = new Deck(high - low);      // one too small

// subdeck(0, 4) makes a 4-card deck for 5 cards
// subdeck(0, 25) makes a 25-card deck for 26 cards
lowhighcards in rangehigh - lowhigh - low + 1
0454 — wrong5 — right
0252625 — wrong26 — right
3310 — wrong1 — right

The last row makes it obvious: a range from 3 to 3 contains one card, and high - low says zero. Checking a formula on the smallest case is the fastest way to catch this.

The fix

Both endpoints are included, so add one.

Deck sub = new Deck(high - low + 1);
for (int i = 0; i < sub.cards.length; i++) {
    sub.cards[i] = this.cards[low + i];
}
ireads fromwrites to
0cards[low]sub.cards[0]
1cards[low + 1]sub.cards[1]
lastcards[high]sub.cards[length - 1]

Drawing a picture is usually the best way to avoid them, says the book — and the table above is that picture. Note also the offset low + i: the subdeck counts from 0 while the source counts from low.

52. What happens if you sort a subdeck?

Prediction

The subdeck shares cards with the original.

Deck sub = deck.subdeck(0, 4);
sub.selectionSort();
deck.print();      // the original
what movesin which array
referencessub.cards only
Card objectsnothing moves — they are shared

Predict first

What happens to the original deck?

  • Nothing — sorting rearranges the subdeck's own array of references
  • It gets sorted too, since the cards are shared
  • Its first five cards become null
  • A NullPointerException

Correct: Nothing — sorting rearranges the subdeck's own array of references

Why: The two decks share Card objects but each has its own array. Sorting moves references around inside sub.cards and never touches deck.cards or any Card. The aliasing would only matter if a Card could be modified — and Lesson 12a made sure none can.

53. Which index advances when?

Definition probe

merge keeps three of them.

Sort into buckets

Sort each index by when it advances.

every iteration
k, the result index
only sometimes
i, when d1's card is taken; j, when d2's card is taken
not then
i, when d2's card is taken
every
A card is placed in the result on every pass, so the result index always advances.
some
A source index advances only when a card is taken from that deck — which is what lets one deck run ahead of the other.
never
Taking from d2 leaves d1's position untouched; advancing i there would skip a card.

54. Why does merge need a new deck at all?

Socratic

It could try to merge in place.

Discussion prompt

merge creates a third deck rather than rearranging the two it was given. What goes wrong if you try to do it in place?

Hint: Where would a card go if its slot is occupied?

Answer:

There is nowhere to put a card. Placing d2's top card at the front of d1 means shifting every card in d1 along — which turns a cheap merge into a quadratic one.

The third deck gives every card a free slot, so each is placed in one step and merge stays proportional to the number of cards.

Merge sort buys its speed with memory — it needs room for a copy. That is a trade worth naming, and it is why some sorting algorithms that use no extra space are preferred in memory-tight settings despite being harder to write.

55. The two sorts, and the design processes

Comparison

Fill the blanks.

Comparison matrix

selection sortmerge sort
how it worksselect the lowest, swap it into placesplit, sort the halves, merge
cost for n itemsproportional to n²proportional to n log₂ n
steps for a million itemsabout a trillionabout 20 million
extra memory needednoneroom for a copy
helper methodsindexLowest, swapCardssubdeck, merge

The fourth row is the one that keeps selection sort alive. Merge sort's speed is bought with memory, and for a small deck the trade is not obviously worth making.

56. The pattern to carry away

Pattern

Top-down design: write the outline, and let it tell you which methods to write.

// 1. write the algorithm as pseudocode
public void shuffle() {
    for each index i {
        // choose a random number between i and length - 1
        // swap the ith card and the randomly-chosen card
    }
}

// 2. each comment becomes a helper method signature
private static int randomInt(int low, int high) { }
private void swapCards(int i, int j) { }

// 3. fill in the helpers, then the outline becomes real code
public void shuffle() {
    for (int i = 0; i < cards.length; i++) {
        swapCards(i, randomInt(i, cards.length - 1));
    }
}
design processwhen to reach for itlesson
incremental developmentyou are not sure the pieces work4b
encapsulation and generalizationyou have working code to tidy5b
top-down designyou know the shape but not the pieces13a

57. Check: the empty constructor

Check

Work it out before you click.

public Deck(int n) {
    this.cards = new Card[n];
}
Deck sub = new Deck(26);
createdcount
Deck objects1
arrays1

Check your understanding

How many Card objects has this created?

  • A. None — the array holds 26 null references (correct)
  • B. 26
  • C. 52
  • D. It does not compile without a rank and suit

Answer: A

Why: The constructor allocates the array and stops; nothing calls new Card. That is exactly what subdeck needs — an empty container to fill with references to cards that already exist. Creating fresh Cards there would be both wasteful and wrong, since the subdeck is meant to share.

Why B tempts people
That would require a loop calling new Card, which this constructor does not have — the no-argument constructor is the one that populates.
Why C tempts people
52 is what new Deck() produces; this constructor makes exactly n slots.
Why D tempts people
A Deck constructor takes a size, not a card's rank and suit.

58. Check: subdeck's length

Check

Work it out before you click.

Deck sub = new Deck(high - low + 1);
// subdeck(3, 3)
lowhighcards in range
331

Check your understanding

Why is the + 1 necessary?

  • A. Both endpoints are included, so the range from low to high holds high − low + 1 cards (correct)
  • B. Arrays need one extra element for the null placeholder
  • C. Java array indexes start at 1
  • D. It reserves space for the merge

Answer: A

Why: The range includes the card at low and the card at high, so subdeck(3, 3) must hold one card — and high - low gives zero. Checking a formula against its smallest case is the quickest way to catch this kind of off-by-one, and Think Java recommends drawing a picture for the same reason.

Why B tempts people
The null placeholder was Chapter 12's RANKS array, a different situation entirely.
Why C tempts people
Java array indexes start at 0; that is why cards[low + i] rather than cards[low + i - 1].
Why D tempts people
merge creates its own result deck; subdeck reserves nothing for it.

59. Check: cost

Check

Work it out before you click.

// a million items
// selection sort: proportional to n^2
// merge sort:     proportional to n log2 n
// log2 of a million is about 20
algorithmsteps
merge sort?
selection sort?

Check your understanding

Roughly how many steps does each need?

  • A. 20 million and one trillion (correct)
  • B. 20 and a million
  • C. a million and a million
  • D. 20 million and 40 million

Answer: A

Why: n log₂ n for a million is 1,000,000 × 20 = 20 million; n² is 1,000,000 × 1,000,000 = one trillion. That factor of fifty thousand is why the choice of algorithm dominates every other optimisation — no amount of tuning selection sort's inner loop closes it.

Why B tempts people
20 is log₂ of a million, not the total — the total is n times that.
Why C tempts people
Neither algorithm is proportional to n alone; merge sort is n log n and selection sort is n².
Why D tempts people
That would make them comparable, which at a million items they are very much not.

60. Pseudocode is not a beginner's tool

Real world

Writing the outline before the code is a habit that professionals keep.

Discussion prompt

Think Java introduces pseudocode as a way to design shuffle. Why do experienced programmers still write it — and where else does the same idea appear?

Hint: What is easy to change on paper and expensive to change in code?

Answer:

Because the outline is where the design decisions are, and it is cheap to change. Rewriting five comments costs a minute; rewriting five methods costs an afternoon.

The same idea appears as a design document before a project, a plan in a comment before a hard method, and a commit message written before the commit — each is an outline that exposes a bad plan early.

And it tells you what to build. The two comments in shuffle's pseudocode named randomInt and swapCards, and one of those turned out to be exactly what selection sort needed as well — a reuse nobody planned, discovered because the step had a name.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

subdeck copies references, so the subdeck and the original share Card objects. Why is that not a bug?

  • Because Card is immutable, so a shared card cannot be changed by either deck
  • Because each deck has its own copy of the cards
  • Because subdeck is private
  • Because sorting a subdeck sorts the original too

Correct: Because Card is immutable, so a shared card cannot be changed by either deck

Why: Think Java raises the worry explicitly — aliasing might not be a good idea, because changes to shared cards would be reflected in multiple decks — and then dismisses it: but since Card objects are immutable, this kind of aliasing is not a problem. The final instance variables and missing setters from Lesson 12a are what make this four-line method correct. Each deck still has its own array, so rearranging one leaves the other alone; only the Cards are shared, and they never change.

62. Explain it to someone else

Explain it

Two minutes, out loud.

Discussion prompt

A classmate has written selection sort and asks why anyone would bother with merge sort, since both produce a sorted deck. Give them the argument with the numbers, and say what merge sort costs.

Hint: One trillion against twenty million, and memory.

Answer:

Both are correct — that is not the question. Selection sort's work is proportional to n², so a million items takes about a trillion steps. Merge sort is n log n, and log₂ of a million is about twenty, so it takes about twenty million.

For 52 cards you would not notice. The gap only opens as n grows, and then it opens enormously — a factor of fifty thousand at a million.

What it costs is memory: merge needs a new deck to write into, so merge sort needs room for a copy while selection sort sorts in place. Naming the cost is what makes it an argument rather than a slogan.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

What is the point of writing pseudocode before writing the method?

  • It settles the algorithm's structure and reveals which helper methods you need, while changes are still cheap
  • It is required before the compiler will accept the method
  • It runs faster than Java
  • It is a way of documenting code you have already written

Correct: It settles the algorithm's structure and reveals which helper methods you need, while changes are still cheap

Why: Think Java's phrasing is that the nice thing about pseudocode is that it often makes clear what other methods you are going to need. The two comments inside shuffle's outline named randomInt and swapCards before either existed — and swapCards turned out to be exactly what selection sort needed too. Rewriting a comment costs nothing; rewriting a method costs an afternoon, which is why the outline comes first.

64. Draw the whole lesson

Connect it up

One page, from memory.

Draw it

Draw a Deck object with its private cards array pointing at Card objects, then draw a subdeck beside it whose references point at the same Cards — and write one sentence saying why that is safe. Beneath it, write shuffle's pseudocode and the two helper signatures it produced, marking which is static and why. Finish with the two cost formulas, n² and n log₂ n, and the two numbers for a million items.

65. Recap

Recap

Six sections that wrap an array in a class and then compare two ways of sorting it.

if you remember one thingit is this
about designwrite the outline first; it names the methods
about sortingn² and n log n differ by fifty thousand at a million
about aliasingimmutability is what makes sharing free

Sources

  1. Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.1-13.6, pp. 217-224
  2. Java SE 21 API — java.util.Collections (shuffle, sort)
  3. Think Java 2e — free online edition and source code

Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108