C# Sort and Custom Comparators: The Coding Interview Reference

May 29, 20269 min read
dsaalgorithmsinterview-prepdata-structures
C# Sort and Custom Comparators: The Coding Interview Reference
TL;DR
  • Array.Sort with a lambda is the fastest in-place sort; always use a.CompareTo(b) over a - b to avoid silent integer overflow with int.MinValue.
  • IComparer<T> is required when a method signature demands a comparator object; Comparer<T>.Create() wraps a lambda without a full class definition.
  • LINQ OrderBy/ThenBy produces a stable sort on any IEnumerable<T>; chaining two OrderBy calls silently discards the first sort.
  • Array.Sort and List<T>.Sort use introsort and are not stable; encode all sort keys into a single comparator or switch to LINQ for stability.
  • The subtraction trick (b - a) compiles without complaint and corrupts silently for extreme values; .CompareTo() has no overflow path.
  • Null in CompareTo throws NullReferenceException; use Comparer<T>.Default.Compare(a, b) when reference types may be null.

You're twenty minutes into the problem. The approach is solid. You write this to sort an array of integers descending:

Array.Sort(arr, (a, b) => b - a);

It passes every test case you try. You submit. Wrong answer. The hidden test has int.MinValue in it, and b - a just overflowed to a large positive number, telling the comparator the wrong element is bigger.

That one-liner costs more interview offers than any algorithmic mistake. Not because it's hard to get right. Because it looks right. It compiles. It runs. It handles your five test cases without blinking. And then int.MinValue walks in and your sort silently corrupts.

This C# sort and custom comparator reference covers every sorting mechanism, how to write comparators that don't blow up, and the patterns you'll reach for most often under time pressure.

Three Tools, One Right Answer Per Situation

C# gives you three distinct tools. Pick the wrong one and the code still compiles, which is the worst kind of wrong.

MechanismWhere you use itReturns
Comparison<T> delegate (lambda)Array.Sort, List<T>.SortIn-place (void)
IComparer<T> interfaceArray.Sort, List<T>.Sort, SortedSet, PriorityQueueIn-place (void)
LINQ OrderBy / ThenByAny IEnumerable<T>New IOrderedEnumerable<T>

The lambda wins for quick one-off sorts. IComparer<T> is what you need when a method signature demands an object. LINQ handles multi-key sorts readably, at the cost of allocating a new sequence.

The C# Sort Custom Comparator Lambda You'll Write Every Time

This is the form you'll reach for in almost every interview. Pass a lambda directly to Array.Sort or List<T>.Sort:

int[] nums = { 5, 2, 8, 1, 9 }; // Ascending (default) Array.Sort(nums, (a, b) => a.CompareTo(b)); // Descending Array.Sort(nums, (a, b) => b.CompareTo(a));

The contract: return negative if a should come before b, zero if equal, positive if a should come after b. The runtime only checks the sign. It does not check whether your arithmetic overflowed.

Sorting a jagged array by first element:

int[][] intervals = { new[]{3,6}, new[]{1,5}, new[]{2,4} }; Array.Sort(intervals, (a, b) => a[0].CompareTo(b[0]));

Sorting strings by length, then alphabetically for ties:

string[] words = { "banana", "fig", "apple", "kiwi" }; Array.Sort(words, (a, b) => { int cmp = a.Length.CompareTo(b.Length); return cmp != 0 ? cmp : string.Compare(a, b, StringComparison.Ordinal); });

The same lambda works with List<T>.Sort:

var list = new List<int> { 5, 2, 8, 1 }; list.Sort((a, b) => b.CompareTo(a)); // descending

a - b Will Cost You the Offer

Never use a - b or b - a as a comparator return value for integers. This is the most common sort bug in interviews, and it compiles without complaint. The interview prep graveyard is full of b - a.

// WRONG - overflows for large positive vs large negative Array.Sort(arr, (a, b) => a - b); // CORRECT Array.Sort(arr, (a, b) => a.CompareTo(b));

Why it breaks: if a = int.MinValue and b = 1, then a - b underflows to int.MaxValue in C#'s default unchecked arithmetic. The comparator now says a > b, but a is much smaller. The sort corrupts silently.

int.CompareTo(int) has no overflow path. Use it everywhere.

A triumphant soldier labeled "My unit tests" stands unscathed while a figure labeled "The actual use case" gets bombarded with bullets, the perfect picture of a comparator that passed five test cases and failed the sixth

Your unit tests after the comparator handles {1, 2, 3}. The actual use case when int.MinValue shows up.

When You Need an Object, Not a Lambda

When you need to pass a comparator as an object (to SortedSet<T>, PriorityQueue<TElement, TPriority>, or any method that takes IComparer<T>), write a class:

class ByAbsoluteValue : IComparer<int> { public int Compare(int x, int y) => Math.Abs(x).CompareTo(Math.Abs(y)); } var set = new SortedSet<int>(new ByAbsoluteValue());

For throwaway comparators, Comparer<T>.Create() saves you the class declaration:

IComparer<int[]> byStart = Comparer<int[]>.Create((a, b) => a[0].CompareTo(b[0])); Array.Sort(intervals, byStart);

Comparer<T>.Create() wraps a lambda into IComparer<T> without a class. Available from .NET 4.5 onward, which covers every competitive coding platform you'll encounter. Write the class when you need to name the concept and reuse it. Skip it when you just need to sort some intervals and move on.

Making Your Type Sort Itself

When you define a custom class and want it sortable by default, implement IComparable<T>:

class Point : IComparable<Point> { public int X, Y; public int CompareTo(Point other) { int cmp = X.CompareTo(other.X); return cmp != 0 ? cmp : Y.CompareTo(other.Y); } } var points = new List<Point> { ... }; points.Sort(); // uses CompareTo

In interviews you rarely define custom classes, but knowing how to layer this with IComparer<T> (which overrides the default) is worth understanding.

LINQ Is Stable. Array.Sort Is Not.

LINQ sorts are stable. If two elements compare equal on your key, their original relative order is preserved. Array.Sort and List<T>.Sort are not. They use introsort, which is fast, cache-friendly, and completely indifferent to your feelings about insertion order.

var sorted = nums.OrderBy(x => x); // ascending var desc = nums.OrderByDescending(x => x); // descending

Multi-key sorting is where LINQ earns its place. Use ThenBy for secondary keys:

var result = people .OrderBy(p => p.LastName) .ThenBy(p => p.FirstName) .ThenByDescending(p => p.Age) .ToList();

Do not chain OrderBy calls. seq.OrderBy(x => x.A).OrderBy(x => x.B) applies two independent sorts and the second throws away the first. Only ThenBy builds a multi-key sort. This bug is silent. The code compiles. The output is sorted. Just sorted by the wrong thing, which you may not discover until the follow-up question.

LINQ returns IOrderedEnumerable<T>. Call .ToList() or .ToArray() to materialize it. Until you do, the sort runs lazily on each enumeration.

You can pass a custom comparer into OrderBy too:

var sorted = words.OrderBy(w => w, StringComparer.OrdinalIgnoreCase);

StringComparer.Ordinal is the right choice for identifiers and keys. In interviews, it's almost always what you want.

When Stability Actually Matters

Stable sort is essential when you sort by one key and then need a second sort to break ties without disturbing the first order. LINQ handles this transparently. With Array.Sort, you encode all keys into a single comparator.

Sorting tasks by priority, with equal-priority tasks in arrival order:

// LINQ: clean, stable var result = tasks.OrderBy(t => t.Priority).ThenBy(t => t.ArrivalIndex).ToList(); // Array.Sort equivalent: encode both keys tasks.Sort((a, b) => { int cmp = a.Priority.CompareTo(b.Priority); return cmp != 0 ? cmp : a.ArrivalIndex.CompareTo(b.ArrivalIndex); });

If you ever want to multi-pass sort (sort by B, then re-sort by A and trust equal-A items stay in B order), only LINQ gives you that guarantee for free.

The Three Patterns You'll Actually Use

Sort by frequency descending, then by value ascending:

var freq = new Dictionary<int, int>(); foreach (int x in nums) freq[x] = freq.GetValueOrDefault(x) + 1; int[] sorted = nums .OrderByDescending(x => freq[x]) .ThenBy(x => x) .Distinct() .ToArray();

Sort intervals by start, break ties by end descending:

intervals.Sort((a, b) => { int cmp = a[0].CompareTo(b[0]); return cmp != 0 ? cmp : b[1].CompareTo(a[1]); });

K closest points to origin:

points.Sort((a, b) => { long da = (long)a[0]*a[0] + (long)a[1]*a[1]; long db = (long)b[0]*b[0] + (long)b[1]*b[1]; return da.CompareTo(db); }); return points.Take(k).ToArray();

Note the long cast. a[0]*a[0] for coordinates up to 10^4 gives 10^8, which fits in int. If constraints allow 10^5, the product overflows and your K-closest problem silently returns the wrong points. Getting into the habit of casting to long before squaring costs nothing. Silent wrong answers cost interviews.

Quick Reference

GoalHow
Sort ascending in-placearr.Sort() or Array.Sort(arr)
Sort descending in-placeArray.Sort(arr, (a, b) => b.CompareTo(a))
Custom key, in-placelist.Sort((a, b) => key(a).CompareTo(key(b)))
Multi-key, stableseq.OrderBy(x => x.A).ThenBy(x => x.B)
IComparer from lambdaComparer<T>.Create((a, b) => ...)
Case-insensitive string sortStringComparer.OrdinalIgnoreCase
Reverse an array after sortingArray.Reverse(arr)

Array.Reverse is a clean alternative to writing a descending comparator when no custom key is needed. O(n) and readable.

Four Bugs You'll Regret in Real Time

Every one of these compiles clean. That's what makes them fun.

Subtraction overflow. Covered above. Use CompareTo, never -.

Unstable in-place sort. Array.Sort and List<T>.Sort use introsort, which is not stable. If you need equal elements to preserve insertion order, use LINQ or encode the original index into your comparator. Discovering this bug during a follow-up question is the sort of thing that sticks with you.

Chained OrderBy discards earlier sorts. seq.OrderBy(x => x.A).OrderBy(x => x.B) gives you a sort by B only. Use ThenBy.

Null in CompareTo. Calling a.CompareTo(b) when a is null throws a NullReferenceException. That's not a wrong answer. That's a crash mid-interview. When comparing nullable or reference types, guard explicitly or use Comparer<T>.Default.Compare(a, b), which treats null as less than any non-null value.

Nobody Fails on the Comparator. They Fail on the Narration.

Sorting problems almost always have a follow-up: "now sort by a different key," or "what if there are duplicates?" Explaining your comparator choice out loud while you code it is the part that separates candidates. Writing a.CompareTo(b) in silence proves you know the syntax. Saying "I'm using CompareTo instead of subtraction because the constraints allow negative integers and subtraction overflows" tells the interviewer you understand what you're doing and why.

If you want to practice that narration under real interview pressure, SpaceComplexity runs voice-based mock sessions where the interviewer pushes on exactly this kind of tradeoff live.

For more on C#'s collection tools in a competitive context, see C# for Coding Interviews. If you're coming from Java, Java Sort and Custom Comparators shows the equivalent patterns. And if the interview involves sorting by multiple passes, Stable Sort: The Property That Makes Multi-Key Sorting Actually Work is worth a read before you go in.

Further Reading