Facebook Pixel

1244. Design A Leaderboard πŸ”’

MediumDesignHash TableSorting
Leetcode Link

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:

  1. 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.

  2. 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.

  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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: A defaultdict(int) that maps playerId to their current score. This provides O(1) access to any player's score.
  • self.rank: A SortedList that maintains all player scores in sorted order. This allows efficient retrieval of top scores.

Implementation Details:

  1. __init__ method:

    • Initializes an empty hash table d using defaultdict(int) to store player scores
    • Creates an empty SortedList called rank to maintain scores in sorted order
  2. 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
    • If the player already exists:
      • Removes the old score from rank using rank.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 using rank.add(self.d[playerId])
    • Time Complexity: O(log n) for sorted list operations
  3. 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
  4. 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 to remove() in a single line
    • Time Complexity: O(log n) for the sorted list removal

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 Evaluator

Example 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 structures
  • addScore(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) where n is the number of players
  • top(K): O(K) for slicing and summing the last K elements
  • reset(playerId): O(n) for removing from SortedList

Space Complexity:

  • The dictionary self.d stores at most n player-score pairs: O(n)
  • The SortedList self.rank stores at most n scores: O(n)
  • Overall space complexity: O(n), where n 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:])
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement recursion?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More