0.11. Multi-Dimensional Arrays and Matrix Algorithms¶
In previous chapters, we used one-dimensional arrays and lists to store linear sequences of values—such as a list of playing cards, an array of daily high temperatures, or a sequence of tweet timestamps. A one-dimensional array can be visualized as a single row or line of values, where each element is accessed by a single integer subscript.
However, many computing problems involve data that is naturally organized in two or more dimensions. Consider a seating chart in a theater, a spreadsheet table with rows and columns, a digital image made of horizontal and vertical pixels, or the game board for checkers, chess, or a puzzle game. In these situations, organizing values into a two-dimensional grid is vastly more natural and intuitive than flattening everything into a single line.
Java provides native support for multi-dimensional arrays—arrays whose elements are themselves arrays. In this chapter, we will explore how to declare, allocate, index, traverse, manipulate, and test two-dimensional arrays and matrices.
0.11.1. Introduction to 2D Arrays & Matrices¶
A two-dimensional array (often called a 2D array or a matrix) is a collection of values organized into a rectangular grid consisting of rows and columns.
To understand why a 2D array is needed, imagine you are developing a scientific application to record precipitation levels across all 12 months of the year, tracking rainfall for each day of every month. If you used a one-dimensional array with 365 elements:
double[] rainfall = new double[365];
it would be cumbersome to determine which index corresponds to March 15th or to compute the average precipitation specifically for the month of July. You would have to manually calculate how many days occurred in prior months to find the correct starting index.
A far clearer and more robust design is to represent the data with a two-dimensional array having one dimension for the month and a second dimension for the day within that month:
double[][] rainfall = new double[12][31];
In this statement:
double[][]indicates that the variablerainfallholds a reference to a two-dimensional array ofdoublevalues.The first bracket pair
[12]specifies the size of the first dimension (12 months).The second bracket pair
[31]specifies the size of the second dimension (31 days per month).
0.11.1.1. Declaring and Allocating 2D Arrays¶
The general syntax for declaring a two-dimensional array reference in Java is:
DataType[][] arrayName;
To allocate the array in memory, we use the new operator, specifying the
dimensions:
arrayName = new DataType[rows][cols];
We can combine declaration and allocation into a single statement:
int[][] seatingChart = new int[15][25]; // 15 rows of 25 seats each
boolean[][] lightGrid = new boolean[8][8]; // 8x8 grid of lights
String[][] timetable = new String[5][8]; // 5 days, 8 time slots per day
Just like one-dimensional arrays, when a 2D array is created with new, all of
its elements are automatically initialized to their default values:
Numeric types (
int,double,byte,short,long,float) are initialized to0or0.0.Boolean types (
boolean) are initialized tofalse.Object reference types (such as
String,Color, or custom classes) are initialized tonull.
Note
Remember that allocating an array of object references (such as
String[][] words = new String[4][6];) creates a grid full of null
references. You must still instantiate each individual object and place it
into the grid cells before invoking methods on those elements, otherwise a
NullPointerException will occur.
0.11.2. Row-Column Indexing & Array-of-Arrays Model¶
To read or modify a specific cell in a two-dimensional array, you provide two subscripts inside individual square bracket pairs:
array[firstIndex][secondIndex]
0.11.2.1. Understanding the Indexing Conventions¶
A critical conceptual insight is that Java itself does not enforce any geometric meaning on the two subscripts. To Java, a 2D array is simply an “array of arrays”: an outer array containing references to several inner arrays.
The interpretation of what the two indices represent is determined entirely by the programmer. In practice, programmers primarily use two distinct indexing conventions depending on the problem domain:
Row-First (Row/Column) Convention:
grid[row][col]In this convention, the first index selects the horizontal row (vertical position, measured downward from the top), and the second index selects the column (horizontal position across that row):
rowranges from0tonumRows - 1.colranges from0tonumCols - 1.
This model matches how people read spreadsheets, tables, and mathematical matrices.
Coordinate-First (Cartesian x/y) Convention:
grid[x][y]In this convention, the first index selects the horizontal position (
x, column index), and the second index selects the vertical position (y, row index):xranges from0towidth - 1(columns).yranges from0toheight - 1(rows).
This model matches graphical coordinate systems and spatial micro-worlds (like the Jeroo island or computer screen pixels).
Important
The programming language does not dictate whether you use
grid[row][col] or grid[x][y]. What matters most is that you choose
one convention and adhere to it consistently throughout your entire
program.
Mixing the two conventions—for example, treating the first index as a row in
one method and as an \(x\)-coordinate in another—is one of the most
frequent sources of bugs in grid-based software, often causing
ArrayIndexOutOfBoundsException whenever the grid is not square.
In this chapter and throughout standard matrix algorithms, we will predominantly
use the row-major convention (grid[row][col]) because it directly
reflects Java’s underlying memory structure.
0.11.2.2. The Array-of-Arrays Memory Model¶
In Java, multi-dimensional arrays are physically structured as arrays of arrays. When you execute:
int[][] matrix = new int[3][4];
Java does not allocate a single monolithic 12-element block. Instead, it performs the following:
It allocates an outer array of 3 elements, where each element is a reference variable capable of pointing to a 1D
int[]array.It allocates three distinct 1D
int[]arrays, each having a length of 4.It assigns the reference of each 4-element array into the corresponding slot of the outer array.
Because of this architecture:
matrix.lengthevaluates to the length of the outer array, which represents the number of rows (here,3).matrix[r].lengthevaluates to the length of the inner array at indexr, which represents the number of columns in rowr(here,4).matrix[r]on its own refers to an entire one-dimensional row array.
Tip
To determine the dimensions of a rectangular 2D array grid:
Number of rows:
grid.lengthNumber of columns:
grid[0].length(assuming at least one row exists)
0.11.2.3. Literal Initializers for 2D Arrays¶
Just as with one-dimensional arrays, you can declare and initialize a 2D array using nested curly braces:
int[][] scores = {
{88, 92, 79},
{95, 84, 91},
{70, 85, 90}
};
Here, scores.length is 3, and each row has length 3. The commas
separate each row, and each row is an array literal enclosed in its own pair of
braces.
0.11.2.4. Ragged (Jagged) Arrays¶
Because each row in a 2D array is an independent object, the individual rows do not all have to be the same length! A multi-dimensional array with rows of different lengths is called a ragged array (or jagged array):
int[][] ragged = {
{1, 2},
{3, 4, 5, 6},
{7, 8, 9}
};
Here, ragged.length is 3, while ragged[0].length is 2,
ragged[1].length is 4, and ragged[2].length is 3. Ragged arrays
can be useful for representing non-rectangular structures, such as a triangle or
sparse data.
0.11.2.5. Higher Dimensions (3D Arrays and Beyond)¶
Java allows you to stack as many dimensions as your problem requires. For example, a 3D array can be visualized as a collection of 2D grids (pages, layers, or depth slices):
int layers = 5;
int rows = 10;
int cols = 20;
double[][][] cube = new double[layers][rows][cols];
0.11.2.6. Multi-Dimensional Lists (Lists of Lists)¶
Just as an array can hold references to other arrays, a List can hold
references to other lists:
List<List<String>> gridOfWords = new ArrayList<>();
While lists of lists offer dynamic resizing, 2D arrays (Type[][]) are
generally preferred for fixed-size grids, board games, and numerical matrices
because of their concise indexing syntax (grid[r][c] vs.
grid.get(r).get(c)) and significantly higher runtime performance.
0.11.3. Traversing 2D Grids: Row-Major vs. Column-Major¶
Processing every element in a two-dimensional array requires two loops: one loop to control the first dimension and a second loop nested inside it to control the second dimension.
0.11.3.1. Nested Loop Mechanics¶
Recall how nested loops execute: for every single iteration of the outer loop, the inner loop executes through its entire cycle from beginning to end.
If an outer loop runs \(R\) times and an inner loop runs \(C\) times, the code inside the body of the inner loop will execute a total of:
For a \(20 \times 10\) grid, the inner loop body runs \(20 \times 10 = 200\) times.
0.11.3.2. Row-Major Traversal¶
In row-major traversal, the outer loop iterates over the rows, and the inner loop iterates over the columns. This visits every element horizontally, row by row, exactly like reading lines of text in a book:
public void printRowMajor(int[][] grid)
{
for (int row = 0; row < grid.length; row++)
{
for (int col = 0; col < grid[row].length; col++)
{
System.out.print(grid[row][col] + "\t");
}
System.out.println(); // newline at the end of each row
}
}
Tracing the execution for a \(3 \times 3\) grid:
row = 0:coliterates0, 1, 2(accesses(0,0),(0,1),(0,2)).row = 1:coliterates0, 1, 2(accesses(1,0),(1,1),(1,2)).row = 2:coliterates0, 1, 2(accesses(2,0),(2,1),(2,2)).
Row-major order is the standard pattern used when printing tabular data, analyzing individual horizontal rows, or searching across a matrix.
0.11.3.3. Column-Major Traversal¶
In column-major traversal, the outer loop iterates over the columns, and the inner loop iterates over the rows. This visits elements vertically, column by column, top-to-bottom:
public void printColumnMajor(int[][] grid)
{
int numRows = grid.length;
int numCols = grid[0].length;
for (int col = 0; col < numCols; col++)
{
for (int row = 0; row < numRows; row++)
{
System.out.print(grid[row][col] + "\t");
}
System.out.println(); // newline at the end of each column
}
}
Tracing the execution for a \(3 \times 3\) grid:
col = 0:rowiterates0, 1, 2(accesses(0,0),(1,0),(2,0)).col = 1:rowiterates0, 1, 2(accesses(0,1),(1,1),(2,1)).col = 2:rowiterates0, 1, 2(accesses(0,2),(1,2),(2,2)).
0.11.3.4. When to Use Each Traversal¶
Use Row-Major when processing horizontal lines or when row data belongs together. For example, in a gradebook array where rows represent students and columns represent assignments, row-major traversal allows you to compute each student’s overall course average.
Use Column-Major when processing vertical lines or vertical slices. In the same gradebook, column-major traversal allows you to compute the class average for each specific assignment across all students.
0.11.4. Matrix Algorithms: Searching & Checking Rows¶
Now that we can traverse grids systematically, let us examine several standard matrix algorithms used to inspect, verify, and reduce grid data.
Consider a reservation system for an auditorium or movie theater represented by a 2D boolean array:
boolean[][] reserved = new boolean[numRows][numCols];
A cell reserved[row][col] is true if that seat is currently booked, and
false if it remains open.
0.11.4.1. Algorithm 1: Checking Full-Row Occupancy¶
Suppose the venue offers a discount when an entire row is completely booked by a
single group. We need a method isRowFull(boolean[][] grid, int row) that
returns true if and only if every cell in that specified row is true:
public boolean isRowFull(boolean[][] grid, int row)
{
// Inspect each column in the given row:
for (int col = 0; col < grid[row].length; col++)
{
// If we find any seat that is NOT occupied, the row is not full
if (!grid[row][col])
{
return false; // Short-circuit early exit
}
}
// If the loop finished without finding any empty seat, all are occupied
return true;
}
Notice the early return design pattern: as soon as the algorithm encounters
a single empty cell, it immediately returns false. The method only returns
true after all columns have been inspected.
0.11.4.2. Algorithm 2: Counting Occupied Seats Across the Entire Matrix¶
To count how many total seats in the venue are booked, we combine an accumulator variable with a nested row-major loop:
public int countTotalOccupied(boolean[][] grid)
{
int count = 0;
for (int row = 0; row < grid.length; row++)
{
for (int col = 0; col < grid[row].length; col++)
{
if (grid[row][col])
{
count++;
}
}
}
return count;
}
0.11.4.3. Algorithm 3: Linear Search in a 2D Matrix¶
Suppose an office building organizes employee mailboxes in a 2D array of names:
String[][] mailboxes. To find whether an employee has a mailbox and determine
their row and column position:
public int[] findMailbox(String[][] grid, String targetName)
{
if (targetName == null)
{
return null;
}
for (int row = 0; row < grid.length; row++)
{
for (int col = 0; col < grid[row].length; col++)
{
if (targetName.equals(grid[row][col]))
{
return new int[] { row, col }; // Found at (row, col)
}
}
}
return null; // Target name was not present in the grid
}
0.11.4.4. Algorithm 4: Clearing an Entire Row¶
When all reservations in a row are cancelled, we reset every cell in that row
back to false:
public void clearRow(boolean[][] grid, int row)
{
for (int col = 0; col < grid[row].length; col++)
{
grid[row][col] = false;
}
}
0.11.5. Row Shifting & Grid Transformations¶
Many grid-based systems—such as falling sand simulators, warehouse shelving management, and puzzle games—require moving entire rows of data upward or downward within a matrix.
Consider a warehouse storage rack represented as a 2D array:
int[][] storageRack = new int[numTiers][numBins];
Here, numbers represent product identifiers stored in bins on each shelf tier,
and 0 represents an empty bin.
Suppose that when an entire shelf tier is cleared or retrieved, all higher tiers must shift downward by one position so inventory remains accessible, and the topmost tier becomes completely vacant.
0.11.5.1. The Direction of Iteration: Avoiding Data Overwrite¶
Suppose shelf row 3 is cleared, and we want all rows above it (rows 0, 1, and 2) to shift downward into rows 1, 2, and 3:
Row 2 moves into Row 3.
Row 1 moves into Row 2.
Row 0 moves into Row 1.
Row 0 becomes empty.
Consider what would happen if we wrote a loop that iterates downward from the top (from row 1 to row 3):
// BUGGY ATTEMPT: Iterating in the wrong direction!
for (int r = 1; r <= clearedRow; r++)
{
for (int c = 0; c < grid[r].length; c++)
{
grid[r][c] = grid[r - 1][c]; // Overwrites row r with row r - 1
}
}
Let us trace this buggy loop:
When
r = 1: Row 1 is replaced by the contents of Row 0. Now Row 1 holds Row 0’s data.When
r = 2: Row 2 is replaced by Row 1. But Row 1 was already overwritten with Row 0! So Row 2 now holds Row 0’s data!When
r = 3: Row 3 is replaced by Row 2, which also holds Row 0’s data!
The entire grid becomes a duplicate copy of Row 0!
To prevent overwriting data before it has been copied, we must iterate in reverse order, starting at the cleared row and working upward toward row 1:
public void shiftRowsDownward(int[][] grid, int clearedRow)
{
// Step backward from clearedRow up to row 1:
for (int r = clearedRow; r > 0; r--)
{
// Copy each element of row r - 1 into row r:
for (int c = 0; c < grid[r].length; c++)
{
grid[r][c] = grid[r - 1][c];
}
}
// Reset the top row (row 0) to empty values:
for (int c = 0; c < grid[0].length; c++)
{
grid[0][c] = 0;
}
}
Important
When shifting elements downward in an array, always iterate from bottom to top so that each row is safely moved before the row above it overwrites it. Conversely, when shifting elements upward, always iterate from top to bottom.
0.11.5.2. Deep Copying vs. Row Reference Swapping¶
In Java, because a 2D array is an array of 1D row references, you might be tempted to write:
// DANGEROUS: Reference aliasing!
grid[r] = grid[r - 1];
While this syntactically compiles, it copies the reference to the row, not
the individual values! As a result, grid[r] and grid[r - 1] now point to
the exact same array object in memory (aliasing). Later modifying a cell in one
row will silently alter the other row as well.
To prevent aliasing bugs, always copy the individual elements in an inner loop as
shown in shiftRowsDownward, or allocate a brand-new array for each shifted
row.
0.11.6. Coordinate Mapping & Offset Arithmetic¶
Real-world applications frequently operate with domain measurements that do not naturally begin at index 0. For example:
Hours of the day may run from 8:00 AM to 5:00 PM (hours 8 to 17).
Year values may span from 2020 to 2030.
Game objects may occupy multiple cells defined by relative offsets from a central anchor point.
To connect real-world data with zero-based arrays, we use coordinate mapping and offset arithmetic.
0.11.6.1. Domain-to-Index Offset Translation¶
Suppose you are building a weekly scheduling application. Business hours run from 8:00 AM to 5:00 PM (hours 8 through 17, for a total of 10 hourly slots). Days of the week are numbered 0 through 6 (Monday through Sunday).
We can represent the schedule using a 2D array:
String[][] weeklyPlanner = new String[7][10]; // 7 days, 10 hour slots
Because valid row indices are 0 through 9, an appointment at hour 8
cannot be placed directly at index 8 without wasting slots 0 through 7.
Instead, we translate the domain hour into an array index using an offset:
public static final int START_HOUR = 8; // 8:00 AM corresponds to index 0
public int hourToIndex(int hour)
{
return hour - START_HOUR;
}
public int indexToHour(int index)
{
return index + START_HOUR;
}
For an appointment on Wednesday (day 2) at 2:00 PM (hour 14):
Day index:
2Hour index:
14 - 8 = 6Target cell:
weeklyPlanner[2][6]
0.11.6.2. Multi-Cell Shapes & Relative Offsets¶
In graphic design and grid-based puzzle software, shapes are frequently composed of multiple neighboring cells placed relative to an anchor coordinate:
// A plus-shaped pattern centered at an anchor:
// (r-1, c)
// (r, c-1) (r, c) (r, c+1)
// (r+1, c)
Rather than hardcoding absolute grid coordinates, we define the shape as an
array of relative row and column offsets (dr, dc) from the anchor position:
int[][] plusPatternOffsets = {
{0, 0}, // center anchor
{-1, 0}, // one step up
{1, 0}, // one step down
{0, -1}, // one step left
{0, 1} // one step right
};
To stamp or place this pattern onto a grid with its center at (anchorRow,
anchorCol):
For each offset
{dr, dc}, compute:\[\text{targetRow} = \text{anchorRow} + dr\]\[\text{targetCol} = \text{anchorCol} + dc\]Validate that the target coordinate falls strictly within the grid bounds:
targetRow >= 0 && targetRow < numRows && targetCol >= 0 && targetCol < numCols
Only modify the grid cells after verifying that all target cells are valid and unoccupied.
0.11.6.3. 1D-to-2D and 2D-to-1D Coordinate Translation¶
Sometimes high-performance graphics engines flatten a two-dimensional grid of dimensions \(\text{ROWS} \times \text{COLS}\) into a single 1D array of size \(\text{ROWS} \cdot \text{COLS}\).
You can convert between the two representations using integer division and the modulo operator:
From 2D to 1D index:
\[\text{index} = (\text{row} \times \text{COLS}) + \text{col}\]From 1D index back to 2D coordinates:
\[\text{row} = \text{index} / \text{COLS}\]\[\text{col} = \text{index} \% \text{COLS}\]
0.11.7. Testing 2D Matrix Classes in JUnit¶
Testing classes that manage two-dimensional grids requires verifying both cell states and structural postconditions.
0.11.7.1. Constructing Synthetic Test Fixtures¶
When writing automated unit tests, create small, focused test grids (such as \(3 \times 3\) or \(4 \times 5\)) so you can manually verify expected outputs and corner cases without overwhelming complexity:
public class GridManagerTest extends TestCase
{
private boolean[][] testGrid;
public void setUp()
{
// Set up a fresh 3x3 grid before each test method
testGrid = new boolean[3][3];
}
public void testIsRowFull()
{
GridManager manager = new GridManager();
// Initially empty: row 1 should not be full
assertThat(manager.isRowFull(testGrid, 1)).isFalse();
// Fill cells (1, 0) and (1, 1), but leave (1, 2) false
testGrid[1][0] = true;
testGrid[1][1] = true;
assertThat(manager.isRowFull(testGrid, 1)).isFalse();
// Fill the last cell in row 1
testGrid[1][2] = true;
assertThat(manager.isRowFull(testGrid, 1)).isTrue();
}
}
0.11.7.2. Verifying Grid Transformations Before and After¶
When testing methods that transform a grid (such as shifting rows, clearing lines, or stamping patterns), verify the state of affected cells, the state of newly introduced boundary cells, and the state of untouched rows:
public void testShiftRowsDownward()
{
int[][] rack = new int[3][3];
rack[0][0] = 10;
rack[0][1] = 20;
rack[1][0] = 30;
rack[1][1] = 40;
rack[2][0] = 50; // cleared row
rack[2][1] = 60;
GridManager manager = new GridManager();
manager.shiftRowsDownward(rack, 2);
// Row 2 should now contain former contents of Row 1:
assertThat(rack[2][0]).isEqualTo(30);
assertThat(rack[2][1]).isEqualTo(40);
// Row 1 should now contain former contents of Row 0:
assertThat(rack[1][0]).isEqualTo(10);
assertThat(rack[1][1]).isEqualTo(20);
// Row 0 must be reset to zero:
assertThat(rack[0][0]).isEqualTo(0);
assertThat(rack[0][1]).isEqualTo(0);
assertThat(rack[0][2]).isEqualTo(0);
}
0.11.7.3. Testing Boundary and Edge Conditions¶
Always construct test cases that exercise the matrix boundaries:
Top and Bottom Rows: Accessing and transforming row
0and rownumRows - 1.Left and Right Columns: Accessing and transforming column
0and columnnumCols - 1.Index Bounds Checking: Verifying that passing coordinates outside valid ranges (negative indices or indices \(\ge\) length) are properly handled or rejected without throwing an unhandled
ArrayIndexOutOfBoundsException.
