Stable Sort and the Two-Pass Trick
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:
- Players are grouped by team.
- Within each team group, the team comparator returns 0 for all members, so their relative order is unchanged.
- “Unchanged” means they remain in the order they were in at the start of the second sort: games-played ascending.
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?
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?
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?
Which Java sort method is NOT documented as stable?
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?
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
Which single statement is the modern replacement for the two-pass trick that sorts players by team and, as a tiebreaker, by games played?