Start learning free

LESSON 570 OF 576 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Sort Your Own Objects in Rust: Implementing Ord

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::new(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 rank_students(students) in impl Solution, which sorts the students with your language's own sort, using that comparison, and returns their names in that order.

In Rust, Student is a struct that implements the trait Ord: its method cmp(&self, other) returns Ordering::Less when self comes first and Ordering::Greater when other does. Ord also needs PartialOrd (its partial_cmp just asks cmp), Eq and PartialEq (derived). A vector's sort() uses cmp.

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 10
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Sort Your Own Objects in Rust, explained

    Problem overview

    Rust can sort numbers and strings on its own because it knows which of two of them comes first. For a struct of your own it doesn't, until the struct says so. You write a Student struct that can compare itself with another student, and a function rank_students that sorts a vector of students with the vector's own sort() and returns their names.

    The rule: a student comes first if their grade is higher. When two grades are equal, the name that comes first alphabetically comes first. For Ana (90), Bob (85) and Cid (90), the answer is Ana Cid Bob: Ana and Cid tie on 90, and Ana's name is first.

    Approach: implement Ord, then call sort()

    A vector's sort() compares elements with the trait Ord. If Student implements it, no comparison needs to be written at the call and no sorting loop is needed. The cost is the sort's own: O(n log n) comparisons for n students, each taking constant time.

    Rust's comparison traits come as a family. Ord requires PartialOrd and Eq, and those require PartialEq. The two equality traits are derived with #[derive(PartialEq, Eq)], so two students are equal when every field is. PartialOrd just asks cmp, so the rule lives in one place.

    Step by step

    1. Declare the fields: name as a String and grade as an i32.
    2. Write new. It makes the &str parameter into a String with to_string() and stores the grade. grade alone is short for grade: grade.
    3. Handle different grades in cmp. cmp returns Ordering::Less when self comes first. To put the higher grade first, compare in reverse: other.grade.cmp(&self.grade).
    4. Break ties by name. With equal grades, return self.name.cmp(&other.name). Strings compare letter by letter, so names go from A to Z.
    5. Write partial_cmp as Some(self.cmp(other)). Students can always be compared, so it never returns None.
    6. Sort in rank_students. The vector is taken as mut and sorted with students.sort().
    7. Collect the names. Loop over the sorted students and push each student.name onto a new vector. The loop owns the students, so each name moves into the new vector without a copy.

    The solution in Rust

    use std::cmp::Ordering;
    
    #[derive(PartialEq, Eq)]
    struct Student {
        name: String,
        grade: i32,
    }
    
    impl Student {
        fn new(name: &str, grade: i32) -> Student {
            Student { name: name.to_string(), grade }
        }
    }
    
    impl Ord for Student {
        fn cmp(&self, other: &Self) -> Ordering {
            if self.grade != other.grade {
                return other.grade.cmp(&self.grade);
            }
            self.name.cmp(&other.name)
        }
    }
    
    impl PartialOrd for Student {
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
            Some(self.cmp(other))
        }
    }
    
    impl Solution {
        fn rank_students(mut students: Vec<Student>) -> Vec<String> {
            students.sort();
            let mut names = Vec::new();
            for student in students {
                names.push(student.name);
            }
            names
        }
    }
    

    A small program that uses it, with the types above in the same file and struct Solution; added:

    struct Solution;
    
    fn main() {
        let students = vec![Student::new("Ana", 90), Student::new("Bob", 85), Student::new("Cid", 90)];
        println!("{}", Solution::rank_students(students).join(" "));
    
        let ana = Student::new("Ana", 90);
        let bob = Student::new("Bob", 85);
        println!("{} {}", ana < bob, ana > bob);
        println!("{:?} {:?}", ana.cmp(&bob), bob.cmp(&ana));
    }
    
    Ana Cid Bob
    true false
    Less Greater
    

    Ana is "less than" Bob in this ordering, because "less" means "comes first". The < and > operators use partial_cmp, which asks cmp.

    Rust details worth knowing

    • Reversing a comparison. Swapping the two sides, other.grade.cmp(&self.grade), turns an ascending order into a descending one.
    • Early return. The if returns right away for different grades. The last line has no semicolon, so its value is what cmp returns for equal grades.
    • Consuming the vector. for student in students takes each student out of the vector. After the loop, students can't be used again.

    Edge cases and common mistakes

    • Comparing the grades the wrong way round. self.grade.cmp(&other.grade) would rank the lowest grade first.
    • Names that start alike. For equal grades, Ann comes before Anna: the shorter name wins when it is the start of the other. For Max, Leo, Anna and Ann, all with 88, the order is Ann Anna Leo Max.
    • Different grades. Eve (55), Dan (100), Bea (0) and Cal (99) come out as Dan Cal Eve Bea: only the grades decide.
    • Forgetting the derive. Without #[derive(PartialEq, Eq)], the impl Ord block doesn't compile, because Ord needs Eq.
    • Writing a loop to sort. The point of the lesson is that the struct knows its own order and the language's sort does the rest.

    Complexity

    Sorting n students takes O(n log n) comparisons, and each call to cmp compares two grades and, at worst, two short names, so it takes constant time for names of at most 10 letters. Building the list of names is one more pass, O(n). Nothing in cmp allocates: it only borrows the two students.

    What else you get from Ord

    Once Student implements Ord, other standard tools work on it too, not just sort(). For the students Eve (55), Dan (100), Bea (0) and Cal (99):

    let students = vec![
        Student::new("Eve", 55),
        Student::new("Dan", 100),
        Student::new("Bea", 0),
        Student::new("Cal", 99),
    ];
    println!("{}", students.iter().min().unwrap().name);
    println!("{}", students.iter().max().unwrap().name);
    println!("{}", Student::new("Ann", 88) < Student::new("Anna", 88));
    println!("{}", Student::new("Ann", 88) == Student::new("Ann", 88));
    
    Dan
    Bea
    true
    true
    

    Here min() returns the student who comes first (Dan, the top grade) and max() the one who comes last (Bea). That can look odd at first: in this ordering, "smaller" means "ranks earlier". The ordering is whatever cmp says it is, and the standard library follows it everywhere.

    One rule to keep in mind: the comparison should agree with equality. Here two students are equal when both fields match, and cmp returns Ordering::Equal only when both the grades and the names match, so the two agree. Because no two students share a name, the sort has no ties to worry about.

    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free