DSA Tutorial
🔍

Stable vs Unstable Sorting

What Does Stability Mean?

A sorting algorithm is stable if elements with equal keys appear in the same relative order in the sorted output as they did in the input.

Input:  [(Alice, 30), (Bob, 25), (Charlie, 30), (Diana, 25)]
        Sort by age ascending.

STABLE output:
  [(Bob, 25), (Diana, 25), (Alice, 30), (Charlie, 30)]
   ↑          ↑            ↑            ↑
   Bob(25) before Diana(25) — original order preserved
   Alice(30) before Charlie(30) — original order preserved

UNSTABLE output (possible):
  [(Diana, 25), (Bob, 25), (Charlie, 30), (Alice, 30)]
   ↑            ↑           ↑             ↑
   Diana now before Bob — original order REVERSED for age=25
   Charlie now before Alice — original order REVERSED for age=30

Stability only matters when equal elements can be distinguished in some way beyond the sort key — by a secondary key, by position in a database, by insertion timestamp, or by any other tie-breaking criterion. If all elements are truly interchangeable, stability is irrelevant.

Why Stability Matters: Multi-Key Sorting

The most important use case for stability is sorting by multiple keys. The technique: sort by secondary key first (stable), then sort by primary key (stable). The secondary-key order is preserved for elements that tie on the primary key.

Records: [("Alice", HR, 60k), ("Bob", IT, 50k), ("Charlie", HR, 50k), ("Diana", IT, 60k)]

GOAL: Sort by department (primary), then by salary within department (secondary).

Step 1: Sort by salary (stable):
  [("Bob", IT, 50k), ("Charlie", HR, 50k), ("Alice", HR, 60k), ("Diana", IT, 60k)]
   Salary order preserved: Bob/Charlie(50k) before Alice/Diana(60k)

Step 2: Sort by department (stable):
  [("Charlie", HR, 50k), ("Alice", HR, 60k), ("Bob", IT, 50k), ("Diana", IT, 60k)]
   HR before IT.
   Within HR: Charlie(50k) before Alice(60k) — salary order preserved from step 1 ✓
   Within IT: Bob(50k) before Diana(60k)     — salary order preserved from step 1 ✓

With UNSTABLE sort in step 2:
  HR employees: could be [("Alice", HR, 60k), ("Charlie", HR, 50k)] — salary order lost!
  IT employees: could be [("Diana", IT, 60k), ("Bob", IT, 50k)]     — salary order lost!
Java
1import java.util.*; 2 3public class MultiKeySort { 4 5 record Employee(String name, String dept, int salary) {} 6 7 public static void main(String[] args) { 8 List<Employee> employees = Arrays.asList( 9 new Employee("Alice", "HR", 60_000), 10 new Employee("Bob", "IT", 50_000), 11 new Employee("Charlie", "HR", 50_000), 12 new Employee("Diana", "IT", 60_000) 13 ); 14 15 // Method 1: Two-pass stable sort (sort secondary first, then primary) 16 employees.sort(Comparator.comparingInt(Employee::salary)); // Step 1: by salary 17 employees.sort(Comparator.comparing(Employee::dept)); // Step 2: by dept (stable) 18 19 System.out.println("Two-pass result:"); 20 employees.forEach(e -> System.out.println(" " + e)); 21 // Charlie(HR,50k), Alice(HR,60k), Bob(IT,50k), Diana(IT,60k) ✓ 22 23 // Method 2: Single-pass with compound comparator 24 List<Employee> employees2 = Arrays.asList( 25 new Employee("Alice", "HR", 60_000), 26 new Employee("Bob", "IT", 50_000), 27 new Employee("Charlie", "HR", 50_000), 28 new Employee("Diana", "IT", 60_000) 29 ); 30 employees2.sort(Comparator.comparing(Employee::dept) 31 .thenComparingInt(Employee::salary)); 32 33 System.out.println("Compound comparator result:"); 34 employees2.forEach(e -> System.out.println(" " + e)); 35 // Same result — cleaner in production code 36 } 37}
Output (Java):
Charlie(HR,50k), Alice(HR,60k), Bob(IT,50k), Diana(IT,60k)

Which Algorithms Are Stable?

STABLE (equal elements never change relative order):
  Bubble Sort     — swaps only when arr[j] > arr[j+1], never on ==
  Insertion Sort  — shifts only when arr[j] > key, stops on ==
  Merge Sort      — merge uses <=, copies left before right on equal
  TimSort         — built on merge sort; stable by design
  Counting Sort   — fills output back-to-front preserving input order
  Radix Sort      — uses stable sort (counting sort) per digit pass

UNSTABLE (equal elements may change relative order):
  Selection Sort  — long-distance swap can jump over equal elements
  Quick Sort      — partition step can reorder equal elements
  Heap Sort       — heapify and sift-down can reorder equal elements
  Shell Sort      — long-distance swaps similar to selection sort

Why Each Algorithm Is or Is Not Stable

Bubble Sort — Stable

Condition: swap if arr[j] > arr[j+1]   (strictly greater)

Equal elements (arr[j] == arr[j+1]):
  Condition is FALSE → no swap
  Equal elements NEVER move past each other
  → STABLE ✓

If the condition were arr[j] >= arr[j+1]:
  Equal elements would be swapped → UNSTABLE

Insertion Sort — Stable

Condition: shift while arr[j] > key   (strictly greater)

Equal elements (arr[j] == key):
  Condition is FALSE → loop stops
  key is inserted AFTER all equal elements already in sorted prefix
  → STABLE ✓

Merge Sort — Stable

Merge condition: if left[i] <= right[j], copy left[i]

Equal elements (left[i] == right[j]):
  Condition is TRUE → copy from LEFT first
  Left elements appeared BEFORE right elements in the original array
  Left before right = original order preserved
  → STABLE ✓

If the condition were left[i] < right[j]:
  Equal elements: right copied first → original order reversed → UNSTABLE

Selection Sort — Unstable

Proof:
  Input:  [(B,3), (A,3), (C,1)]   sort by number
  Pass 1: minimum is (C,1) at index 2 → swap with (B,3) at index 0
  Result: [(C,1), (A,3), (B,3)]

  B was at index 0 originally. After swap, B is at index 2.
  A was at index 1 originally. A stays at index 1.
  B jumped PAST A without any comparison between them.
  Original order of (B,3) and (A,3): B before A.
  Final order: A before B.
  → UNSTABLE ✗

Root cause: non-adjacent swaps can jump equal elements to new positions.

Quick Sort — Unstable

Proof:
  Input:  [(A,3), (B,3), (C,3)]   all equal, sort by number
  Pivot = (C,3) (last element), Lomuto partition:

  j=0: arr[0]=(A,3) <= (C,3) → i++, swap arr[0] with arr[0] → no change
  j=1: arr[1]=(B,3) <= (C,3) → i++, swap arr[1] with arr[1] → no change
  Place pivot: swap arr[2] with arr[2] → no change

  Here the result happens to be stable — but consider:
  Input: [(B,1), (A,1), (C,2)]   pivot = (C,2)

  j=0: (B,1) <= 2 → i=0, swap arr[0]↔arr[0] → no change
  j=1: (A,1) <= 2 → i=1, swap arr[1]↔arr[1] → no change
  Place pivot at index 2 → [(B,1),(A,1),(C,2)]  stable here

  More revealing:
  Input: [(A,2), (B,1), (C,2)]   pivot = (C,2)
  j=0: (A,2) <= 2 → i=0, swap arr[0]↔arr[0]
  j=1: (B,1) <= 2 → i=1, swap arr[1]↔arr[1]
  Place pivot: swap arr[2]↔arr[2] → [(A,2),(B,1),(C,2)]

  Input: [(C,2), (A,2), (B,1)]   pivot = (B,1)
  j=0: (C,2) > 1 → skip
  j=1: (A,2) > 1 → skip
  Place pivot: swap arr[0]↔arr[2] → [(B,1),(A,2),(C,2)]
  C was at index 0, A at index 1. Now C(2) is at index 2, A(2) at index 1.
  Both have key=2. C now comes AFTER A — reversed from input where C was before A.
  → UNSTABLE ✗

Heap Sort — Unstable

Heap sort builds a max-heap, then repeatedly extracts the maximum.
During heapify, the sift-down process compares children and swaps the
larger child upward. Two equal elements may be in parent-child relationship,
and sift-down may swap them — reordering equal elements.

Proof:
  Input: [(A,5), (B,5), (C,3)]   sort by number descending then ascending
  Build max-heap: (A,5) may go to root, (B,5) as child — or vice versa.
  Extraction order depends on heap structure, not insertion order.
  → UNSTABLE ✗

How to Make Any Sort Stable

Method 1: Compound Sort Key (Add Original Index)

Extend the comparison key to include the element's original index as a tiebreaker. Elements with equal primary keys are then ordered by index — which preserves the original order.

Java
1import java.util.*; 2 3public class MakeStable { 4 5 public static void main(String[] args) { 6 // Original array with duplicates 7 int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5}; 8 9 // Wrap elements with their original index 10 Integer[][] indexed = new Integer[arr.length][2]; 11 for (int i = 0; i < arr.length; i++) { 12 indexed[i][0] = arr[i]; // value 13 indexed[i][1] = i; // original index 14 } 15 16 // Sort by value, then by original index on tie (makes any comparison stable) 17 Arrays.sort(indexed, (a, b) -> a[0].equals(b[0]) ? a[1] - b[1] : a[0] - b[0]); 18 19 System.out.print("Stable result: "); 20 for (Integer[] pair : indexed) System.out.print(pair[0] + "(idx" + pair[1] + ") "); 21 System.out.println(); 22 // 1(idx1) 1(idx3) 2(idx6) 3(idx0) 4(idx2) 5(idx4) 5(idx8) 6(idx7) 9(idx5) 23 // Both 1s appear in original order (idx1 before idx3) ✓ 24 // Both 5s appear in original order (idx4 before idx8) ✓ 25 } 26}
Output (Java):
Stable result: 1(idx1) 1(idx3) 2(idx6) 3(idx0) 4(idx2) 5(idx4) 5(idx8) 6(idx7) 9(idx5)

Method 2: Use a Stable Sort Directly

The cleanest approach: just use a sort that is already stable.

Language          Stable sort to use
──────────────────────────────────────────────────────────────────────
Java (objects)    Collections.sort(), List.sort(), Arrays.sort(Object[])
                  → all use TimSort — STABLE
Java (primitives) Arrays.sort(int[]) uses Dual-Pivot QuickSort — NOT STABLE
                  → if stability needed, use Integer[] and Arrays.sort(Integer[])

Python            list.sort() and sorted() → TimSort — STABLE (always)

C++               std::stable_sort() → guaranteed stable
                  std::sort() → NOT guaranteed stable (use stable_sort when needed)

JavaScript        Array.prototype.sort → STABLE since ECMAScript 2019
                  (V8 ≥ 7.0, SpiderMonkey always stable, before ES2019 not guaranteed)

How to Test Stability

Stability test:
1. Create an array of objects where multiple elements share the same sort key
2. Record the original positions of equal-key elements
3. Sort with the algorithm under test
4. Check: do equal-key elements appear in their original relative order?

Test case:
  Input: [(B,1), (A,1), (D,2), (C,1)]   sort by number
  Equal-key elements (key=1): B at idx 0, A at idx 1, C at idx 3
  Expected stable output: B before A before C   (original order 0,1,3)

  Stable result:   [(B,1),(A,1),(C,1),(D,2)] ✓
  Unstable result: [(A,1),(C,1),(B,1),(D,2)] ← B jumped to index 2, after A and C
Java
1import java.util.*; 2 3public class StabilityTest { 4 5 static boolean isStable(int[] arr, Comparator<Integer[]> sort) { 6 int n = arr.length; 7 Integer[][] indexed = new Integer[n][2]; 8 for (int i = 0; i < n; i++) { indexed[i][0] = arr[i]; indexed[i][1] = i; } 9 10 Arrays.sort(indexed, sort); 11 12 // Check: for equal values, indices must be increasing 13 for (int i = 0; i < n - 1; i++) { 14 if (indexed[i][0].equals(indexed[i+1][0])) { 15 if (indexed[i][1] > indexed[i+1][1]) return false; 16 } 17 } 18 return true; 19 } 20 21 public static void main(String[] args) { 22 int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3}; 23 24 // Test Arrays.sort on Integer[] — TimSort — should be stable 25 boolean stable = isStable(arr, (a, b) -> a[0] - b[0]); 26 System.out.println("Arrays.sort on Integer[] (TimSort) stable: " + stable); 27 // true 28 29 // Manual unstable sort (simulating selection sort) 30 boolean unstable = isStable(arr, (a, b) -> { 31 // Simulate unstable: ignore original index intentionally 32 return a[0] - b[0]; // This alone doesn't make it unstable 33 // Real test: run actual unstable algorithm and check 34 }); 35 System.out.println("Stability test result: " + unstable); 36 } 37}

Stability in Real-World Scenarios

1. Database query results
   SELECT * FROM orders ORDER BY customer_id
   Then additionally sort by date — stable sort preserves customer_id
   ordering within the same date.

2. Spreadsheet column sorting
   User sorts by "Region" column. Then sorts by "Revenue" column.
   Stable sort: within each revenue value, regions stay in alphabetical
   order from the previous sort.
   Unstable sort: alphabetical region order is destroyed on the second sort.

3. Version control file listing
   Files listed alphabetically, then sorted by modification date.
   Stable: files modified on the same date remain alphabetical.
   Unstable: same-date files appear in random order.

4. Leaderboard with ties
   Players sorted by score. Ties broken by join date (earlier = higher rank).
   Two-pass stable sort: sort by join date first, then by score.
   Stable: tied-score players appear in join-date order.
   Unstable: tie-breaking is lost.

5. Counting sort and Radix sort
   Radix sort processes digit by digit — it MUST use a stable sort
   for each digit pass. If the digit-level sort is unstable, the
   ordering from previous passes is destroyed. This is why counting sort
   (which is naturally stable when implemented correctly) is used.

Stability Across Languages

Java:
  Arrays.sort(Object[])     → TimSort    → STABLE ✓
  Arrays.sort(int[])        → DualPivotQS → NOT STABLE ✗
  Collections.sort(List)    → TimSort    → STABLE ✓
  List.sort(comparator)     → TimSort    → STABLE ✓

  To sort int[] stably: convert to Integer[] first:
    Integer[] boxed = Arrays.stream(arr).boxed().toArray(Integer[]::new);
    Arrays.sort(boxed, comparator);

Python:
  list.sort()   → TimSort → STABLE ✓ (since Python 2.2, always)
  sorted()      → TimSort → STABLE ✓

C++:
  std::sort          → IntroSort      → NOT GUARANTEED STABLE ✗
  std::stable_sort   → MergeSort      → STABLE ✓
  std::sort with full compound comparator → effectively stable

JavaScript:
  Array.prototype.sort → TimSort in V8 → STABLE ✓ (since ES2019)
  Before ES2019: V8 used unstable sort for arrays > 10 elements.
  Node.js ≥ 11 (V8 7.0+): stable.
  Chrome ≥ 70: stable.
  All modern browsers: stable.

Summary Table

AlgorithmStableWhy
Bubble Sort✓ YesSwaps only on > — equal elements never moved past each other
Insertion Sort✓ YesShifts only on > — equal elements stop the shift
Merge Sort✓ YesMerge uses <= — left half copied before right on ties
TimSort✓ YesBuilt on merge sort — stable by design
Counting Sort✓ YesFills back-to-front with decremented counts
Radix Sort✓ YesUses stable counting sort at each digit level
Selection Sort✗ NoLong-distance swap can skip over equal elements
Quick Sort✗ NoPartition step can reorder equal elements
Heap Sort✗ NoHeapify reorders equal elements during sift-down
Shell Sort✗ NoLong-gap swaps can skip over equal elements

Common Mistakes

Using Arrays.sort(int[]) expecting stability in Java. Primitive array sort uses Dual-Pivot QuickSort — unstable. If you are sorting int[] values that correspond to records and stability matters, box them to Integer[] first, then sort with a comparator.

Assuming JavaScript's Array.sort() is always stable in legacy environments. Before ECMAScript 2019, V8 (used in Node.js and Chrome) used an unstable insertion sort for short arrays (≤ 10 elements) and an unstable QuickSort for longer arrays. Code running on older browsers or older Node.js versions cannot rely on sort() being stable. Check your target environment or add an index tiebreaker.

Using < instead of <= in a custom merge implementation. The stability of merge sort depends entirely on this one character. if (left[i] < right[j]) takes the right element first on ties — reversing equal elements. Always use <= (left before right on ties) in a stable merge.

Not recognising that two-pass sorting requires a stable intermediate sort. The technique "sort by secondary key, then sort by primary key" only works if the second sort is stable. If the second sort is unstable, the secondary-key ordering for equal primary-key elements is lost. The second sort must be stable.

Interview Questions

Q: What is the difference between a stable and an unstable sort, and name two of each?

A stable sort preserves the original relative order of equal-key elements. An unstable sort may reorder them. Stable: Merge Sort, Insertion Sort, TimSort, Bubble Sort. Unstable: Quick Sort, Selection Sort, Heap Sort.

Q: Why does it matter whether a sort is stable when sorting database records?

Database applications frequently sort by one column and then by another. If the second sort is unstable, the ordering from the first sort is destroyed for equal primary-key values. For example: sort employees by name (stable), then sort by department (unstable). The result may interleave employees with the same department in random order rather than alphabetical name order within each department.

Q: How does the two-pass stable sort work and why does it require stability?

To sort by (department, salary): first sort by salary (secondary key, stable), then sort by department (primary key, must be stable). After step 2, equal-department employees are in the order established by step 1 (salary order). If step 2 were unstable, equal-department employees could appear in any order — the salary ordering would be lost.

Summary

Stability means equal elements keep their original relative order after sorting. This matters whenever equal elements are distinguishable — by a secondary key, position, timestamp, or other criterion.

Stable sorts: Bubble Sort (never swap on equal), Insertion Sort (never shift on equal), Merge Sort (<= in merge copies left first), TimSort (built on merge sort), Counting Sort, Radix Sort.

Unstable sorts: Selection Sort (long-distance swap skips equal elements), Quick Sort (partition reorders equal elements), Heap Sort (heapify reorders equal elements).

Language defaults:

  • Python list.sort() / sorted() → always stable (TimSort since Python 2.2)
  • Java Arrays.sort(Object[]), Collections.sort() → stable (TimSort)
  • Java Arrays.sort(int[]) → NOT stable (Dual-Pivot QuickSort)
  • C++ std::sort → NOT guaranteed stable; std::stable_sort → stable
  • JavaScript Array.sort() → stable (ECMAScript 2019+, modern V8/SpiderMonkey)

Two-pass multi-key sort: sort by secondary key first (stable), then by primary key (stable). Works because the second sort preserves the first sort's ordering within equal primary keys.

Making any sort stable: add the original index as a tiebreaker in the comparator — compare(a, b) = primaryKey(a) - primaryKey(b) || originalIndex(a) - originalIndex(b).

In the next topic, you will explore Sorting Comparison — a comprehensive side-by-side analysis of all sorting algorithms with benchmarks, use-case guidance, and the definitive decision framework.

Suggested Quiz

A stable sort guarantees that equal elements appear in the output in the same relative order as the input. Which scenario makes this matter?

1/6
Stable vs Unstable Sorting