LeetCode 1244 - Design A Leaderboard
This problem asks us to design a leaderboard system that supports three operations efficiently. Each player has a unique playerId and an associated score. The leaderboard begins empty, and players can be added dynamically as operations are performed.
Difficulty: 🟡 Medium
Topics: Hash Table, Design, Sorting
Solution
LeetCode 1244 - Design A Leaderboard
Problem Understanding
This problem asks us to design a leaderboard system that supports three operations efficiently. Each player has a unique playerId and an associated score. The leaderboard begins empty, and players can be added dynamically as operations are performed.
The addScore(playerId, score) operation either creates a new player with the given score or increases the score of an existing player. The top(K) operation returns the sum of the highest K scores currently present in the leaderboard. The reset(playerId) operation removes a player's score by resetting it to zero, effectively deleting them from the active leaderboard.
The input consists of a sequence of method calls and arguments. The expected output is the result returned by each operation that produces a value, specifically the top() operation. Methods that modify the leaderboard without returning anything produce null in the example output.
The constraints are important for choosing the implementation strategy:
- There are at most
1000function calls. playerIdandKare at most10000.- Individual score increments are small, at most
100.
The small number of total operations means we do not necessarily need highly sophisticated balanced tree structures or heaps with lazy deletion. A straightforward and clean solution using hash maps and sorting is completely acceptable within the problem limits.
Several edge cases are important to recognize early:
- A player may receive multiple score updates over time.
- Multiple players may have the same score.
- Reset removes a player completely from consideration.
top(K)is guaranteed to have at leastKactive players.- The leaderboard starts empty, so implementations must correctly initialize internal data structures.
Approaches
Brute Force Approach
The simplest possible implementation stores every player's current score in a hash map. Whenever top(K) is called, we collect all scores into a list, sort them in descending order, and sum the first K values.
This approach is correct because sorting guarantees that the largest scores appear first, so taking the first K elements produces the exact answer required.
The downside is that every top(K) query performs a full sort over all active players. If the number of players becomes large, repeated sorting becomes inefficient.
Still, because the constraint limits total operations to only 1000, this solution is actually fast enough for LeetCode.
Optimized Approach
A better approach maintains two data structures:
- A hash map from
playerIdto current score. - A sorted multiset structure storing all current scores.
When a player's score changes, we remove the old score from the sorted structure and insert the updated score. Then top(K) can iterate over the largest scores directly without re-sorting everything each time.
In languages with built-in balanced trees or sorted containers, this approach improves repeated query efficiency. However, implementing a true balanced tree manually is unnecessary here.
For LeetCode, the practical optimal solution is usually considered the hash map plus sorting approach because the constraints are intentionally small.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(N log N) for top, O(1) updates |
O(N) | Sort scores during every top() call |
| Optimal | O(N log N) worst case overall operations | O(N) | Hash map for scores, sorting only when needed |
Algorithm Walkthrough
Optimal Strategy Using Hash Map and Sorting
- Initialize a hash map called
player_scores.
The key is the player ID, and the value is the player's current total score. This gives constant time access for updates and resets.
2. For addScore(playerId, score):
- If the player already exists, add the new score to the existing total.
- Otherwise, create a new entry with the given score.
A hash map is ideal here because player lookup and updates are average O(1) operations.
3. For top(K):
- Extract all score values from the hash map.
- Sort them in descending order.
- Sum the first
Kvalues.
Sorting guarantees that the highest scores appear first.
4. For reset(playerId):
- Remove the player from the hash map entirely.
Since the problem defines reset as erasing the player from the leaderboard, deletion is more appropriate than setting the score to zero.
Why it works
The core invariant is that the hash map always stores the exact current score for every active player. Because top(K) sorts the complete set of current scores before selecting the largest K, it always computes the correct leaderboard sum. Updates and resets modify the hash map immediately, ensuring that all future queries operate on the latest state.
Python Solution
class Leaderboard:
def __init__(self):
self.player_scores = {}
def addScore(self, playerId: int, score: int) -> None:
if playerId in self.player_scores:
self.player_scores[playerId] += score
else:
self.player_scores[playerId] = score
def top(self, K: int) -> int:
sorted_scores = sorted(
self.player_scores.values(),
reverse=True
)
return sum(sorted_scores[:K])
def reset(self, playerId: int) -> None:
del self.player_scores[playerId]
# Your Leaderboard object will be instantiated and called as such:
# obj = Leaderboard()
# obj.addScore(playerId, score)
# param_2 = obj.top(K)
# obj.reset(playerId)
The implementation begins by initializing an empty dictionary called player_scores. This dictionary acts as the central source of truth for all active players and their scores.
The addScore method checks whether the player already exists. If so, the score is incremented. Otherwise, a new player entry is created.
The top method extracts all current scores using values(), sorts them in descending order, and sums the first K entries. Python's built-in sorting is efficient and concise, making the implementation straightforward.
The reset method removes the player completely using del. This ensures the player no longer contributes to future leaderboard calculations.
Go Solution
package main
import "sort"
type Leaderboard struct {
playerScores map[int]int
}
func Constructor() Leaderboard {
return Leaderboard{
playerScores: make(map[int]int),
}
}
func (this *Leaderboard) AddScore(playerId int, score int) {
this.playerScores[playerId] += score
}
func (this *Leaderboard) Top(K int) int {
scores := make([]int, 0, len(this.playerScores))
for _, score := range this.playerScores {
scores = append(scores, score)
}
sort.Sort(sort.Reverse(sort.IntSlice(scores)))
total := 0
for i := 0; i < K; i++ {
total += scores[i]
}
return total
}
func (this *Leaderboard) Reset(playerId int) {
delete(this.playerScores, playerId)
}
/**
* Your Leaderboard object will be instantiated and called as such:
* obj := Constructor()
* obj.AddScore(playerId, score)
* param_2 := obj.Top(K)
* obj.Reset(playerId)
*/
The Go implementation mirrors the Python solution closely. A map from integer player IDs to scores provides efficient updates and lookups.
One important Go-specific detail is that map access automatically returns zero for missing keys. This allows AddScore to use:
this.playerScores[playerId] += score
without checking whether the player already exists.
For sorting, Go requires explicitly constructing a slice of scores and then applying sort.Sort(sort.Reverse(sort.IntSlice(scores))) to obtain descending order.
The delete built-in cleanly removes players during reset operations.
Worked Examples
Example 1
Input:
["Leaderboard","addScore","addScore","addScore","addScore","addScore","top","reset","reset","addScore","top"]
[[],[1,73],[2,56],[3,39],[4,51],[5,4],[1],[1],[2],[2,51],[3]]
Step-by-step Trace
| Operation | Leaderboard State | Result |
|---|---|---|
| addScore(1, 73) | {1: 73} | |
| addScore(2, 56) | {1: 73, 2: 56} | |
| addScore(3, 39) | {1: 73, 2: 56, 3: 39} | |
| addScore(4, 51) | {1: 73, 2: 56, 3: 39, 4: 51} | |
| addScore(5, 4) | {1: 73, 2: 56, 3: 39, 4: 51, 5: 4} | |
| top(1) | Scores sorted: [73, 56, 51, 39, 4] | 73 |
| reset(1) | {2: 56, 3: 39, 4: 51, 5: 4} | |
| reset(2) | {3: 39, 4: 51, 5: 4} | |
| addScore(2, 51) | {2: 51, 3: 39, 4: 51, 5: 4} | |
| top(3) | Scores sorted: [51, 51, 39, 4] | 141 |
The final answer is 141 because the top three scores are 51, 51, and 39.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(N log N) for top() |
Sorting all active scores dominates runtime |
| Space | O(N) | Hash map stores all active players |
The addScore and reset operations both run in average constant time because hash map operations are efficient. The expensive operation is top(K), which sorts all current player scores. If there are N active players, sorting requires O(N log N) time.
The space complexity is linear because we store one hash map entry per active player.
Test Cases
# Example case from problem statement
lb = Leaderboard()
lb.addScore(1, 73)
lb.addScore(2, 56)
lb.addScore(3, 39)
lb.addScore(4, 51)
lb.addScore(5, 4)
assert lb.top(1) == 73 # highest score
lb.reset(1)
lb.reset(2)
lb.addScore(2, 51)
assert lb.top(3) == 141 # top three scores
# Single player leaderboard
lb = Leaderboard()
lb.addScore(10, 100)
assert lb.top(1) == 100 # only one player
# Multiple updates to same player
lb = Leaderboard()
lb.addScore(1, 20)
lb.addScore(1, 30)
assert lb.top(1) == 50 # accumulated score
# Reset removes player
lb = Leaderboard()
lb.addScore(1, 40)
lb.addScore(2, 60)
lb.reset(2)
assert lb.top(1) == 40 # player 2 removed
# Equal scores
lb = Leaderboard()
lb.addScore(1, 50)
lb.addScore(2, 50)
lb.addScore(3, 50)
assert lb.top(2) == 100 # duplicate top scores
# Large K equals number of players
lb = Leaderboard()
lb.addScore(1, 10)
lb.addScore(2, 20)
lb.addScore(3, 30)
assert lb.top(3) == 60 # sum all players
# Re-adding reset player
lb = Leaderboard()
lb.addScore(1, 70)
lb.reset(1)
lb.addScore(1, 15)
assert lb.top(1) == 15 # player starts fresh after reset
| Test | Why |
|---|---|
| Example input | Validates overall functionality |
| Single player | Smallest practical leaderboard |
| Multiple updates | Confirms score accumulation |
| Reset player | Ensures removal works correctly |
| Equal scores | Verifies duplicate values are handled |
| K equals player count | Ensures full leaderboard summation |
| Re-adding player | Confirms reset fully clears prior score |
Edge Cases
Repeated score additions for the same player
One common source of bugs is incorrectly overwriting a player's score instead of accumulating it. The problem explicitly states that addScore adds to the existing score. The implementation handles this by incrementing the current value rather than replacing it.
Resetting a player and adding them again
After reset, a player should behave like a completely new player if added again later. Some implementations mistakenly leave stale data behind. This solution removes the player from the hash map entirely, so future additions begin from zero naturally.
Multiple players with identical scores
Leaderboards often contain ties. A flawed implementation might accidentally deduplicate equal values during sorting or storage. Since this solution stores scores independently per player and sorts raw score values, equal scores are counted correctly and independently during top(K) calculations.