Problem overview
leaderboard receives a list of players. Each player is a Pair<String, Int>: a name and a score. It returns only the names, from the highest score to the lowest. Players with the same score go in alphabetical order of their names. Names are never repeated.
For [("Ava", 30), ("Ben", 25), ("Cy", 40)] the answer is Cy, Ava, Ben. For [("Mia", 50), ("Leo", 70), ("Ava", 50)] Leo leads with 70, and Ava and Mia tie at 50, so the answer is Leo, Ava, Mia. A single player, [("Zoe", 0)], gives Zoe.
Approach
Sort the pairs first, then pull the names out in that order. The sort has two keys: the score from high to low, and for equal scores the name from A to Z.
Kotlin's sortedWith(compareBy(...)) sorts by the lambdas in order. The lambdas sort ascending, and there is a trick for "highest first" with numbers: sort by the negative score. The score 40 becomes -40, which is smaller than -30, so Cy comes before Ava.
Step by step
- Build
orderedwithplayers.sortedWith(compareBy({ -it.second }, { it.first })). In the lambdasitis a player:secondis the score andfirstis the name. - Make an empty list
nameswithmutableListOf<String>(). - Loop over
orderedwithfor (player in ordered). - Inside the loop, add
player.firsttonames. - After the loop, return
names.
The solution in Kotlin
class Solution {
fun leaderboard(players: List<Pair<String, Int>>): List<String> {
val ordered = players.sortedWith(compareBy({ -it.second }, { it.first }))
val names = mutableListOf<String>()
for (player in ordered) {
names.add(player.first)
}
return names
}
}
On the three examples it returns:
[Cy, Ava, Ben]
[Leo, Ava, Mia]
[Zoe]
The returned MutableList is accepted where a List<String> is expected, because a mutable list is also a read-only list.
A run with a tie
Take [("Ava", 30), ("Ben", 25), ("Cy", 40), ("Dee", 30)]. After the sort, ordered holds the whole pairs in this order:
[(Cy, 40), (Ava, 30), (Dee, 30), (Ben, 25)]
Cy has the highest score. Ava and Dee tie at 30, so the second lambda puts Ava before Dee. Ben is last. The loop then copies the names in that order, and the function returns [Cy, Ava, Dee, Ben]. The sorting does the thinking; the loop only strips the scores.
The minus sign is a trick for numbers only. You can't negate a name, which is why the name is the second lambda and sorts in its normal A to Z order.
Testing
When every score is equal, only the names decide. For [("Mia", 5), ("Ava", 5), ("Leo", 5)] the answer is [Ava, Leo, Mia]. A list that is already in the right order, such as [("Cy", 40), ("Ava", 30), ("Ben", 25)], comes back as Cy, Ava, Ben. Sorting takes about n log n comparisons for n players, which is fine for at most 100.
Another way
You can sort in descending order directly with compareByDescending and chain the tie-break with thenBy:
val ordered = players.sortedWith(compareByDescending<Pair<String, Int>> { it.second }.thenBy { it.first })
On the second example it gives the same names, [Leo, Ava, Mia]. The negative score is shorter, and it works because scores are whole numbers in a small range.
Edge cases and common mistakes
- Only sorting by score.
sortedByDescending { it.second }keeps tied players in their original order. On the second example it gives(Leo, 70), (Mia, 50), (Ava, 50), with Mia before Ava. The second lambda fixes this. - Dropping the minus sign. Then the lowest score comes first.
- Returning pairs. The result type is
List<String>, names only. Takeplayer.first. - Swapping
firstandsecond. The name is the first slot, the score the second.
