Data Science
I wanted to put our own spin on the Data lab from CollegeBoard for this project. Rather than just following the standard lab, we're going to analyze real-world video game sales data that will give you hands-on experience with classes, interfaces, and data processing in Java.
For this project, we'll be using a comprehensive dataset containing information about video games, including sales figures, critic ratings, user scores, and more. I've sourced this dataset from Kaggle: Video Game Sales and Ratings.
This project will give you practical experience with:
Creating and using custom classes
Working with interfaces
Processing CSV data files
Implementing search and analysis algorithms
Visualizing data patterns
Activity 1: Getting Started
Our project requires two main Java classes:
GameDataRow
- Represents a single game record from our datasetGameDataManager
- Handles loading and analyzing collections of game data
Let's start by looking at how to set up the GameDataRow
class:
/**
* The GameDataRow class represents a single video game from the dataset
* and provides methods for analyzing game data including sales, ratings, and more.
*/
public class GameDataRow implements Comparable<GameDataRow> {
// Instance variables will go here
// Methods will go here
}
Understanding the Class Header and Interfaces
The class header public class GameDataRow implements Comparable<GameDataRow>
has several important components:
public
- This access modifier makes the class accessible from outside its package.class GameDataRow
- Defines a new class named GameDataRow.implements Comparable<GameDataRow>
- Indicates that this class implements the Comparable interface.
The Comparable Interface
The Comparable interface is part of the Java Collections Framework and enables objects of a class to be compared to each other. When a class implements Comparable, it must provide an implementation of the compareTo()
method:
@Override
public int compareTo(GameDataRow other) {
// if you want to implement Comparable, you'll need this sort of thing
if (this.number > other.number) return -1;
if (this.number < other.number) return 1;
return 0;
}
The compareTo()
method must return:
A negative value if this object should be ordered before the other object
Zero if both objects are equal
A positive value if this object should be ordered after the other object
In our implementation, we're sorting games by global sales in descending order (highest sales first).
Other Common Interfaces in Java
While we're only using Comparable in this project, here are other common interfaces you might encounter:
Serializable - Marks a class as capable of being serialized (converted to a byte stream).
public class MyClass implements Serializable { ... }
Runnable - Used for classes whose instances will be executed by a thread.
public class MyTask implements Runnable { public void run() { // Code to be executed in a thread } }
ActionListener - Used for handling action events, commonly in GUI applications.
public class ButtonHandler implements ActionListener { public void actionPerformed(ActionEvent e) { // Handle button click } }
Iterable - Allows an object to be the target of the "for-each loop" statement.
public class MyCollection<T> implements Iterable<T> { public Iterator<T> iterator() { // Return an iterator } }
Properties and Encapsulation
Now, let's determine what properties our GameDataRow
class needs. Our CSV file contains 17 columns of data about each game, and we want to represent each data point as instance variables in our class.
Looking at the structure of our CSV file, we need to create properties that match each column:
// Instance variables
private int index;
private String name;
private String platform;
private float yearOfRelease;
private String genre;
private String publisher;
private float naSales;
private float euSales;
private float jpSales;
private float otherSales;
private float globalSales;
private float criticScore;
private float criticCount;
private String userScore; // Note: String type, not float
private float userCount;
private String developer;
private String rating;
// Static variable to track number of games analyzed
private static int gamesAnalyzed = 0;
// Constants for regional sales references
public static final int NA_REGION = 0;
public static final int EU_REGION = 1;
public static final int JP_REGION = 2;
public static final int OTHER_REGION = 3;
Encapsulation Review
Remember that encapsulation is one of the four fundamental OOP concepts, along with inheritance, polymorphism, and abstraction. Encapsulation means bundling data and methods that operate on that data within a single unit (the class) and restricting direct access to some of the object's components.
Notice all our instance variables are marked as private
. This is a key principle of encapsulation:
Private instance variables: By making our instance variables private, we prevent outside classes from directly accessing or modifying them.
Public accessor methods: We provide controlled access through getter methods (and sometimes setter methods).
Data validation: Through these methods, we can validate any changes to ensure they meet our requirements.
Good encapsulation ensures that the internal representation of an object can't be seen from outside the object's definition, which creates a "black box" effect.
Constructors
We need constructors to initialize our GameDataRow
objects. A well-designed class should have multiple constructors to provide flexibility in object creation:
/**
* Primary constructor for creating a new GameDataRow object with all fields
*/
public GameDataRow(int index, String name, String platform, float yearOfRelease,
String genre, String publisher, float naSales, float euSales,
float jpSales, float otherSales, float globalSales,
float criticScore, float criticCount, String userScore,
float userCount, String developer, String rating) {
this.index = index;
this.name = name;
this.platform = platform;
this.yearOfRelease = yearOfRelease;
this.genre = genre;
this.publisher = publisher;
this.naSales = naSales;
this.euSales = euSales;
this.jpSales = jpSales;
this.otherSales = otherSales;
this.globalSales = globalSales;
this.criticScore = criticScore;
this.criticCount = criticCount;
this.userScore = userScore;
this.userCount = userCount;
this.developer = developer;
this.rating = rating;
gamesAnalyzed++;
}
/**
* Simplified constructor with essential fields only
*/
public GameDataRow(String name, String platform, float yearOfRelease,
String genre, String publisher, float globalSales) {
this.index = -1; // Default value for index
this.name = name;
this.platform = platform;
this.yearOfRelease = yearOfRelease;
this.genre = genre;
this.publisher = publisher;
this.naSales = 0.0f;
this.euSales = 0.0f;
this.jpSales = 0.0f;
this.otherSales = 0.0f;
this.globalSales = globalSales;
this.criticScore = 0.0f;
this.criticCount = 0.0f;
this.userScore = "0.0";
this.userCount = 0.0f;
this.developer = "Unknown";
this.rating = "Not Rated";
gamesAnalyzed++;
}
/**
* Copy constructor that creates a deep copy of another GameDataRow object
*/
public GameDataRow(GameDataRow other) {
this.index = other.index;
this.name = other.name;
this.platform = other.platform;
this.yearOfRelease = other.yearOfRelease;
this.genre = other.genre;
this.publisher = other.publisher;
this.naSales = other.naSales;
this.euSales = other.euSales;
this.jpSales = other.jpSales;
this.otherSales = other.otherSales;
this.globalSales = other.globalSales;
this.criticScore = other.criticScore;
this.criticCount = other.criticCount;
this.userScore = other.userScore;
this.userCount = other.userCount;
this.developer = other.developer;
this.rating = other.rating;
gamesAnalyzed++;
}
Notice I've included three constructors:
A primary constructor with all fields
A simplified constructor with only essential fields
A copy constructor to create a deep copy of an existing object
Accessor Methods (Getters)
Now we need accessor methods (getters) for all our instance variables. This is a perfect opportunity to use AI responsibly. Rather than typing out all these repetitive methods, you can ask Claude or an IDE's chatbot to generate them for you.
For example, you could say:
Please generate all the accessor methods for the GameDataRow class based on the instance variables.
This isn't plagiarism or cutting corners - it's using a tool to handle the tedious, repetitive aspects of coding so you can focus on the more challenging and creative aspects like designing algorithms and implementing the business logic.
Here's a snippet showing what a few of these accessor methods would look like:
// Accessor methods (getters)
public int getIndex() { return index; }
public String getName() { return name; }
public String getPlatform() { return platform; }
public float getYearOfRelease() { return yearOfRelease; }
public String getGenre() { return genre; }
public String getPublisher() { return publisher; }
// ... and so on for all instance variables
Test-Driven Development
Let's talk about Test-Driven Development (TDD) - a software development approach where you write tests before writing the actual implementation code. This practice has been widely adopted by professional developers because it leads to more reliable, maintainable code.
In TDD, the process follows three simple steps, often called "Red-Green-Refactor":
Write a test that fails (Red)
Write the minimal code to make the test pass (Green)
Improve the code without changing its behavior (Refactor)
For our project, I've prepared a complete main method that serves as a test suite. It will systematically test all the required functionality of your GameDataRow
class and tell you exactly what's missing or not working correctly.
/**
* Main method for testing the GameDataRow class
*/
public static void main(String[] args) {
System.out.println("===== GAME DATA ROW TEST SUITE =====\n");
int testsPassed = 0;
int totalTests = 9;
try {
// Test 1: Basic constructor and accessors
GameDataRow game1 = new GameDataRow(
1, "Wii Sports", "Wii", 2006.0f, "Sports", "Nintendo",
41.49f, 29.02f, 3.77f, 8.46f, 82.74f,
76.0f, 51.0f, "8.0", 322.0f, "Nintendo", "E"
);
if (game1.getName().equals("Wii Sports") &&
game1.getPlatform().equals("Wii") &&
game1.getGlobalSales() == 82.74f) {
System.out.println("✓ Test 1 Passed: Basic constructor and accessors working");
testsPassed++;
} else {
System.out.println("✗ Test 1 Failed: Basic constructor or accessors not working correctly");
}
// Test 2: Simplified constructor
GameDataRow game2 = new GameDataRow("Super Mario Bros.", "NES", 1985.0f,
"Platform", "Nintendo", 40.24f);
if (game2.getName().equals("Super Mario Bros.") &&
game2.getGlobalSales() == 40.24f) {
System.out.println("✓ Test 2 Passed: Simplified constructor working");
testsPassed++;
} else {
System.out.println("✗ Test 2 Failed: Simplified constructor not working correctly");
}
// Test 3: Copy constructor
GameDataRow game3 = new GameDataRow(game1);
if (game3.getName().equals(game1.getName()) &&
game3.getGlobalSales() == game1.getGlobalSales() &&
game3 != game1) { // Different objects
System.out.println("✓ Test 3 Passed: Copy constructor working");
testsPassed++;
} else {
System.out.println("✗ Test 3 Failed: Copy constructor not working correctly");
}
// Test 4: Static counter
int expectedCount = 3; // game1, game2, game3
if (GameDataRow.getGamesAnalyzed() == expectedCount) {
System.out.println("✓ Test 4 Passed: Static counter working");
testsPassed++;
} else {
System.out.println("✗ Test 4 Failed: Static counter expected " +
expectedCount + " but got " +
GameDataRow.getGamesAnalyzed());
}
// Test 5: Get regional sales
if (Math.abs(game1.getRegionalSales(GameDataRow.NA_REGION) - 41.49f) < 0.01f) {
System.out.println("✓ Test 5 Passed: Regional sales accessor working");
testsPassed++;
} else {
System.out.println("✗ Test 5 Failed: Regional sales accessor not working correctly");
}
// Test 6: Dominant region
if (game1.getDominantRegion().equals("North America")) {
System.out.println("✓ Test 6 Passed: Dominant region calculation working");
testsPassed++;
} else {
System.out.println("✗ Test 6 Failed: Dominant region expected 'North America' but got '" +
game1.getDominantRegion() + "'");
}
// Test 7: Sales percentages
float[] percentages = game1.getSalesPercentages();
if (percentages.length == 4 &&
Math.abs(percentages[0] - 50.14f) < 0.1f) { // ~50.14% for NA
System.out.println("✓ Test 7 Passed: Sales percentages calculation working");
testsPassed++;
} else {
System.out.println("✗ Test 7 Failed: Sales percentages not calculated correctly");
}
// Test 8: User score numeric conversion
if (Math.abs(game1.getUserScoreNumeric() - 8.0f) < 0.01f) {
System.out.println("✓ Test 8 Passed: User score numeric conversion working");
testsPassed++;
} else {
System.out.println("✗ Test 8 Failed: User score numeric conversion not working correctly");
}
// Test 9: compareTo implementation (formerly Test 10)
if (game1.compareTo(game2) < 0 && // game1 has higher sales
game2.compareTo(game1) > 0) {
System.out.println("✓ Test 9 Passed: Comparable implementation working");
testsPassed++;
} else {
System.out.println("✗ Test 9 Failed: Comparable implementation not working correctly");
}
} catch (Exception e) {
System.out.println("\nTest failed with exception: " + e.getMessage());
e.printStackTrace();
}
// Summary
System.out.println("\n===== TEST SUMMARY =====");
System.out.println("Passed: " + testsPassed + "/" + totalTests + " tests");
if (testsPassed == totalTests) {
System.out.println("🎉 Congratulations! All tests passed!");
} else {
System.out.println("❌ Some tests failed. Review the output above to see which tests failed.");
}
}
Adding GameDataManager and Testing
Now that your GameDataRow
class is complete, let's work with collections of games:
Copy the contents of
GameDataManager.java
file from this link: https://gist.github.com/dadiletta/2310e45953e3bcb8db3cf151c7c75d06Download the
video_games.csv
file and place it in your project directory. The location should match the path in theGameDataManager.loadFromCSV()
method call. https://www.kaggle.com/datasets/thedevastator/video-game-sales-and-ratings?resource=download
Activity 2: Building Methods
Now that we have our class structure and properties defined, it's time to add meaningful functionality to our GameDataRow
class. To complete this class, you'll need to implement several methods that analyze game data in different ways.
I'm providing you with a few utility methods that handle some of the trickier aspects of parsing data from the CSV file, such as handling non-numeric user scores. Let's first look at these provided methods:
/**
* Determines if the game has user reviews
* @return true if user score and count are available
*/
public boolean hasUserReviews() {
try {
return Float.parseFloat(userScore) > 0 && userCount > 0;
} catch (NumberFormatException e) {
return false;
}
}
/**
* Convert user score from string to float, handling special values
* @return user score as float or 0.0 if not available
*/
public float getUserScoreNumeric() {
try {
return Float.parseFloat(userScore);
} catch (NumberFormatException e) {
return 0.0f; // Return 0 for "tbd" or other non-numeric values
}
}
/**
* Returns a string representation of the game data
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(name).append(" (").append(yearOfRelease).append(")");
sb.append("\nPlatform: ").append(platform);
sb.append("\nGenre: ").append(genre);
sb.append("\nPublisher: ").append(publisher);
sb.append("\nDeveloper: ").append(developer);
sb.append("\nRating: ").append(rating);
sb.append("\nGlobal Sales: ").append(globalSales).append(" million units");
sb.append("\nDominant Region: ").append(getDominantRegion());
if (hasCriticReviews()) {
sb.append("\nCritic Score: ").append(criticScore).append("/100 (").append(criticCount).append(" critics)");
}
if (hasUserReviews()) {
sb.append("\nUser Score: ").append(userScore).append("/10 (").append(userCount).append(" users)");
}
return sb.toString();
}
I'm giving you these methods because:
The
hasUserReviews()
andgetUserScoreNumeric()
methods handle data conversion and error checking that might be challengingThe
toString()
method is lengthy but straightforward, and implementing it wouldn't teach you much
Now, let's focus on the methods you need to implement:
Method 1: getAverageScore()
Return Type: float
This method calculates the average between critic and user scores. However, there's a challenge: critic scores are on a scale of 0-100, while user scores are on a scale of 0-10.
Requirements:
Return a float representing the normalized average score on a scale of 0-10
Normalize the critic score to a 0-10 scale by dividing by 10
Handle cases where one or both scores are missing/invalid
Return the appropriate average or individual score based on what's available
Return 0.0f if no scores are available
Key Considerations:
What constitutes an "available" score? (Hint: check for positive values)
How do you handle the case where only one score type is available?
Hints:
You can paste this code into your method as a starting guide:
// Convert critic score to scale of 10 like user score
// fetch the user score using the built-in converter
// check if any score is 0
// if not, return the average of the two / 2.0f
// else return the one with the number
Method 2: getSalesPercentages()
Return Type: float[]
This method calculates what percentage of the global sales comes from each region (North America, Europe, Japan, and Other).
Requirements:
Return an array of float values representing percentages for each region
The array should have 4 elements, one for each region
Use the defined constants (NA_REGION, EU_REGION, etc.) for array indices
Handle the case where globalSales is zero to avoid division by zero
Key Considerations:
The formula for calculating percentage is: (regional sales / global sales) * 100
If globalSales is zero, return an array of zeros to avoid division by zero
Method 3: hasSalesData()
Return Type: boolean
This method determines whether the game has any sales data available.
Requirements:
Return true if any sales data (regional or global) is available
Return false if all sales fields are zero
Key Considerations:
Check all sales fields (naSales, euSales, jpSales, otherSales, globalSales)
Use logical OR operators to efficiently check multiple conditions
Method 4: hasCriticReviews()
Return Type: boolean
This method determines whether the game has critic review data available.
Requirements:
Return true if the game has both a critic score and a count of critics
Return false if either piece of information is missing or zero
Key Considerations:
Both criticScore and criticCount should be positive values
Method 5: getDominantRegion()
Return Type: String
This method determines which region has the highest sales for this game.
Requirements:
Compare sales across all regions and find the highest
Return a string representing that region ("North America", "Europe", "Japan", or "Other")
Handle ties by choosing the first region encountered with the maximum value
Key Considerations:
You'll need to track the maximum sales value and the corresponding region
You can use a switch statement to convert from region index to region name (see example below)
Method 6: getRegionalSales()
Return Type: float
This method returns the sales figures for a specific region.
Requirements:
Accept an int parameter representing the region (use the class constants)
Return the sales figure (as a float) for the specified region
Handle all four possible regions: NA_REGION, EU_REGION, JP_REGION, OTHER_REGION
Key Considerations:
Use a switch statement to select the appropriate sales figure based on the region parameter
Switch Statement Example
A switch statement allows you to select one of many code blocks to be executed. Here's how you might implement the getRegionalSales
method using a switch statement:
// Example of how to use a switch statement to return sales for a specific region
// Example switch statement to convert a day number to a day name
int dayNumber = 3;
String dayName;
switch(dayNumber) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
case 3:
dayName = "Wednesday";
break;
case 4:
dayName = "Thursday";
break;
case 5:
dayName = "Friday";
break;
case 6:
dayName = "Saturday";
break;
case 7:
dayName = "Sunday";
break;
default:
dayName = "Invalid day";
break;
}
The switch statement evaluates the regionIndex
parameter and executes the code block that matches the case. The default
case handles any values that don't match any specified cases.
Method 7: Implementation of Comparable
Return Type: int
You need to implement the compareTo
method from the Comparable
interface.
Requirements:
Override the compareTo method to compare games based on global sales
Higher sales should sort before lower sales (descending order)
Return negative value if this game has higher sales than the other game
Return positive value if this game has lower sales than the other game
Return zero if both games have equal sales
Key Considerations:
Remember that returning a negative value means "this" comes before "other" in the sorted order
Activity 3: Results
Let's add one final method to our GameDataRow
class before moving on to working with collections of game data:
// Static method to get count of games analyzed
public static int getGamesAnalyzed() {
return gamesAnalyzed;
}
Understanding Static Variables and Methods
This method introduces an important concept in Java: static members. Unlike instance variables and methods that belong to specific objects, static members belong to the class itself.
Here's what you need to know:
Static variables (like our
gamesAnalyzed
counter) are shared across all instances of a class. There's only one copy in memory, regardless of how many objects you create.Static methods operate at the class level rather than the instance level. They can be called using the class name without creating an object:
GameDataRow.getGamesAnalyzed()
.Cannot use
this
in static context: Since static methods don't operate on a specific instance, you cannot use thethis
keyword inside them. There's no "current object" to reference! If you try to usethis
in a static method, you'll get a compiler error.Access limitations: Static methods can only directly access other static members (variables or methods). They cannot directly access instance variables or methods without referencing a specific object.
In our case, we're using the static variable gamesAnalyzed
to track how many game objects have been created throughout our program's execution. Each constructor increments this counter, and our static method lets us retrieve the current count.
Creating Your Own Analysis
Now it's time to put your skills to work! Modify the GameDataManager.java
file to add your own custom analysis of the video game data.
Add at least one new method to the GameDataManager
class that performs an interesting analysis. Here are some ideas:
Year Analysis: Which year had the highest average game quality? Most releases?
Developer Success Rate: For developers with multiple games, which ones have the highest average scores?
Cross-Platform Analysis: Which games appeared on the most platforms? Do multi-platform games sell better?
Sales vs. Ratings Correlation: Do higher-rated games generally sell better? Is there a difference between critic and user ratings?
Genre Evolution: How have the popularity of different genres changed over time?
Once you've added your analysis method(s), modify the main
method to call your new method and display the results.
Example Custom Analysis Method
Here's what your custom analysis might look like (don't just copy this - create your own!):
/**
* Analyze which platforms have the highest-rated games on average
*/
public void analyzePlatformQuality() {
Map<String, Float> totalScores = new HashMap<String, Float>();
Map<String, Integer> gameCounts = new HashMap<String, Integer>();
// Collect data
for (GameDataRow game : games) {
if (game.hasCriticReviews()) {
String platform = game.getPlatform();
totalScores.put(platform, totalScores.getOrDefault(platform, 0.0f) + game.getCriticScore());
gameCounts.put(platform, gameCounts.getOrDefault(platform, 0) + 1);
}
}
// Calculate averages and find the best
String bestPlatform = "";
float bestScore = 0.0f;
System.out.println("\n===== PLATFORM QUALITY ANALYSIS =====");
System.out.println("Platform\tAvg. Critic Score\tGame Count");
for (String platform : totalScores.keySet()) {
int count = gameCounts.get(platform);
if (count >= 10) { // Only consider platforms with enough games
float avgScore = totalScores.get(platform) / count;
System.out.printf("%s\t\t%.1f\t\t%d\n", platform, avgScore, count);
if (avgScore > bestScore) {
bestScore = avgScore;
bestPlatform = platform;
}
}
}
System.out.println("\nBest-rated platform: " + bestPlatform +
" with average score " + bestScore);
}
In the main
method, you would add a call to your new analysis:
// Call your custom analysis
manager.analyzePlatformQuality();
Requirements for Your Custom Analysis
Your analysis should:
Process data from multiple games
Use loops and conditionals appropriately
Output meaningful results
Include comments explaining your approach
Handle edge cases (like missing data)
Activity 4: Recursion
In this final activity, you'll explore recursion - a powerful programming technique where a method calls itself to solve a problem by breaking it down into smaller instances of the same problem.
Understanding Recursion
Recursion is a fundamental concept in computer science that provides elegant solutions to certain types of problems. A recursive method has two essential components:
Base case(s): The simplest scenario(s) that can be solved directly without further recursion
Recursive case(s): Where the method calls itself with a simpler version of the problem
Let's examine a simple example - calculating the factorial of a number:
/**
* Calculates the factorial of n (n!)
* @param n a non-negative integer
* @return the factorial of n
*/
public static int factorial(int n) {
// Base case
if (n == 0 || n == 1) {
return 1;
}
// Recursive case
else {
return n * factorial(n - 1);
}
}
When calculating factorial(5)
:
The method first checks if n equals 0 or 1 (our base case) - it doesn't, so we move to the recursive case
It returns
5 * factorial(4)
Now it calculates
factorial(4)
, which is4 * factorial(3)
This continues until we reach
factorial(1)
, which returns 1 (base case)Then the recursion "unwinds":
1 * 2 * 3 * 4 * 5 = 120
Why Use Recursion?
Recursion offers several advantages:
Elegance: Recursive solutions can be concise and elegant for problems that would be complex to solve iteratively
Divide and conquer: Recursion naturally implements divide-and-conquer strategies, breaking down large data sets into smaller pieces
Tree traversal: For hierarchical data structures like trees, recursion provides a natural approach
Problem decomposition: Complex problems become simpler when broken into smaller, similar sub-problems
Implementing Recursion in Game Data Analysis
Looking at our GameDataManager class, several methods could benefit from a recursive approach. For example, finding games within a specific criteria could be implemented using a binary search algorithm rather than the current linear search.
Here's an example of how we could improve the findGamesByTitle
method using a recursive binary search (assuming the games list is sorted by title):
/**
* Find a game by title using recursive binary search
* @param title the title to search for
* @return the game if found, null otherwise
*/
public GameDataRow findGameByTitle(String title) {
// Sort the games by title first (if not already sorted)
Collections.sort(games, (g1, g2) -> g1.getName().compareTo(g2.getName()));
// Call the recursive helper method
return findGameByTitleHelper(title, 0, games.size() - 1);
}
/**
* Recursive helper method for binary search
*/
private GameDataRow findGameByTitleHelper(String title, int low, int high) {
// Base case: element not found
if (low > high) {
return null;
}
// Calculate middle index
int mid = (low + high) / 2;
// Get the game at the middle
GameDataRow midGame = games.get(mid);
// Compare the middle element
int comparison = midGame.getName().compareTo(title);
// If found, return it
if (comparison == 0) {
return midGame;
}
// If the middle element is greater, search the left half
else if (comparison > 0) {
return findGameByTitleHelper(title, low, mid - 1);
}
// Otherwise, search the right half
else {
return findGameByTitleHelper(title, mid + 1, high);
}
}
The recursive binary search has a time complexity of O(log n), which is much more efficient than the O(n) time complexity of linear search, especially with large datasets.
Another example would be implementing a recursive merge sort:
/**
* Sort games by a certain criteria using merge sort
* @param criteria the sorting criteria (e.g., "sales", "score")
*/
public void sortGamesByMergeSort(String criteria) {
games = mergeSort(games, criteria, 0, games.size() - 1);
}
/**
* Recursive merge sort implementation
*/
private ArrayList<GameDataRow> mergeSort(ArrayList<GameDataRow> list, String criteria, int left, int right) {
// Base case: list of size 1 or empty
if (left >= right) {
ArrayList<GameDataRow> result = new ArrayList<>();
if (left == right) {
result.add(list.get(left));
}
return result;
}
// Find the middle point
int mid = (left + right) / 2;
// Recursively sort both halves
ArrayList<GameDataRow> leftHalf = mergeSort(list, criteria, left, mid);
ArrayList<GameDataRow> rightHalf = mergeSort(list, criteria, mid + 1, right);
// Merge the sorted halves
return merge(leftHalf, rightHalf, criteria);
}
/**
* Merge two sorted lists
*/
private ArrayList<GameDataRow> merge(ArrayList<GameDataRow> left, ArrayList<GameDataRow> right, String criteria) {
ArrayList<GameDataRow> result = new ArrayList<>();
int leftIndex = 0, rightIndex = 0;
// Compare elements from both lists and add the smaller one to result
while (leftIndex < left.size() && rightIndex < right.size()) {
GameDataRow leftGame = left.get(leftIndex);
GameDataRow rightGame = right.get(rightIndex);
boolean addLeft = false;
// Compare based on criteria
if (criteria.equals("sales")) {
addLeft = leftGame.getGlobalSales() >= rightGame.getGlobalSales();
} else if (criteria.equals("score")) {
addLeft = leftGame.getCriticScore() >= rightGame.getCriticScore();
} else { // Default to name
addLeft = leftGame.getName().compareTo(rightGame.getName()) <= 0;
}
if (addLeft) {
result.add(leftGame);
leftIndex++;
} else {
result.add(rightGame);
rightIndex++;
}
}
// Add remaining elements
while (leftIndex < left.size()) {
result.add(left.get(leftIndex++));
}
while (rightIndex < right.size()) {
result.add(right.get(rightIndex++));
}
return result;
}
Your Task
Choose one method from your GameDataRow or GameDataManager class that could benefit from a recursive approach
Reimplement it using recursion
Test your implementation and compare it to the original approach
Document your findings: Was the recursive approach more elegant? More efficient? What challenges did you face?
Alternatively, you can implement a new recursive method that performs a complex analysis that would be difficult to do iteratively. Here are some ideas:
Find the most profitable franchise (games with similar names) recursively
Implement a recursive algorithm to categorize games into hierarchical genres (e.g., "RPG > Action RPG")
Create a recursive method to find "hidden gems" - games with high ratings but low sales
Example: Recursive Method for Maximum Critic Score
Here's an example of how you might implement a recursive method to find the game with the highest critic score within a list of games:
/**
* Find the game with the highest critic score using recursion
*/
public GameDataRow findGameWithHighestCriticScore() {
if (games.isEmpty()) {
return null;
}
return findGameWithHighestCriticScoreHelper(0, games.size() - 1);
}
/**
* Recursive helper to find the game with highest critic score
*/
private GameDataRow findGameWithHighestCriticScoreHelper(int start, int end) {
// Base case: only one game
if (start == end) {
return games.get(start);
}
// Base case: two games
if (start + 1 == end) {
if (games.get(start).getCriticScore() >= games.get(end).getCriticScore()) {
return games.get(start);
} else {
return games.get(end);
}
}
// Find middle index
int mid = (start + end) / 2;
// Recursively find max in both halves
GameDataRow leftMax = findGameWithHighestCriticScoreHelper(start, mid);
GameDataRow rightMax = findGameWithHighestCriticScoreHelper(mid + 1, end);
// Return the game with higher critic score
if (leftMax.getCriticScore() >= rightMax.getCriticScore()) {
return leftMax;
} else {
return rightMax;
}
}
This method uses a divide-and-conquer approach, recursively finding the maximum in each half of the list and then comparing the results. While this specific example might not be more efficient than a simple loop, it demonstrates the recursive pattern that can be applied to more complex problems.
Remember, not all problems are best solved with recursion. Some considerations:
Recursion can lead to stack overflow errors with very large inputs
Recursive solutions sometimes have higher memory usage
Some problems have simpler iterative solutions
Last updated
Was this helpful?