Problem overview
Scala can sort numbers and strings on its own, because it knows which of two of them comes first. For objects of your own class it doesn't, until the class says how two of its objects compare. After that, the standard sorted works on an array of those objects, with no comparison written at the call.
The lesson asks for two things:
- a class
Student(name, grade)that can compare itself with another student: a higher grade comes first, and when the grades are equal, the name that comes first from A to Z comes first; - a method
rankStudents(students)inSolutionthat sorts the students with Scala's own sort and returns their names in that order.
For Ana (90), Bob (85) and Cid (90), the answer is Ana Cid Bob. Ana and Cid tie on 90, so their names decide, and Bob comes last.
Approach
In Scala, a class becomes sortable by extending Ordered[Student] and defining compare(other). The method returns a negative number when this student comes first, a positive number when other does and 0 when they are equal. With it, sorted can order the array, and the operators < and > work on students as well.
The rule for compare has two parts:
- If the grades differ, the higher grade comes first.
other.grade - gradeis negative exactly when this student's grade is bigger. - If the grades are equal, use the names. A string's
compareTois negative when the first string comes first alphabetically.
Then rankStudents sorts and collects the names. No sorting loop is needed.
Step by step
- In
compare, whengrade != other.grade, returnother.grade - grade. - Otherwise, return
name.compareTo(other.name). - In
rankStudents, createrankedasstudents.sorted. This returns a new array and leaves the original alone. - Create an empty
ArrayBuffer[String]callednames. - Loop over
rankedand add eachstudent.namewith+=. - Return
names.toList.
The solution in Scala
class Student(val name: String, val grade: Int) extends Ordered[Student] {
override def compare(other: Student): Int = {
if (grade != other.grade) {
return other.grade - grade
}
return name.compareTo(other.name)
}
}
class Solution {
def rankStudents(students: Array[Student]): List[String] = {
val ranked = students.sorted
val names = scala.collection.mutable.ArrayBuffer[String]()
for (student <- ranked) {
names += student.name
}
return names.toList
}
}
Details worth knowing:
- Why
sortedworks. It needs an ordering forStudent. BecauseStudentextendsOrdered[Student], Scala finds one. Without theextends,Array(...).sorteddoesn't compile:No implicit Ordering defined. - Only the sign matters. Sorting looks at whether the result is negative, zero or positive, not at its size.
- Cost. The sort makes O(n log n) comparisons for n students, and each comparison takes constant time for names up to 10 letters.
Trying it
The comparison can be called directly:
val ana = new Student("Ana", 90)
val bob = new Student("Bob", 85)
println(ana.compare(bob))
println(bob.compare(ana))
println(ana < bob)
println(ana > bob)
This prints:
-5
5
true
false
Ana ranks before Bob, so ana < bob is true: for an Ordered class, "less" means "comes first". Ranking whole lists gives:
- Eve 55, Dan 100, Bea 0, Cal 99 give
Dan Cal Eve Bea. All the grades differ, so only the grades decide. - Max, Leo, Anna and Ann, all with grade 88, give
Ann Anna Leo Max. A name that is the start of another comes first, soAnnprecedesAnna.
Why a subtraction works as a comparison
Sorting needs to know which of two items goes first, and the answer can be any negative number, zero or any positive number. That is why subtracting works. For Ana (90) and Bob (85), other.grade - grade is 85 - 90 = -5: negative, so Ana comes first. Seen from Bob, it is 90 - 85 = 5: positive, so the other student comes first. The two answers agree, which a comparison must always do.
The grades are small whole numbers, from 0 to 100, so the subtraction can't overflow an Int. For large values, Integer.compare would be the safer way to compare two numbers, because a difference of two big numbers can wrap around.
The tie
For Ana and Cid, both with 90, the grade check is skipped and the names decide. "Ana".compareTo("Cid") is negative, so Ana comes first. For "Ann" and "Anna", the shorter one is the start of the longer one and gets the negative result, so it comes first.
Edge cases
A single student sorts to a list of one name. Grades of 0 and 100 are valid and sort like any others. No two students share a name, so two students never compare as equal in the lesson's tests, but compare still returns 0 for the same name and grade, which is what Ordered expects. Names are a capital letter followed by lowercase letters, so the alphabetical order of compareTo is the order a person expects.
Common mistakes
- Reversing the grade subtraction.
grade - other.gradewould put the lowest grade first. The higher grade must give a negative result, which isother.grade - grade. - Forgetting the tie. Equal grades must fall back on the names, or the order of equal students stays arbitrary.
- Comparing names with
-. Strings can't be subtracted. UsecompareTo. - Writing a sorting loop. The lesson wants Scala's own
sorted, which usescompare. - Returning students instead of names. The method returns a
List[String].
