Skip to content

7.4 Two-Dimensional Arrays

Key terms: two-dimensional array, ragged array

7.4.1 Motivation

The Game of Fifteen, which dates back to the 19th century, is played on a 4×4 grid of tiles numbered from 1 to 15. A tile adjacent to the empty space can be moved by sliding it horizontally or vertically into the space. The goal is to rearrange the tiles in ascending order.

Figure 7.4.1a: Random and Winning Configurations

In a program presenting the game to a player, how should the game state be represented? One idea might be to declare 16 integer variables for the 16 different grid positions. Each variable could store either a tile number or a value representing the empty space. But with this approach the variables do not reflect the structure of the game, so checking if a particular move is valid would require a long and tedious sequence of decision statements. Alternatively, the tile numbers could be stored in a linear array, using 16 to denote the empty space:

Figure 7.4.1b: Game State with Linear Array

This is a little more intuitive but still problematic. The game state is naturally viewed as a two-dimensional (2D) grid. If the tile numbers are stored in a linear array, the programmer must mentally translate the two-dimensional adjacency relationships into the one-dimensional structure of the array. A language-level structure is needed that more naturally matches the conceptual structure of the game. The same need arises in many applications, such as spreadsheets, in which information is organized as a grid, matrix, or table.

The following code shows how to declare a 2D array to store the game state. Note that the type name for a 2D array is the same as for a linear array except that there is a second pair of brackets for the second dimension. The first dimension specifies the number of rows, and the second specifies the number of columns.

int[][] grid = new int[4][4];

A program for the Game of Fifteen might initialize the grid by first putting it into the winning state and then working backwards to a shuffled starting state by making a series of legal random moves — that is, by repeatedly sliding a tile chosen at random from among those that are adjacent to the empty space. This guarantees that the initial configuration is solvable, since reversing the same sequence of moves returns the puzzle to the winning configuration. The first part of this initialization, putting the grid into the winning state, could be coded as shown below. The outer loop ranges over the row indices, and for each row the inner loop ranges over the column indices. Successive tile numbers are assigned to grid[i][j], which refers to row i and column j.

// initialize grid to the winning configuration 
int tile = 1; 
for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 4; j++) { 
        grid[i][j] = tile++;
    } 
}

7.4.2 Arrays of Arrays

A computer’s memory is really a vast linear array of bytes, so how exactly should a 2D array be understood in terms of physical storage? The answer is that a 2D array is implemented as an array whose elements are themselves arrays. Thus, although Java provides convenient syntax for working with two-dimensional arrays, the underlying representation is really an array of arrays. To make this clear, the code below shows how an initializer list could be used to put grid into the winning state (compare with the preceding loop-based initialization). Recall that an initializer list is a comma-separated sequence of array elements inside curly braces. Here the elements of the outer array are themselves arrays representing the rows of grid, each initialized with its own initializer list.

int[][] grid = {
        {1, 2, 3, 4}, 
        {5, 6, 7, 8}, 
        {9, 10, 11, 12}, 
        {13, 14, 15, 16}
};

Figure 7.4.2: How the Array is Represented in Memory

The following code shows how to traverse a 2D array with a for-each loop. The outer loop iterates over the rows of grid, each of which is a linear array. For each row, the inner loop iterates over its int elements.

// output the grid in tabular format 
for (int[] row : grid) { 
    for (int tile : row) {
        System.out.printf("%2d ", tile); 
    }
    System.out.println(); // insert newline after each row 
}

Listing 7.4.2a brings these ideas together in a simple program.

Listing 7.4.2a - AnimalTable.java

AnimalTable.java
package chap07.sect4;

import java.util.Arrays;

/**
 * Displays a table of animal names before and after sorting each row.
 *
 * @author Drue Coles
 */
public class AnimalTable {

   public static void main(String[] args) {
      String[][] animals = {
         {"gorilla", "buffalo", "lobster", "cheetah"},
         {"penguin", "quetzal", "meerkat", "octopus"},
         {"vulture", "whippet", "raccoon", "wallaby"}
      };

      System.out.println(toString(animals));

      for (String[] row : animals) {
         Arrays.sort(row);
      }
      System.out.println(toString(animals));
   }

   /**
    * Returns a string representation of a 2D array with rows separated by newlines.
    */
   private static String toString(String[][] arr) {
      String result = "";
      for (String[] row : arr) {
         for (String item : row) {
            result += item + " ";
         }
         result += "\n"; // end of row
      }
      return result;
   }
}
Output 7.4.2a
gorilla buffalo lobster cheetah 
penguin quetzal meerkat octopus 
vulture whippet raccoon wallaby 

buffalo cheetah gorilla lobster 
meerkat octopus penguin quetzal 
raccoon vulture wallaby whippet 

Listing 7.4.2b illustrates a graphical application of 2D arrays. The program implements a simple mathematical process known as Rule 90. Starting with a single 1 in the center of the first row, each subsequent row is computed from the previous one according to a fixed rule. The rows therefore represent successive time steps, while the columns represent positions in space. Displaying the completed array reveals that Rule 90 provides an alternative construction of the Sierpiński triangle (see Section 6.2.3).

Listing 7.4.2b - Rule90.java

Rule90.java
package chap07.sect4;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

/**
 * Displays a time-space diagram of a sequence of bits updated by Rule 90. At discrete steps, each
 * bit becomes the exclusive-or of its two neighbors. The initial configuration consists of a 1 in
 * the middle and 0s everywhere else. From this configuration, a structured pattern emerges over
 * time.
 *
 * @author Drue Coles
 */
public class Rule90 extends Application {

    @Override
    public void start(Stage stage) {
        final int width = 800;
        final int height = 385;
        Pane root = new Pane();
        Scene scene = new Scene(root, width, height, Color.LIGHTSTEELBLUE);

        // drawing constants
        final int radius = 3;
        final int diameter = 2 * radius;
        final int numCells = width / diameter;

        // Computes the space-time history of a sequence of bits evolving according to Rule 90.
        // Row k represents the sequence at time k. The sequence starts with a 1 in the center
        // and 0s elsewhere.
        int[][] grid = spaceTimeHistoryRule90(numCells);

        // display the grid
        for (int row = 0; row < numCells; row++) {
            for (int col = 0; col < numCells; col++) {
                if (grid[row][col] == 1) {
                    int x = diameter + col * diameter;
                    int y = diameter + row * diameter;
                    root.getChildren().add(new Circle(x, y, radius));
                }
            }
        }

        stage.setTitle("Rule 90");
        stage.setScene(scene);
        stage.show();
    }

    /**
     * Creates and returns an n x n array of bits, initializing the first row with a 1 in the center
     * and 0s everywhere else. Each subsequent row is obtained from the previous one by applying
     * Rule 90.
     */
    private static int[][] spaceTimeHistoryRule90(int n) {
        int[][] grid = new int[n][n];
        grid[0][n / 2] = 1; // initial condition: a single 1 in the center

        for (int i = 1; i < n; i++) { // fill rows 1, 2, ..., n-1
            for (int j = 0; j < n; j++) { // calculate the j-th bit in row i
                int left = get(grid, i - 1, j - 1);
                int right = get(grid, i - 1, j + 1);
                grid[i][j] = left ^ right; // Rule 90 (XOR)
            }
        }
        return grid;
    }

    /**
     * Returns grid[i][j] if j is in bounds, and 0 otherwise.
     */
    private static int get(int[][] grid, int i, int j) {
        if (j < 0 || j >= grid[i].length) {
            return 0;
        }
        return grid[i][j];
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Output 7.4.2b

Output 7.4.2b – Rule 90

7.4.3 Ragged Arrays

Occasionally, a programmer needs a 2D array whose rows have different lengths. This is called a ragged array. Since a 2D array is an array of arrays, declaring a ragged array is simple. Only the number of rows is specified in the declaration. Each row is then created separately and may have a different length. For example:

int[][] x = new int[3][]; 
x[0] = new int[8]; 
x[1] = new int[10]; 
x[2] = new int[6];

x[1][6] = 97;

Figure 7.4.3: Representation in Memory

7.4.4 Multidimensional Arrays

The syntax for multidimensional arrays is a straightforward extension of the 2D case: an extra set of brackets is needed for each extra dimension. As a practical example, consider a program that stores daily temperatures recorded at a particular location throughout the twentieth century. The following statement declares a 3D array for this purpose.

int[][][] temperature = new int[13][32][100]; 

The three dimensions correspond to month, day, and year. The first index represents the month and ranges from 1–12, the second represents the day and ranges from 1–31, and the third represents the year and ranges from 0–99. Index 0 is unused for the month and day dimensions. In this scheme, the temperature on June 18, 1986, would be stored at temperature[6][18][86].

If hourly rather than daily temperatures were needed, a fourth dimension could represent the hour of the day.