← Skill tree CS Skill Tree 0 CSCD211

Stable Sort and the Two-Pass Trick

Textbook: BJP (Reges and Stepp)

Before You Start

Check each box you can do from memory. A box you cannot check yet is not a problem; it points you to a quick refresher, not a grade.

Not sure? Take the 60-second self-check.

1. Stable sort definition. What does “stable sort” mean?

Check

A sort is stable when elements that compare equal under the sort’s comparator remain in their original relative order after sorting. Equal elements are not swapped.

2. Java’s guarantee. Is Arrays.sort(Player[], Comparator) stable?

Check

Yes. Java’s object-array sort uses TimSort, which is guaranteed stable. Primitive-array sorts (Arrays.sort(int[])) are not documented as stable.

3. Which sort is last. Two sorts run in sequence: first by last name, then by team. What determines the final outer grouping?

Check

The final sort (by team) determines the outer grouping. The first sort’s result (last name order) survives only within groups of equal team values, because stability preserves the previous ordering among elements the last sort considers equal.


Try This First

Six players: three from “Bears,” three from “Aces.” Across both teams, games played are 5, 10, 15, 8, 12, 20.

// Pass 1
players.sort(byGamesPlayed);   // ascending games played

// Pass 2
players.sort(byTeam);          // alphabetical by team

Before reading further: predict the final order. Which team appears first? Within each team, what order?

Check

After pass 2, “Aces” appears first (alphabetically before “Bears”). Within each team, the order from pass 1 is preserved by stability: “Aces” players appear in ascending games-played order, and so do “Bears” players.


What You Need To Walk In With

The Insight: A stable sort preserves the previous ordering among elements the new comparator considers equal. This means: sort by the least important key first, most important key last. After the final sort, all elements are in primary-key order. Within each primary-key group, the stable property keeps them in secondary-key order from the previous sort. The insight is that “least first” is backwards from how most people think, which is why this is one of the most commonly confused sorting idioms.

A memory anchor that many people find helpful: think of the final sort as the “boss.” The boss has last say on the overall order. Every previous sort sets up a detail ordering that the boss preserves among elements it considers equal. The boss does not care about details among equals, so whatever order the earlier sort left them in, stability keeps it.

You can: state what stable means and cite Java’s guarantee for object-array sorts; apply the two-pass trick to sort by two keys; translate a two-pass sort into a single thenComparing chain; and explain from first principles why the secondary key must run first.


How It Works

Stability: the formal definition

A sort algorithm is stable when elements that are equal under the comparator maintain their original relative order. After a stable sort, if element a preceded element b in the input, and compare(a, b) == 0, then a still precedes b in the output.

Java’s guarantees (from the Java 25 Arrays.sort API):

This sort is guaranteed to be stable: equal elements will not be reordered as a result of the sort.

This applies to Arrays.sort(T[], Comparator) and Collections.sort/list.sort. It does NOT apply to Arrays.sort(int[]) and other primitive-array overloads, which use a different algorithm.

The two-pass trick: secondary before primary

To sort by team (primary) then by games played within each team (secondary):

players.sort(byGamesPlayed);   // secondary key: sort by games played ascending
players.sort(byTeam);          // primary key: sort by team alphabetically

Why secondary first:

After the first sort, players are in games-played order globally. After the second sort:

The primary sort dominates the outer order. The stable property preserves the secondary order within each primary group.

Quick check

Check your understanding

You want players sorted by team (primary) and by games played within each team (secondary). Which two-pass call sequence is correct?

Tier 2 · BJP (Reges and Stepp), Ch 13

The modern alternative: thenComparing

The two-pass trick requires two full sorts. The modern idiom composes both keys into one comparator:

players.sort(byTeam.thenComparing(byGamesPlayed));

This is a single pass. It sorts by team first, then by games played as a tiebreaker. The result is identical to the two-pass trick and is the preferred form for new code. Multi-key ordering with thenComparing covers this in depth.


Worked Example: Predict Then Check

Initial list:

Player Team GamesPlayed
Adams Bears 15
Brown Aces 10
Chen Bears 5
Davis Aces 20
Evans Bears 12
Ford Aces 8
players.sort(byGamesPlayed);   // ascending games played
players.sort(byTeam);          // alphabetical by team

Predict the final order (team, then within team).

Step-by-step

After byGamesPlayed sort (ascending): Chen(5), Ford(8), Brown(10), Evans(12), Adams(15), Davis(20).

After byTeam sort: Aces come before Bears. Within Aces: Ford, Brown, Davis (preserved from the games-played pass). Within Bears: Chen, Evans, Adams (also preserved).

Show answer
Final order Player Team GamesPlayed
1 Ford Aces 8
2 Brown Aces 10
3 Davis Aces 20
4 Chen Bears 5
5 Evans Bears 12
6 Adams Bears 15

The thenComparing equivalent:

players.sort(byTeam.thenComparing(byGamesPlayed));

produces the same result in one pass.


Common Misconceptions

Misconception 1: sorting by primary key first in a two-pass scheme

Wrong mental model: “Sort by team first, then by games played: the team grouping will persist.”

Why it breaks: The second sort (by games played) is the dominant sort. After it runs, elements are ordered globally by games played, not by team. The first sort’s team grouping is destroyed by the second pass wherever games-played values differ across teams.

How to correct: In the two-pass trick, secondary key first, primary key last. Or use thenComparing to avoid the trap entirely.


Misconception 2: assuming Arrays.sort(int[]) is stable

Wrong mental model: “All Java sort methods are documented as stable.”

Why it matters: Arrays.sort(int[]) uses dual-pivot quicksort, which is not stable. For primitive arrays, this rarely matters in practice (there is no “identity” to preserve for primitive values). But when students design a two-pass trick on a boxed Integer[], they need the object-array overload, not the primitive overload.

How to correct: Use Integer[] (object array) or List<Integer> when stability matters. Verify the stability guarantee from the javadoc for the specific overload.

Source: Java 25 Arrays.sort for int arrays javadoc (no stability guarantee mentioned).

Quick check

Check your understanding

A student writes players.sort(byTeam) then players.sort(byGamesPlayed), expecting the final list to be grouped by team with ascending games played within each team. What actually happens?

Tier 2 · BJP (Reges and Stepp), Ch 13


Formal Definition and Interface Contract

From the Java 25 Arrays.sort API:

This sort is guaranteed to be stable: equal elements will not be reordered as a result of the sort.

From the Java 25 Collections.sort API:

This sort is guaranteed to be stable: equal elements will not be reordered as a result of the sort.

Both use TimSort (introduced in Java 7), which achieves O(n log n) worst-case while maintaining stability.


Mental Model

Think of the two-pass trick as writing in pencil first, then in ink. Pass 1 writes the secondary ordering in pencil. Pass 2 writes the primary ordering in ink over the whole list. Where the ink says two elements are equal, the pencil order underneath is preserved (because the stable sort does not erase pencil when the ink says “same”). Where the ink says two elements differ, the ink dominates.


Connections

Within CSCD 210/211: The stable-sort guarantee of Arrays.sort was mentioned in passing in Arrays.sort with a Comparator. This lesson unpacks why stability matters and how it enables the two-pass trick.

Looking back: list.sort with a Comparator covered the sort call. This lesson covers a property of that sort that enables a specific multi-key idiom.

Looking ahead: Multi-key ordering with thenComparing covers thenComparing, which replaces the two-pass trick with a single composed comparator. That is the modern approach for all new code.


Practice

Level 1

Sort ArrayList<Player> players by last name (secondary), then by team (primary), using two passes. Write both sort calls.

Show answer
players.sort(Comparator.comparing(Player::getLastName));  // secondary key first
players.sort(Comparator.comparing(Player::getTeam));      // primary key last

After both calls: players are grouped by team; within each team, they are in last-name order.


Level 2

Rewrite the two-pass sort from Level 1 as a single list.sort call using thenComparing.

Show answer
players.sort(Comparator.comparing(Player::getTeam)
                       .thenComparing(Player::getLastName));

One sort, same result, clearer intent.


Level 3

Explain in 4-5 sentences why the secondary key must be sorted first in the two-pass trick, referencing the definition of stability. Then give a concrete counter-example showing what happens when the order is reversed.

Show answer

Explanation: Stability means that elements equal under the current comparator keep their previous relative order. In the two-pass trick, the final sort runs last and determines the outer order. For elements that are equal under the final comparator (same team), the stable sort leaves them in the order they were in at the start of that final pass. If the secondary-key sort ran first, those elements are already in secondary-key order when the final sort processes them, and stability preserves that order within each group. If the primary-key sort runs first, it creates team groups; but the second sort (by games played) then destroys the team grouping by reordering across team boundaries.

Counter-example (wrong order):

Initial: Adams(Bears,15), Brown(Aces,10), Chen(Bears,5)

Pass 1 (wrong: primary byTeam first): Adams(Bears,15), Chen(Bears,5), Brown(Aces,10)

Pass 2 (wrong: secondary byGamesPlayed second): Chen(Bears,5), Brown(Aces,10), Adams(Bears,15)

Final order: Chen, Brown, Adams. The players are sorted globally by games played; the team grouping is gone. Aces and Bears are interleaved, not grouped.


Go Deeper (optional)

This depth is not needed to write correct sorting code. Read on only if you are curious about where this fits in the bigger picture.

The two-pass trick shows up throughout professional codebases, particularly in code written before Java 8 introduced thenComparing. Reading legacy code and recognizing the pattern, then confidently suggesting a thenComparing replacement, is a concrete skill that appears in code reviews at every level of seniority.

Stability is also a genuine algorithmic property with a real history. Merge sort is inherently stable: its merge step always picks the left element when two elements are equal, preserving their relative order by construction. Quicksort is not inherently stable: its partition step can move an element past another equal element. TimSort, which Java uses for object-array sorts since Java 7, is a hybrid of insertion sort and merge sort, designed by Tim Peters to exploit the natural runs that appear in real-world data. The stability of TimSort comes from the merge-sort half of its design. This is why the primitive-array overload Arrays.sort(int[]) uses dual-pivot quicksort (fast, not documented as stable) while the object-array overload Arrays.sort(T[], Comparator) uses TimSort (stable, and fast on typical data).

If you continue into algorithms or systems work, you will see stability reappear as a design constraint: database sort operators, spreadsheet sort dialogs, and distributed data pipelines all depend on it in different ways. The core principle is always the same: when the ordering criterion says two things are equal, which one gets to keep its position?


Check Yourself

Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.

Check your understanding

Which of the following correctly defines a stable sort?

Tier 1 · BJP (Reges and Stepp), Ch 13

Which Java sort method is NOT documented as stable?

Tier 1 · Java SE 25 API: java.util.Arrays.sort

You want to sort a list of Player objects by team (primary key) and, within each team, by games played ascending (secondary key). Which two-pass call sequence is correct?

Tier 2 · BJP (Reges and Stepp), Ch 13

Trace the two-pass sort below and select the correct final order. Initial list: Brown | Aces | 10 Adams | Bears | 15 Davis | Aces | 20 Chen | Bears | 5 Code: players.sort(byGamesPlayed); // ascending players.sort(byTeam); // alphabetical

Tier 2 · BJP (Reges and Stepp), Ch 13

Which single statement is the modern replacement for the two-pass trick that sorts players by team and, as a tiebreaker, by games played?

Tier 3 · BJP (Reges and Stepp), Ch 13