<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Searching and sorting
All lessons in this course

Lesson 18 of 50 14 minIntermediate

Sorting in .NET: stability and comparers

Choose between Array.Sort, List.Sort and LINQ OrderBy, sort by several keys, write correct comparers, and avoid the classic sorting bugs.

In real code you will almost never write your own sort. .NET’s built-in sorts are fast and well tested. What you do need to get right is which method to call, how to describe the order you want, and whether equal items may be reordered.

MethodAlgorithmChanges the original?Stable?
Array.Sort(a), list.Sort(), span.Sort()IntrosortYes, sorts in placeNo
items.OrderBy(...), ThenBy(...) (LINQ)Stable sort of keysNo, returns a new sequenceYes
items.Order() (.NET 7+)Same as OrderBy with the item itself as keyNoYes

Stability matters

A stable sort keeps items with equal keys in their original order. That matters when the input already has a meaningful order, such as beams listed in erection sequence that you now want grouped by profile, or when you sort by one key and then by another.

Interactive

Sort keys and stability

The # column is each beam’s original position. A stable sort keeps that order among beams with equal keys.

Sort by
Then by
Algorithm
#NameProfileLengthLevel
2B2HEA2004.5L2
4B4HEA2006.0L1
7B7HEA2003.0L1
1B1IPE3006.0L1
3B3IPE3004.5L1
5B5IPE3003.0L2
8B8IPE3006.0L2
6B6UB4064.5L2
// Stable, returns a new sequence
var sorted = beams.OrderBy(b => b.Profile)
     .ToList();

// In place, NOT guaranteed stable
list.Sort((x, y) =>
{
    return x.Profile.CompareTo(y.Profile);
});
Stable: within each group of equal keys, the beams keep their original order (look at the # column).

Sorting by several keys

MultiKey.cs
var beams = new List<Beam>{    new("B1", "IPE300", 6.0), new("B2", "HEA200", 4.5),    new("B3", "IPE300", 4.5), new("B4", "HEA200", 6.0)}; // LINQ: readable, stable, returns a new listvar byProfileThenLongest = beams    .OrderBy(b => b.Profile)    .ThenByDescending(b => b.Length)    .ToList(); foreach (var b in byProfileThenLongest)    Console.WriteLine($"{b.Name} {b.Profile} {b.Length:0.0}"); // In place with a Comparison<T> lambda (no new list)beams.Sort((x, y) =>{    int c = string.Compare(x.Profile, y.Profile, StringComparison.Ordinal);    return c != 0 ? c : y.Length.CompareTo(x.Length);   // y before x = descending}); record Beam(string Name, string Profile, double Length);
Output
B4 HEA200 6.0
B2 HEA200 4.5
B1 IPE300 6.0
B3 IPE300 4.5

Three ways to define an order

  • `IComparable<T>` on the type itself: its natural order. int, string, DateTime already have one. Then list.Sort() needs no arguments.
  • `IComparer<T>`: a separate object describing an order. Useful when there are several orders, or you cannot change the type. Comparer<T>.Create(lambda) makes one from a lambda.
  • `Comparison<T>`: a plain lambda (x, y) => int, accepted by List<T>.Sort and Array.Sort.
Comparable.cs
var levels = new List<Level> { new("Roof", 12.0), new("Ground", 0.0), new("First", 4.0) };levels.Sort();                                         // uses CompareToConsole.WriteLine(string.Join(", ", levels.Select(l => l.Name))); class Level : IComparable<Level>{    public string Name { get; }    public double Elevation { get; }    public Level(string name, double elevation) => (Name, Elevation) = (name, elevation);     // negative: this comes first, 0: equal, positive: other comes first    public int CompareTo(Level? other) => other is null ? 1 : Elevation.CompareTo(other.Elevation);}
Output
Ground, First, Roof

Classic sorting bugs

1. Subtracting to compare. (x, y) => x - y looks clever but overflows: for int.MinValue and 1 the result wraps around to a large positive number, so the order is wrong. Always use x.CompareTo(y).

2. Text order surprises. Strings are compared character by character, so "B10" comes before "B2" (because 1 < 2). The default comparer is also culture-sensitive. For IDs and file names, decide explicitly:

StringOrder.cs
var names = new List<string> { "apple", "Banana", "cherry" }; names.Sort(StringComparer.Ordinal);            // by character code: capitals firstConsole.WriteLine(string.Join(" ", names)); names.Sort(StringComparer.OrdinalIgnoreCase);  // ignore case, culture-independentConsole.WriteLine(string.Join(" ", names)); var marks = new List<string> { "B10", "B2", "B1" };marks.Sort(StringComparer.Ordinal);Console.WriteLine(string.Join(" ", marks));    // text order, not number order
Output
Banana apple cherry
apple Banana cherry
B1 B10 B2

3. Inconsistent comparers. A comparer must be consistent: if a < b and b < c then a < c, and comparing an item with itself gives 0. A comparer that uses random numbers or rounds values unevenly can make Sort throw InvalidOperationException (“IComparer.Compare() method returns inconsistent results”) or produce garbage.

4. Sorting what you only need partly. If you need only the 10 longest beams out of a million, sorting everything is O(n log n). A PriorityQueue of size 10 does it in O(n log 10), see the heaps lesson.

Quick check

You need beams grouped by level but, within each level, still in their original erection order. Which call is guaranteed correct?

Quick check

Why is list.Sort((x, y) => x - y) dangerous for ints?

Practice

Drawing numbers look like "A-10", "A-2", "B-1", "A-1". Sort them by the letter, then by the number (so A-2 comes before A-10). Expected: A-1, A-2, A-10, B-1.

Show a hint

Split on -. Order by the first part, then by int.Parse of the second part.

Show the solution
Solution.cs
var drawings = new List<string> { "A-10", "A-2", "B-1", "A-1" }; var sorted = drawings    .OrderBy(d => d.Split('-')[0], StringComparer.Ordinal)    .ThenBy(d => int.Parse(d.Split('-')[1]))    .ToList(); Console.WriteLine(string.Join(", ", sorted));
Output
A-1, A-2, A-10, B-1

Remember

  • Array.Sort / List.Sort use introsort: fast, in place, but not stable.
  • LINQ OrderBy / ThenBy is stable and returns a new sequence.
  • Define order with IComparable<T>, IComparer<T> or a Comparison<T> lambda, using CompareTo, never subtraction.
  • Be explicit about string order (StringComparer.Ordinal, OrdinalIgnoreCase) and sort numbers inside IDs as numbers.
0 of 50 lessons completed