When match data is split across multiple months, the cleanest way to compute “best players” is to:
Aggregate all records into a per-player summary (matches + total score, month-wise counts)
Filter by eligibility rules
Sort by average score
Pick the top K
This post walks through a simple, interview-friendly Java solution using a HashMap + a Streams pipeline.
Problem
You’re given match performance data for the last 3 months:
month1.csvmonth2.csvmonth3.csv
Each record is:
(playerId, score)
Each record represents one match played by that player.
Eligibility Rules (as used in the code below)
A player is eligible if:
Played at least 4 matches overall (across all months)
Played at least 1 match in each month
Their average score across all matches is greater than 50
From eligible players, return the TOP 3 by average score (descending)
Want the original stricter rules (like
20 overalland5 per month)?
This solution is parameterized, so you’ll only change the thresholds.
Example Input
month1:
(P1, 60), (P2, 45), (P3, 70)
month2:
(P1, 55), (P1, 56), (P2, 50), (P3, 75)
month3:
(P1, 65), (P2, 55), (P3, 68), (P3, 69)
Expected Output
["P3", "P1"]
Why?
P1 scores: 60, 55, 56, 65 → average = 59.0 ✅ (played all months + 4 matches)
P2 scores: 45, 50, 55 → average = 50.0 ❌ (must be > 50 and also only 3 matches)
P3 scores: 70, 75, 68, 69 → average = 70.5 ✅
Sorted by average score: P3, then P1
Approach
Step 1: Aggregate into a per-player summary
We build a map:
Map<String, PlayerScoreSummary> summaryByPlayer
Each player summary tracks:
matchesByMonth[m]scoreByMonth[m]
This makes it easy to check:
“played each month”
“total matches”
“average score”
Step 2: Filter + sort + limit
After aggregation:
filter players that fail rules
sort by average score descending
take top K
Final Java Solution (Single File, Runnable)
import java.util.*;
import java.util.stream.Collectors;
public class TopPlayersSelectorBlog {
// ---------------- SAMPLE INPUT ----------------
static List<PlayerScore> month1 = Arrays.asList(
new PlayerScore("P1", 60),
new PlayerScore("P2", 45),
new PlayerScore("P3", 70)
);
static List<PlayerScore> month2 = Arrays.asList(
new PlayerScore("P1", 55),
new PlayerScore("P1", 56),
new PlayerScore("P2", 50),
new PlayerScore("P3", 75)
);
static List<PlayerScore> month3 = Arrays.asList(
new PlayerScore("P1", 65),
new PlayerScore("P2", 55),
new PlayerScore("P3", 68),
new PlayerScore("P3", 69)
);
public static void main(String[] args) {
List<List<PlayerScore>> months = List.of(month1, month2, month3);
// Rules (easy to change)
int minTotalMatches = 4;
int minMatchesEachMonth = 1;
double minAvgScore = 50.0; // must be strictly greater than 50
int topK = 3;
List<String> selected = selectTopPlayers(
months,
minTotalMatches,
minMatchesEachMonth,
minAvgScore,
topK
);
System.out.println("Selected Players: " + selected);
// Expected: [P3, P1]
}
/**
* Returns topK playerIds by average score (descending),
* after applying eligibility rules.
*/
static List<String> selectTopPlayers(
List<List<PlayerScore>> months,
int minTotalMatches,
int minMatchesEachMonth,
double minAvgScoreExclusive,
int topK
) {
int monthCount = months.size();
Map<String, PlayerScoreSummary> summaryByPlayer = new HashMap<>();
// 1) Aggregate (single pass over each month)
for (int m = 0; m < monthCount; m++) {
for (PlayerScore ps : months.get(m)) {
summaryByPlayer
.computeIfAbsent(ps.playerId, id -> new PlayerScoreSummary(id, monthCount))
.addMatch(m, ps.score);
}
}
// 2) Filter + sort + limit + map to playerId
return summaryByPlayer.values().stream()
.filter(s -> s.totalMatches() >= minTotalMatches)
.filter(s -> s.matchesEachMonthAtLeast(minMatchesEachMonth))
.filter(s -> s.averageScore() > minAvgScoreExclusive)
.sorted(
Comparator.comparingDouble(PlayerScoreSummary::averageScore).reversed()
// optional tie-breaker for stability
.thenComparing(PlayerScoreSummary::totalMatches, Comparator.reverseOrder())
.thenComparing(s -> s.playerId)
)
.limit(topK)
.map(s -> s.playerId)
.collect(Collectors.toList());
}
// ---------------- DATA MODELS ----------------
static class PlayerScore {
final String playerId;
final int score;
PlayerScore(String playerId, int score) {
this.playerId = playerId;
this.score = score;
}
}
static class PlayerScoreSummary {
final String playerId;
final int[] matchesByMonth;
final int[] scoreByMonth;
PlayerScoreSummary(String playerId, int monthCount) {
this.playerId = playerId;
this.matchesByMonth = new int[monthCount];
this.scoreByMonth = new int[monthCount];
}
void addMatch(int monthIndex, int score) {
matchesByMonth[monthIndex]++;
scoreByMonth[monthIndex] += score;
}
int totalMatches() {
int sum = 0;
for (int c : matchesByMonth) sum += c;
return sum;
}
int totalScore() {
int sum = 0;
for (int s : scoreByMonth) sum += s;
return sum;
}
double averageScore() {
int matches = totalMatches();
if (matches == 0) return 0.0;
return (double) totalScore() / matches; // avoid integer division
}
boolean matchesEachMonthAtLeast(int minMatchesEachMonth) {
for (int c : matchesByMonth) {
if (c < minMatchesEachMonth) return false;
}
return true;
}
}
}
Complexity
Let N be the total number of match records across all months.
Aggregation: O(N)
Sorting eligible players: O(P log P) where
Pis number of playersSpace: O(P) for the per-player summaries
In interviews, this is typically considered optimal and clean.
Common Pitfalls (and how this code avoids them)
1) Integer division bugs
If you do totalScore / totalMatches with integers, you truncate.
We use:
(double) totalScore / totalMatches
2) Missing months
If a player never appears in a month, their matchesByMonth[m] stays 0, and they fail the per-month filter.
3) Hardcoding month fields (scoreA/scoreB/scoreC)
Instead of scoreA, scoreB, scoreC, we use arrays so the code is:
less repetitive
easy to scale from 3 months to N months
How to Switch to the Original Stricter Rules
Just change the parameters:
int minTotalMatches = 20;
int minMatchesEachMonth = 5;
double minAvgScore = 50.0;
int topK = 3;
Everything else stays the same.
Interview Add-ons (If You Want to Impress)
Read CSVs (BufferedReader) → convert to
List<PlayerScore>Handle malformed rows safely
If the dataset is massive:
stream file line-by-line
aggregate in a map without storing all rows
Add unit tests for:
“exactly 50 average should fail”
“missing one month should fail”
“fewer than 3 eligible players”