Start learning free

LESSON 567 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Sort Your Own Objects in Go: sort.Interface

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

Go has no comparison methods or operators for a struct. sort.Sort sorts any value that has the three methods of sort.Interface: Len(), Less(i, j) (does element i come before element j?) and Swap(i, j). Give them to ByRank, a slice of students, and sort with sort.Sort(ByRank(students)). Student is a struct created by NewStudent(name, grade), and rankStudents is a plain function.

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

    GO · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Sort Your Own Objects in Go, explained

    Problem overview

    Go 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 you say how two of them compare. Once you have, the standard sort package can sort a slice of those structs with no comparison written at the call.

    You write a Student struct, created by NewStudent(name, grade), and a function rankStudents(students) that sorts the students and returns their names in order. 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 result is [Ana Cid Bob]: Ana and Cid tie on 90, and Ana's name comes first.

    Approach

    Go has no comparison operators or methods for structs. Instead, sort.Sort accepts any value that has three methods, which together make up sort.Interface:

    • Len() returns how many elements there are.
    • Less(i, j) says whether element i must come before element j.
    • Swap(i, j) exchanges two elements.

    The sort calls them as often as it needs; you write no sorting loop. You attach the methods to a named slice type, here ByRank, defined as []Student. All the ranking rules live in Less.

    Step by step

    1. Give Student two fields, name and grade, and write NewStudent to return one with both set.
    2. Len returns len(this), since this is the slice itself.
    3. In Less(i, j), first check whether the grades differ. If they do, return whether student i's grade is greater than student j's, because a higher grade ranks first.
    4. If the grades are equal, return whether student i's name is less than student j's. Strings compare letter by letter, so equal grades go from A to Z.
    5. Swap exchanges the two elements in one assignment: this[i], this[j] = this[j], this[i]. Go evaluates the right side first, so no temporary variable is needed.
    6. In rankStudents, call sort.Sort(ByRank(students)). The conversion ByRank(students) views the same slice as a ByRank, so the sort reorders students itself.
    7. Then collect the names into a new []string, with append, in the sorted order, and return it.

    The solution in Go

    type Student struct {
        name  string
        grade int
    }
    
    func NewStudent(name string, grade int) Student {
        return Student{name: name, grade: grade}
    }
    
    type ByRank []Student
    
    func (this ByRank) Len() int {
        return len(this)
    }
    
    func (this ByRank) Less(i, j int) bool {
        if this[i].grade != this[j].grade {
            return this[i].grade > this[j].grade
        }
        return this[i].name < this[j].name
    }
    
    func (this ByRank) Swap(i, j int) {
        this[i], this[j] = this[j], this[i]
    }
    
    func rankStudents(students []Student) []string {
        sort.Sort(ByRank(students))
        names := []string{}
        for _, student := range students {
            names = append(names, student.name)
        }
        return names
    }

    A few details:

    • The method names Len, Less and Swap start with capital letters because sort.Interface names them that way. Your own methods elsewhere may start lowercase.
    • Nothing says that ByRank implements sort.Interface. Having the three methods is enough.
    • sort.Sort changes the slice you pass, so after rankStudents(students) the caller's slice is sorted too. The function returns the names only.

    Trying it

    students := []Student{NewStudent("Ana", 90), NewStudent("Bob", 85), NewStudent("Cid", 90)}
    ranked := ByRank(students)
    fmt.Println(ranked.Len(), ranked.Less(0, 1), ranked.Less(1, 0), ranked.Less(0, 2), ranked.Less(2, 0))
    fmt.Println(rankStudents(students))
    fmt.Println(students)
    3 true false true false
    [Ana Cid Bob]
    [{Ana 90} {Cid 90} {Bob 85}]
    

    On the first line, Ana (index 0) comes before Bob (index 1) but not the other way round, and Ana comes before Cid (index 2) but not the other way round: the names decide the tie. The last line shows that the original slice is now in ranked order.

    Complexity

    The sort makes O(n log n) calls to Less for n students, and each call takes constant time, since it compares an integer and, at most, two short names. Building the names list is O(n) more.

    Edge cases and common mistakes

    • Direction of the grade test. A higher grade comes first, so the test is >, not <. Reversing it ranks the worst student first.
    • Tie-break direction. Names go from A to Z, so the name test is <. For Max, Leo, Anna and Ann with the same grade 88, the result is [Ann Anna Leo Max]: when one name is the start of the other, the shorter one is smaller, so "Ann" < "Anna" is true.
    • Capitals. Strings compare by byte values, and capital letters sort before lowercase ones. The names here are one capital followed by lowercase letters, so the comparison works as expected.
    • Less must be strict. It should return false for two equal elements. Here no two students share a name, so it never matters.
    • Different grades. For Eve 55, Dan 100, Bea 0 and Cal 99 the names never matter, and the result is [Dan Cal Eve Bea].

    Another way

    For a one-off sort, sort.Slice takes just a comparison function and needs no named type:

    sort.Slice(students, func(i, j int) bool {
        if students[i].grade != students[j].grade {
            return students[i].grade > students[j].grade
        }
        return students[i].name < students[j].name
    })

    It gives the same order. This lesson uses ByRank because defining the ordering once on a type means every sort of that kind of slice uses the same rule.

    Testing

    Check three kinds of input. A list where all the grades differ (Eve 55, Dan 100, Bea 0, Cal 99) tests the grade rule alone. A list with one shared grade (Max, Leo, Anna and Ann at 88) tests the name rule alone, including the shorter-name case. The mixed list, Ana 90, Bob 85, Cid 90, tests both together. Also try a single student: Len returns 1, the sort has nothing to compare, and the one name comes back. Because sort.Sort isn't stable, equal elements may swap places, which is why the name tie-break matters: it leaves no two students equal.

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