1244. Design A Leaderboard π
Problem Description
You need to design a Leaderboard class that manages player scores and provides functionality to track top performers.
The Leaderboard class must implement three main functions:
-
addScore(playerId, score)
: This function adds points to a player's current score. If the player doesn't exist on the leaderboard yet, they are added with the given score as their initial score. If they already exist, the score is added to their existing total. -
top(K)
: This function returns the sum of scores of the top K players. The top players are those with the highest scores on the leaderboard. -
reset(playerId)
: This function removes a player from the leaderboard by resetting their score to 0. The problem guarantees that this function will only be called for players who already exist on the leaderboard.
The leaderboard starts empty when initialized.
The solution uses a hash table to store each player's score for quick lookup and updates, combined with a SortedList
data structure to maintain scores in sorted order. This allows efficient retrieval of top K scores. When updating scores, the old score is removed from the sorted list and the new score is added to maintain the correct ordering. The top(K)
function simply sums the last K elements of the sorted list (which are the K highest scores), and reset
removes both the player's entry from the hash table and their score from the sorted list.
Intuition
When we need to track player scores and frequently query the top K players, we face two main challenges: efficiently updating individual player scores and quickly finding the highest scores.
The first thought might be to use just a hash table mapping playerId
to score
. This gives us O(1) access to any player's score, making addScore
and reset
operations fast. However, finding the top K players would require sorting all scores every time we call top(K)
, which is inefficient.
Alternatively, we could maintain a sorted array of all scores. This would make top(K)
very fast - just take the last K elements. But updating scores becomes problematic because we'd need to search for and update the player's entry, which could be O(n).
The key insight is to use both data structures together. The hash table d
gives us instant access to any player's current score by their ID. The SortedList
maintains all scores in sorted order automatically.
When adding a score, if the player already exists, we need to maintain the sorted order correctly. We can't just add the new points to the existing entry in the sorted list - we need to remove the old score value and insert the new total. For example, if a player has 50 points and gains 30 more, we remove 50 from the sorted list and add 80.
This dual structure approach elegantly handles all operations: addScore
uses the hash table to check existence and track totals while maintaining the sorted list, top(K)
simply sums the largest K values from the sorted list, and reset
removes entries from both structures. The SortedList
(typically implemented as a balanced binary search tree) ensures all operations remain efficient with O(log n) complexity for insertions and removals.
Learn more about Sorting patterns.
Solution Approach
The solution uses a combination of a hash table and a sorted list to efficiently manage the leaderboard operations.
Data Structures:
self.d
: Adefaultdict(int)
that mapsplayerId
to their current score. This provides O(1) access to any player's score.self.rank
: ASortedList
that maintains all player scores in sorted order. This allows efficient retrieval of top scores.
Implementation Details:
-
__init__
method:- Initializes an empty hash table
d
usingdefaultdict(int)
to store player scores - Creates an empty
SortedList
calledrank
to maintain scores in sorted order
- Initializes an empty hash table
-
addScore(playerId, score)
method:- First checks if the player exists in hash table
d
- If the player is new:
- Adds the score directly to
d[playerId]
- Inserts the score into the sorted list
rank
- Adds the score directly to
- If the player already exists:
- Removes the old score from
rank
usingrank.remove(self.d[playerId])
- Updates the player's score in the hash table by adding:
self.d[playerId] += score
- Inserts the new total score into
rank
usingrank.add(self.d[playerId])
- Removes the old score from
- Time Complexity: O(log n) for sorted list operations
- First checks if the player exists in hash table
-
top(K)
method:- Returns the sum of the K highest scores
- Accesses the last K elements using
self.rank[-K:]
(since the list is sorted in ascending order, the largest values are at the end) - Uses Python's built-in
sum()
function to calculate the total - Time Complexity: O(K) for accessing and summing K elements
-
reset(playerId)
method:- Removes the player's score from the sorted list using
rank.remove()
- Simultaneously removes the player from the hash table using
d.pop(playerId)
, which returns the removed value - The returned value from
pop()
is used as the argument toremove()
in a single line - Time Complexity: O(log n) for the sorted list removal
- Removes the player's score from the sorted list using
The SortedList
data structure (from the sortedcontainers
library) maintains elements in sorted order automatically, providing O(log n) insertion and removal operations through balanced tree implementations. This makes all leaderboard operations efficient even with a large number of players.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a simple example to see how the leaderboard works:
Initial State:
- Hash table
d = {}
- Sorted list
rank = []
Step 1: addScore(1, 50)
- Player 1 doesn't exist in
d
- Add to hash table:
d = {1: 50}
- Add to sorted list:
rank = [50]
Step 2: addScore(2, 30)
- Player 2 doesn't exist in
d
- Add to hash table:
d = {1: 50, 2: 30}
- Add to sorted list:
rank = [30, 50]
(automatically sorted)
Step 3: addScore(3, 70)
- Player 3 doesn't exist in
d
- Add to hash table:
d = {1: 50, 2: 30, 3: 70}
- Add to sorted list:
rank = [30, 50, 70]
Step 4: top(2)
- Need sum of top 2 scores
- Access last 2 elements:
rank[-2:] = [50, 70]
- Return sum:
50 + 70 = 120
Step 5: addScore(2, 40)
- Player 2 exists with score 30
- Remove old score from sorted list:
rank.remove(30)
βrank = [50, 70]
- Update hash table:
d[2] = 30 + 40 = 70
βd = {1: 50, 2: 70, 3: 70}
- Add new score to sorted list:
rank.add(70)
βrank = [50, 70, 70]
Step 6: top(3)
- Access last 3 elements:
rank[-3:] = [50, 70, 70]
- Return sum:
50 + 70 + 70 = 190
Step 7: reset(1)
- Remove from sorted list:
rank.remove(50)
βrank = [70, 70]
- Remove from hash table:
d.pop(1)
βd = {2: 70, 3: 70}
Final State:
- Hash table
d = {2: 70, 3: 70}
- Sorted list
rank = [70, 70]
This example demonstrates how the dual data structure approach efficiently handles all operations: the hash table provides quick player lookups while the sorted list maintains score ordering for fast top-K queries.
Solution Implementation
1from collections import defaultdict
2from sortedcontainers import SortedList
3
4class Leaderboard:
5 def __init__(self):
6 # Dictionary to store player_id -> total_score mapping
7 self.player_scores = defaultdict(int)
8 # SortedList to maintain scores in sorted order for efficient top-K queries
9 self.sorted_scores = SortedList()
10
11 def addScore(self, playerId: int, score: int) -> None:
12 """
13 Add score to a player's total score.
14 If player doesn't exist, create new entry with the given score.
15 If player exists, add the score to their existing total.
16 """
17 if playerId not in self.player_scores:
18 # New player - add score directly
19 self.player_scores[playerId] = score
20 self.sorted_scores.add(score)
21 else:
22 # Existing player - update their score
23 # Remove old score from sorted list
24 self.sorted_scores.remove(self.player_scores[playerId])
25 # Update player's total score
26 self.player_scores[playerId] += score
27 # Add new score to sorted list
28 self.sorted_scores.add(self.player_scores[playerId])
29
30 def top(self, K: int) -> int:
31 """
32 Return the sum of scores of the top K players.
33 Uses negative indexing to get the K highest scores from sorted list.
34 """
35 # Get the last K elements (highest scores) and sum them
36 return sum(self.sorted_scores[-K:])
37
38 def reset(self, playerId: int) -> None:
39 """
40 Reset a player's score to 0 by removing them from the leaderboard.
41 """
42 # Remove player's score from sorted list and delete from dictionary
43 self.sorted_scores.remove(self.player_scores.pop(playerId))
44
45
46# Your Leaderboard object will be instantiated and called as such:
47# obj = Leaderboard()
48# obj.addScore(playerId, score)
49# param_2 = obj.top(K)
50# obj.reset(playerId)
51
1class Leaderboard {
2 // Map to store each player's total score (playerId -> totalScore)
3 private Map<Integer, Integer> playerScores = new HashMap<>();
4
5 // TreeMap to store score frequencies in descending order (score -> count)
6 // Using custom comparator to sort scores from highest to lowest
7 private TreeMap<Integer, Integer> scoreFrequency = new TreeMap<>((a, b) -> b - a);
8
9 public Leaderboard() {
10 // Empty constructor - data structures already initialized
11 }
12
13 public void addScore(int playerId, int score) {
14 // Get the player's current total score (0 if new player)
15 int oldScore = playerScores.getOrDefault(playerId, 0);
16
17 // Calculate the new total score for this player
18 int newScore = oldScore + score;
19
20 // Update the player's total score in the map
21 playerScores.put(playerId, newScore);
22
23 // If player had a previous score, decrease its frequency count
24 if (oldScore > 0) {
25 // Decrease the frequency of the old score
26 int oldFrequency = scoreFrequency.get(oldScore);
27 if (oldFrequency == 1) {
28 // Remove the score entirely if no other player has it
29 scoreFrequency.remove(oldScore);
30 } else {
31 // Otherwise just decrease the count
32 scoreFrequency.put(oldScore, oldFrequency - 1);
33 }
34 }
35
36 // Increase the frequency count for the new score
37 scoreFrequency.merge(newScore, 1, Integer::sum);
38 }
39
40 public int top(int K) {
41 int totalSum = 0;
42 int remainingPlayers = K;
43
44 // Iterate through scores in descending order
45 for (Map.Entry<Integer, Integer> entry : scoreFrequency.entrySet()) {
46 int score = entry.getKey();
47 int playerCount = entry.getValue();
48
49 // Take minimum of available players with this score and remaining slots
50 int playersToInclude = Math.min(playerCount, remainingPlayers);
51
52 // Add their contribution to the total
53 totalSum += score * playersToInclude;
54
55 // Decrease the remaining slots
56 remainingPlayers -= playersToInclude;
57
58 // Stop if we've included K players
59 if (remainingPlayers == 0) {
60 break;
61 }
62 }
63
64 return totalSum;
65 }
66
67 public void reset(int playerId) {
68 // Get and remove the player's score from the map
69 Integer score = playerScores.remove(playerId);
70
71 // If player existed, update the frequency map
72 if (score != null) {
73 // Decrease the frequency count for this score
74 int newFrequency = scoreFrequency.merge(score, -1, Integer::sum);
75
76 // Remove the score from frequency map if count reaches 0
77 if (newFrequency == 0) {
78 scoreFrequency.remove(score);
79 }
80 }
81 }
82}
83
84/**
85 * Your Leaderboard object will be instantiated and called as such:
86 * Leaderboard obj = new Leaderboard();
87 * obj.addScore(playerId,score);
88 * int param_2 = obj.top(K);
89 * obj.reset(playerId);
90 */
91
1class Leaderboard {
2public:
3 Leaderboard() {
4 // Constructor - no initialization needed as containers are empty by default
5 }
6
7 void addScore(int playerId, int score) {
8 // Add score to player's total
9 playerScores[playerId] += score;
10 int newTotalScore = playerScores[playerId];
11
12 // If player had a previous score, remove the old score from the ranking
13 if (newTotalScore != score) {
14 // Player existed before, so remove their old score from the multiset
15 scoreRanking.erase(scoreRanking.find(newTotalScore - score));
16 }
17
18 // Insert the new total score into the ranking
19 scoreRanking.insert(newTotalScore);
20 }
21
22 int top(int K) {
23 // Calculate sum of top K scores
24 int totalSum = 0;
25
26 // Iterate through scores in descending order (due to greater<int> comparator)
27 for (const auto& score : scoreRanking) {
28 totalSum += score;
29 K--;
30 if (K == 0) {
31 break;
32 }
33 }
34
35 return totalSum;
36 }
37
38 void reset(int playerId) {
39 // Get player's current score
40 int currentScore = playerScores[playerId];
41
42 // Remove player from the score map
43 playerScores.erase(playerId);
44
45 // Remove player's score from the ranking
46 scoreRanking.erase(scoreRanking.find(currentScore));
47 }
48
49private:
50 // Map to store each player's total score (playerId -> total score)
51 unordered_map<int, int> playerScores;
52
53 // Multiset to maintain scores in descending order for quick top-K retrieval
54 // Using greater<int> to sort in descending order
55 multiset<int, greater<int>> scoreRanking;
56};
57
58/**
59 * Your Leaderboard object will be instantiated and called as such:
60 * Leaderboard* obj = new Leaderboard();
61 * obj->addScore(playerId,score);
62 * int param_2 = obj->top(K);
63 * obj->reset(playerId);
64 */
65
1// Map to store each player's total score (playerId -> total score)
2const playerScores: Map<number, number> = new Map();
3
4// Array to maintain all scores for sorting and top-K retrieval
5// We'll need to keep track of scores and sort when needed
6const scoreRanking: number[] = [];
7
8/**
9 * Initialize the leaderboard
10 */
11function Leaderboard(): void {
12 // Clear any existing data
13 playerScores.clear();
14 scoreRanking.length = 0;
15}
16
17/**
18 * Add score to a player's total or create new player with score
19 * @param playerId - The ID of the player
20 * @param score - The score to add to the player's total
21 */
22function addScore(playerId: number, score: number): void {
23 // Get current score or 0 if player doesn't exist
24 const currentScore = playerScores.get(playerId) || 0;
25
26 // Calculate new total score
27 const newTotalScore = currentScore + score;
28
29 // Update player's score in the map
30 playerScores.set(playerId, newTotalScore);
31
32 // If player had a previous score, remove it from the ranking array
33 if (currentScore > 0) {
34 const indexToRemove = scoreRanking.indexOf(currentScore);
35 if (indexToRemove !== -1) {
36 scoreRanking.splice(indexToRemove, 1);
37 }
38 }
39
40 // Add the new total score to the ranking array
41 scoreRanking.push(newTotalScore);
42
43 // Sort the ranking array in descending order for efficient top-K retrieval
44 scoreRanking.sort((a, b) => b - a);
45}
46
47/**
48 * Calculate the sum of the top K scores
49 * @param K - The number of top scores to sum
50 * @returns The sum of the top K scores
51 */
52function top(K: number): number {
53 // Calculate sum of top K scores
54 let totalSum = 0;
55
56 // Take the first K elements (which are the highest due to descending sort)
57 const limit = Math.min(K, scoreRanking.length);
58 for (let i = 0; i < limit; i++) {
59 totalSum += scoreRanking[i];
60 }
61
62 return totalSum;
63}
64
65/**
66 * Reset a player's score to 0 (remove from leaderboard)
67 * @param playerId - The ID of the player to reset
68 */
69function reset(playerId: number): void {
70 // Get player's current score
71 const currentScore = playerScores.get(playerId);
72
73 // If player doesn't exist, nothing to reset
74 if (currentScore === undefined) {
75 return;
76 }
77
78 // Remove player from the score map
79 playerScores.delete(playerId);
80
81 // Remove player's score from the ranking array
82 const indexToRemove = scoreRanking.indexOf(currentScore);
83 if (indexToRemove !== -1) {
84 scoreRanking.splice(indexToRemove, 1);
85 }
86}
87
Time and Space Complexity
Time Complexity:
__init__()
:O(1)
- Initializing empty data structuresaddScore(playerId, score)
:- If player doesn't exist:
O(log n)
for adding to SortedList - If player exists:
O(n)
for removing from SortedList +O(log n)
for adding =O(n)
- Overall:
O(n)
wheren
is the number of players
- If player doesn't exist:
top(K)
:O(K)
for slicing and summing the lastK
elementsreset(playerId)
:O(n)
for removing from SortedList
Space Complexity:
- The dictionary
self.d
stores at mostn
player-score pairs:O(n)
- The SortedList
self.rank
stores at mostn
scores:O(n)
- Overall space complexity:
O(n)
, wheren
is the number of players
This matches the reference answer that states the space complexity is O(n)
, where n
is the number of players.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Score Updates in Sorted List
One of the most common mistakes is forgetting to remove the old score from the sorted list before adding the updated score when a player already exists. This leads to duplicate scores in the sorted list and incorrect results for the top(K)
function.
Incorrect Implementation:
def addScore(self, playerId: int, score: int) -> None:
# WRONG: Forgetting to remove old score
self.player_scores[playerId] += score
self.sorted_scores.add(self.player_scores[playerId])
Why it's wrong: Each time an existing player gets a score update, their new total is added to the sorted list without removing the old total. This causes:
- Multiple entries for the same player in the sorted list
- Inflated sums when calling
top(K)
- Memory waste from storing obsolete scores
Correct Implementation:
def addScore(self, playerId: int, score: int) -> None:
if playerId not in self.player_scores:
self.player_scores[playerId] = score
self.sorted_scores.add(score)
else:
# Must remove old score first
self.sorted_scores.remove(self.player_scores[playerId])
self.player_scores[playerId] += score
self.sorted_scores.add(self.player_scores[playerId])
2. Race Condition Between Dictionary Update and Sorted List Update
Another pitfall is updating the dictionary before removing the old value from the sorted list, which makes it impossible to find the old score.
Incorrect Implementation:
def addScore(self, playerId: int, score: int) -> None:
if playerId in self.player_scores:
# WRONG: Dictionary updated first, old score is lost
self.player_scores[playerId] += score
self.sorted_scores.remove(self.player_scores[playerId]) # This removes the NEW score!
self.sorted_scores.add(self.player_scores[playerId])
Why it's wrong: After updating player_scores[playerId]
, we lose access to the old score value. The remove()
call will try to remove the new score (which isn't in the list yet), causing an error or unexpected behavior.
Correct Implementation:
def addScore(self, playerId: int, score: int) -> None:
if playerId in self.player_scores:
# Remove old score BEFORE updating dictionary
self.sorted_scores.remove(self.player_scores[playerId])
self.player_scores[playerId] += score
self.sorted_scores.add(self.player_scores[playerId])
3. Using Regular List Instead of SortedList
Some developers might try to use a regular Python list and manually sort it, which severely impacts performance.
Incorrect Approach:
def __init__(self):
self.player_scores = defaultdict(int)
self.scores_list = [] # Regular list
def addScore(self, playerId: int, score: int) -> None:
# ... update logic ...
self.scores_list.append(new_score)
self.scores_list.sort() # O(n log n) every time!
Why it's wrong: Sorting the entire list after each insertion is O(n log n), making the solution inefficient for frequent updates. SortedList maintains order automatically with O(log n) insertions.
4. Misunderstanding Negative Indexing for Top K
Some might incorrectly use positive indexing to get top K scores, forgetting that SortedList maintains ascending order.
Incorrect Implementation:
def top(self, K: int) -> int:
# WRONG: This gets the K LOWEST scores
return sum(self.sorted_scores[:K])
Correct Implementation:
def top(self, K: int) -> int:
# Use negative indexing to get K highest scores
return sum(self.sorted_scores[-K:])
Which data structure is used to implement recursion?
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!