Start learning free

LESSON 566 OF 572 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Sort Your Own Objects in C#: IComparable and CompareTo

Practice O(n log n) Read the full explanation

A language can sort numbers and strings on its own: it already knows which of two of them comes first. For objects of your own class it doesn't, until the class tells it. Every language has a way for a class to say how two of its objects compare. Once a class has it, the language's own sort can sort a list of those objects, with no comparison written at the call.

Write:

  • a class Student, created by Student(name, grade), that can compare itself with another student: a student comes first if their grade is higher; when two grades are equal, the name that comes first in alphabetical order (A to Z) comes first;
  • a method RankStudents(students) in the class Solution, which sorts the students with your language's own sort, using that comparison, and returns their names in that order.

In C#, Student implements IComparable<Student>: its method CompareTo(other) returns a negative number when this student comes first, a positive number when other does, and 0 when they are equal. A list's Sort() uses it.

Don't write your own sorting loop. The program also checks your comparison directly: for each two neighbours in the list you return, it asks Student whether the first one comes before the second.

 

Example 1:

Input: students = [("Ana", 90), ("Bob", 85), ("Cid", 90)]
Output: Ana Cid Bob
Explanation: Ana and Cid have the highest grade, 90. They tie, so their names decide: Ana comes before Cid.
Bob, with 85, comes last.

Example 2:

Input: students = [("Eve", 55), ("Dan", 100), ("Bea", 0), ("Cal", 99)]
Output: Dan Cal Eve Bea
Explanation: All the grades are different, so only the grades decide, from 100 down to 0.

Example 3:

Input: students = [("Max", 88), ("Leo", 88), ("Anna", 88), ("Ann", 88)]
Output: Ann Anna Leo Max
Explanation: Everyone has the same grade, so the list is in alphabetical order.
"Ann" comes before "Anna": when one name is the start of the other, the shorter one comes first.

 

Constraints:

  • There are 1 to 100 students.
  • 0 <= grade <= 100
  • A name has 2 to 10 letters: a capital letter, then lowercase letters. No two students have the same name.
Note:

The language's sort takes O(n log n) comparisons for n students, and each comparison takes constant time.

STEP 1 OF 9
    Ctrl + Enter to check

    C# · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Sort Your Own Objects in C#, explained

    Problem overview

    A list of numbers or strings sorts itself, because C# knows which of two of them comes first. For objects of your own class it does not, until the class says so. Here the class is Student, created with Student(name, grade). A student comes first when the grade is higher; when two grades are equal, the name that comes first alphabetically comes first. RankStudents(students) sorts the list with its own Sort() and returns the names in that order.

    For Ana 90, Bob 85 and Cid 90 the result is Ana Cid Bob: Ana and Cid tie on 90 and Ana's name comes first, and Bob is last.

    Approach: teach the class to compare itself

    C#'s List<T>.Sort() with no arguments looks for the interface IComparable<T>. The interface has one method, CompareTo(other), which returns:

    • a negative number if this object comes first;
    • a positive number if other comes first;
    • 0 if they are equal.

    Once Student says : IComparable<Student> and provides that method, no sorting loop and no comparison at the call is needed.

    Step by step

    1. Give Student two public fields, string name and int grade, and store the constructor's parameters in them with this..
    2. In CompareTo, if the grades differ, return other.grade - grade. Higher grades must come first, and when this student's grade is bigger the subtraction is negative, which means "this one first".
    3. If the grades are equal, return name.CompareTo(other.name). Strings already compare alphabetically, with a negative result when name comes first.
    4. In RankStudents, call students.Sort(), create a List<string>, add each sorted student's name in a foreach loop and return it.

    The solution in C#

    class Student : IComparable<Student> {
        public string name;
        public int grade;
    
        public Student(string name, int grade) {
            this.name = name;
            this.grade = grade;
        }
    
        public int CompareTo(Student other) {
            if (grade != other.grade) {
                return other.grade - grade;
            }
            return name.CompareTo(other.name);
        }
    }
    
    class Solution {
        public List<string> RankStudents(List<Student> students) {
            students.Sort();
            List<string> names = new List<string>();
            foreach (Student student in students) {
                names.Add(student.name);
            }
            return names;
        }
    }

    Details worth knowing

    The sign is what matters. Sort() only looks at whether the result is negative, zero or positive. Comparing Ana (90) with Bob (85) returns -5, comparing Bob with Ana returns 5, and comparing two students with the same name and grade returns 0.

    The order of the subtraction is the whole trick. other.grade - grade sorts from high to low. Writing grade - other.grade would put the lowest grade first. For names, the order is the natural one, name.CompareTo(other.name), A to Z.

    Prefixes. "Ann".CompareTo("Anna") is negative, so when one name is the start of the other, the shorter name comes first. Four students with grade 88 named Max, Leo, Anna and Ann come out as Ann Anna Leo Max.

    Subtracting is safe here because the grades are between 0 and 100. With arbitrary integers a subtraction can overflow, and other.grade.CompareTo(grade) would be the safer form.

    Testing

    Results of RankStudents:

    Ana 90, Bob 85, Cid 90                 ->  Ana Cid Bob
    Eve 55, Dan 100, Bea 0, Cal 99        ->  Dan Cal Eve Bea
    Max 88, Leo 88, Anna 88, Ann 88       ->  Ann Anna Leo Max

    Complexity

    The sort makes O(n log n) comparisons for n students and each comparison takes constant time. The loop that builds the names is O(n). Together this is O(n log n) time, with O(n) extra space for the list of names.

    Tracing the first example

    The sort asks the students to compare themselves with each other. Some of the comparisons for Ana 90, Bob 85 and Cid 90:

    CallResultMeaning
    Ana.CompareTo(Cid)-1equal grades, "Ana" is before "Cid": Ana first
    Cid.CompareTo(Bob)-590 is higher than 85: Cid first
    Bob.CompareTo(Ana)5Ana is higher: Ana first

    The sorted list is Ana, Cid, Bob. Without calling Sort(), the list stays in the order it was built, Ana Bob Cid.

    Another way: a comparison at the call

    Sort also accepts a comparison function, which is useful when you cannot change the class or want a second order:

    students.Sort((p, q) => q.grade != p.grade
        ? q.grade - p.grade
        : string.CompareOrdinal(p.name, q.name));

    On the first example this gives Ana Cid Bob too. The lesson asks for the class itself to know its order, though, so that a plain Sort() works anywhere. LINQ gives the same result with OrderByDescending(s => s.grade).ThenBy(s => s.name), and it returns a new sequence instead of sorting the list in place.

    Common mistakes

    • Forgetting IComparable<Student>. Without it, Sort() cannot compare two students. For a class with no comparison, a call to Sort() throws InvalidOperationException with the message "Failed to compare two elements in the array."
    • Reversing the subtraction and getting the grades in ascending order.
    • Comparing the names first, or only the names.
    • Writing your own sorting loop instead of letting Sort() use CompareTo.

    Why the names are compared with CompareTo

    Strings already know how to compare themselves: "Ana".CompareTo("Cid") is negative, so the tie-breaker needs no extra code. Names in this lesson are a capital letter followed by lowercase letters, so the default comparison puts them in the order a person would expect. Notice also that no two students share a name, which means two different students are never "equal" for the sort, and the final order is fully decided.

    Key takeaways

    • To make your own class sortable with a plain Sort(), implement IComparable<T> and its CompareTo.
    • The result's sign is its meaning: negative for "me first", positive for "the other first", 0 for equal.
    • For a tie-breaker, compare the main key first, and only when it is equal compare the second one.
    • Comparing neighbours in the returned list, as the lesson's tests do, is a good way to check your own CompareTo: for each pair of neighbours the first must not come after the second.
    Keep going with C#500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free