SparseArray: 2015 FRQ 3
A step-by-step solution to the 2015 AP CSA FRQ 3 (SparseArray), covering looking up and removing entries stored in an ArrayList-backed sparse array in Java.
Representing a mostly-empty grid without wasting memory on all those zeros is the idea behind this AP Computer Science A free-response question — you look up and remove entries from a list-backed sparse array instead of a full two-dimensional array.
What This FRQ Tests
- AP CSA units: Unit 7 (ArrayList)
- Core skill: searching an
ArrayListof objects for a match based on more than one field at once - Secondary skill: safely removing and updating elements of an
ArrayListwhile iterating over it, without corrupting the indices still left to check - Official category: this year's FRQ 3 tests Array/ArrayList content (Unit 7), matching what's now the modern FRQ-3 slot — though as with every pre-2019 exam, that alignment isn't guaranteed and is confirmed here by the question's actual content, not assumed from its number alone.
The Setup
SparseArrayEntry(given, complete, and immutable — it "cannot be modified after it has been constructed") represents one non-zero value in the grid:int getRow(),int getCol(),int getValue()
SparseArrayrepresents the whole grid:private int numRows,private int numColsprivate List<SparseArrayEntry> entries— only the non-zero elements are stored, in no particular order
- You're asked to write two methods:
getValueAt(int row, int col)— looks up the value at a given position, or0if nothing is stored thereremoveColumn(int col)— deletes an entire column, shifting every entry to its right one column to the left
Part (a): Writing getValueAt(int row, int col)
Step-by-Step Approach
- Loop through every entry currently in the list.
- For each one, check whether its row and its column both match what was asked for.
- If they match, return that entry's value immediately.
- If the loop finishes without finding a match, the position must be an implicit zero — return
0.
The Code
public int getValueAt(int row, int col)
{
for (int i = 0; i < entries.size(); i++)
{
SparseArrayEntry entry = entries.get(i);
if (entry.getRow() == row && entry.getCol() == col)
{
return entry.getValue();
}
}
return 0;
}
Why Each Piece Matters
- Both
getRow()andgetCol()have to match — checking only one would return the value from the wrong entry whenever two entries happen to share a row, or share a column. - Returning inside the loop as soon as a match is found avoids needlessly checking the remaining entries.
return 0after the loop is what makes "no entry found" mean the same thing as "this position holds zero" — exactly how a sparse array is supposed to behave, since only non-zero values get stored at all.
Tracing the Example
Using the question's own sample sparse object, whose entries list holds (row, col, value) triples (1, 4, 4), (2, 0, 1), (3, 1, -9), (1, 1, 5):
sparse.getValueAt(3, 1)— checks(1,4,4)(no match),(2,0,1)(no match),(3,1,-9)(row and column both match) → returns-9sparse.getValueAt(3, 3)— no entry in the list has both row3and column3(the only row-3entry has column1) → loop finishes → returns0
Both results match the question exactly.
Common Mistakes to Avoid
- Checking only the row or only the column, not both together in the same condition.
- Returning
0the moment a single non-matching entry is checked, instead of waiting until the entire list has been searched. - Assuming an empty
entrieslist is a special case that needs its own check — it isn't. The loop simply never executes, and0is returned by default, which is exactly correct for an all-zero array.
Part (b): Writing removeColumn(int col)
The Rule, Broken Down
- Every entry whose column matches
colis deleted entirely — that column no longer exists. - Every entry whose column is greater than
colneeds its column index reduced by one, since everything to the right of the removed column shifts left. - Entries whose column is less than
colare left untouched. numColsitself has to shrink by one, to reflect the missing column.
Step-by-Step Approach
- Since
SparseArrayEntryobjects are immutable, an entry that needs a new column can't have its field reassigned — it has to be replaced with a brand-newSparseArrayEntry, put back into the same list position withset. - Walk through the list, checking each entry's column against
col. - An exact match gets removed from the list entirely.
- A column greater than
colgets replaced with a new entry — same row, same value, column minus one. - Reduce
numColsby one, exactly once, after the list has been fully processed. - The trickiest part: removing elements from an
ArrayListwhile looping over it in the normal forward direction skips entries, because everyremovecall shifts everything after it one position to the left. Looping backward instead — from the last index down to0— sidesteps this entirely, since any entry already visited never moves, no matter what gets removed further down the list.
The Code
public void removeColumn(int col)
{
for (int i = entries.size() - 1; i >= 0; i--)
{
SparseArrayEntry entry = entries.get(i);
if (entry.getCol() == col)
{
entries.remove(i);
}
else if (entry.getCol() > col)
{
SparseArrayEntry shifted =
new SparseArrayEntry(entry.getRow(), entry.getCol() - 1, entry.getValue());
entries.set(i, shifted);
}
}
numCols--;
}
Why Each Piece Matters
- Looping backward (
i--, starting atentries.size() - 1) is the detail that makes the whole method correct. Removing indexionly shifts elements at positions afteri— and in a backward loop, those have already been checked. A forward loop would instead skip whichever entry slides into a just-removed position. - Building a new
SparseArrayEntryinstead of modifying the existing one — the problem states directly that these objects "cannot be modified after it has been constructed," so there's no setter to call even if one seemed convenient. entries.set(i, shifted)replaces the old entry in place at the same index, rather than removing it and adding a new one at the end (which would scramble the list's order for no reason).numCols--happens once, outside the loop — it's a property of the array as a whole, not something tied to any individual entry.
Tracing the Example
Walking backward through the question's sample list — entries (1, 4, 4) at index 0, (2, 0, 1) at index 1, (3, 1, -9) at index 2, (1, 1, 5) at index 3 — for the call sparse.removeColumn(1):
| Index visited | Entry (row, col, value) | Column vs. 1 | Action |
|---|---|---|---|
| 3 | (1, 1, 5) | equal | removed |
| 2 | (3, 1, -9) | equal | removed |
| 1 | (2, 0, 1) | less | unchanged |
| 0 | (1, 4, 4) | greater | replaced with (1, 3, 4) |
Final list: {(1, 3, 4), (2, 0, 1)}, and numCols drops from 5 to 4 — matching the question's expected result exactly (the question itself notes the order within the list doesn't matter).
Common Mistakes to Avoid
- Looping forward while removing. This is the single most common bug on exactly this style of
ArrayListproblem — it's specifically why this solution loops backward instead. - Trying to call something like
entry.setCol(...). There is no such method;SparseArrayEntryis immutable by design, so a new object is the only option. - Mixing up
>and==. An entry exactly at the removed column gets deleted, not shifted — only entries strictly greater thancolget their column reduced. - Decrementing
numColsinside the loop, which would subtract more than 1 total if the loop body ever ran more than once.
Key Takeaways
- When removing elements from an
ArrayListwhile iterating over it by index, loop backward — it's the simplest way to guarantee removals never cause an element to get skipped. - An immutable object can't be edited in place; "changing" one of its fields really means constructing a new object and swapping it in, often with
set. - Searching a list of objects for a match on multiple fields at once (
rowandcoltogether, never either alone) is worth double-checking any time similar-but-not-identical data could exist side by side in the same collection.