Lecture 26 - Multi-Dimensional Arrays

Agenda

Announcements

Multi-Dimensional arrays

We have seen that we can create arrays to hold objects of any type, either basic data types like int and double, or instances of objects such as Image and DrawableInterface. Nothing stops us from defining arrays of arrays. To declare an array, each of whose elements is an array of int:

  int[][] twoDArray;

While it is normally written without parentheses, we can think of the above declaration as defining twoDArray as having type (int []) []. Thus each element of twoDArray is an array of ints.

Despite the fact that Java will treat this as an array of arrays, we usually think about this as a two-dimensional array, with the elements arranged in a two-dimensional table so that twoDArray[i][j] can be seen as the element in the ith row and jth column. For example here is the layout for a two-dimensional array a with 6 rows (numbered 0 to 5) and 4 columns:

0 1 2 3
0 a[0][0] a[0][1] a[0][2] a[0][3]
1 a[1][0] a[1][1] a[1][2] a[1][3]
2 a[2][0] a[2][1] a[2][2] a[2][3]
3 a[3][0] a[3][1] a[3][2] a[3][3]
4 a[4][0] a[4][1] a[4][2] a[4][3]
5 a[5][0] a[5][1] a[5][2] a[5][3]
Viewed in this way, our two-dimensional array is a grid, much like a map or a spreadsheet. This is a natural way to store things like tables of data or matrices.

We access elements of two-dimensional arrays in a manner similar to that used for one dimensional arrays, except that we must provide both the row and column to access an element, giving the row number first.

We create a two-dimensional array by providing the number of rows and columns. Thus we can create the two-dimensional array above by writing:

   int[][] a = new int[6][4];

Of course, in real life, we would usually define constants for the number of rows and the number of columns.

A nested for loop is the most common way to access or update the elements of a two-dimensional array. One loop walks through the rows and the other walks through the columns. For example, if we wanted to assign a unique number to each cell of our two-dimensional array, we could do the following:

    for (int row = 0; row < 6; row++) {
        for (int col = 0; col < 4; col++) {
            a[row][col] = 4*row + col + 1;
        }
    }

This assigns the numbers 1 through 24 to the elements of array a. The array is filled by assigning values to the elements in the first row, then the second row, etc. and results in:

 1  2  3  4
 5  6  7  8
 9 10 11 12
13 14 15 16
17 18 19 20
21 22 23 24

You could modify the above to be slightly more interesting by computing a multiplication table.

We could just as well process all the elements of column 0 first, then all of column 1, etc., by swapping the order of our loops:


    for (int col = 0; col < 4; col++)
        for (int row = 0; row < 6; row++)
            ...

For the most part, it doesn't matter which order you choose, though for large arrays it is generally a good idea to traverse the array in the same order that your programming language will store the values in memory. For Java (and C, C++), the data is stored by rows, known as row major order. However, an two-dimensional array in FORTRAN is stored in column major order. You will certainly see this again if you go on and take courses like Computer Organization or Operating Systems.

Magic Squares

Let's look at a program that generates "magic squares" and stores them in two-dimensional arrays. A square array of numbers is said to be a "magic square" if all the rows, columns, and diagonals add up to the same number. For example, here is a 3 ×3 magic square:

   4 9 2
   3 5 7
   8 1 6

Each row, column, and diagonal of this square add up to 15.

Using for loops to check magic squares

Suppose we are given a two-dimensional array of int values

   int[][] magicIntArray = new int[SIZE][SIZE];

that has been filled with values, and we want to determine if it is a magic square.

We need to verify that the sum of each row, each column, and each diagonal, are the same. We begin by computing the sum of the first row, so we can compare it to other sums we compute.

    int firstSum = 0;

    for (col = 0; col < SIZE; col++) {
       firstSum = firstSum + magicIntArray[row][col];
    }
    System.out.println ("The sum in the first row = " + firstSum);

Next, we compute the sums of the other rows and make sure they match the first. We print a message if the sum is different than what we expect, and set a boolean flag to false, indicating that we have determined that the array is not a valid magic square:

  for (int row = 1; row < SIZE; row++) {  // check sum of each row
    sum = 0;
    for (col = 0; col < SIZE; col++) {
      sum = sum + magicIntArray[row][col];
    }
    if (sum != firstSum) {
      System.out.println("The sum of elements in row "+row+" is "+ sum);
      isMagicSquare = false;
    }
  }

Now, the columns. We need to do our loops in the opposite nesting order.

   for (col = 0; col < SIZE; col++) {      // check sum of each column
      sum = 0;
      for (row = 0; row < SIZE; row++) {
        sum = sum + magicIntArray[row][col];
      }
      if (sum != firstSum) {
        System.out.println("The sum of elements in column "+col+" is "+ sum);
        isMagicSquare = false;
      }
   }

Finally, we check the two diagonals, requiring just one loop for each diagonal and no nesting:

   sum = 0;
   for (int diag = 0; diag < SIZE; diag++) { // check sum of major diagonal
     sum = sum + magicIntArray[diag][diag];
   }
   if (sum != firstSum) {
     System.out.println("The sum of elements in the main diagonal is "+ sum);
     isMagicSquare = false;
   }
       
   sum = 0;
   for (int diag = 0; diag < SIZE; diag++) { // check sum of minor diagonal
     sum = sum + magicIntArray[diag][SIZE-diag-1];
   }
   if (sum != firstSum) {
     System.out.println("The sum of elements in the reverse diagonal is "+ sum);
     isMagicSquare = false;
   }

Finally, we report the answer - isMagicSquare is false only if one of the checks found a row, column, or diagonal, with a sum not equal to the sum of the first row.

   if (isMagicSquare) {
     System.out.println("This is a magic square!");
   } else {
     System.out.println("This is not a magic square!");
   }

Creating Magic Squares

While it is certainly not obvious, it turns out that there is a straightforward algorithm that will generate a magic square whose width is an odd number. Begin with a 1 in the center of the bottom row, then fill in the remainder of the array by the following rules:

  1. Try to place the next integer (one greater than the last one you placed) in the cell one slot below and one slot to the right of the last place filled. If you fall off the bottom of the array, go to the top row. If you fall off the right edge of the array, go to the leftmost column. If that cell is empty, write the next integer there and continue.
  2. If the cell found above is full, go back to where you wrote the last integer and write the next integer in the cell directly above it.

Let's build the 3 ×3 example in an array named magicIntArray. The 1 is placed in magicIntArray[2][1]. We try to place the 2 one down and one to the right. Since we fall off the bottom of the array, we wrap around to the top row and place the 2 in magicIntArray[0][2]. We move down and to the right to place the 3, but when we fall off of the right edge, we go around to the left-most column (0) and place the 3 in magicIntArray[1][0]. We try to place the 4 down and to the right at magicIntArray[2][1], but that cell is filled. We apply the second rule and place the 4 in the cell directly above the one where we placed the 3, in magicIntArray[0][0]. The 5 and 6 go to the right and below. When we try to place the 7, we wrap around both the right edge and the bottom, sending us to the already occupied top left. Thus we place the 7 directly above the 6, then 8 and 9 go down and to the right, wrapping around as needed.

Demo: Magic Square of Buttons

The demo above builds a Magic Square of a given size, and uses buttons on the screen to display them. We'll look at various parts of the demo below.

First of all, our program does not extend WindowController, since we do not need a canvas. In fact, we do not use any objectdraw objects or methods, so we don't even import objectdraw.*; Instead, we extend Applet, a Java class that will create a window for us where we can add AWT components. Instead of a begin method, our program starts in the init method, though the idea is the same.

Back to the actual magic square functionality, consider the method buildArray, that uses the algorithm described above to generate our magic square.

We initialize the array to all 0's, so we can easily tell if a cell in the array has been given a value yet or not:

   // Initialize to all 0s
   for (int row = 0; row < SIZE; row++) {
     for (int col = 0; col < SIZE; col++) {
       magicIntArray[row][col] = 0;
     }
   }

We are going to place consecutive integer values into cells in the array, beginning with 1. We might consider a nested for loops, adding the values at each position in the same order we used for initialization. But think about our algorithm. It places values in the array starting with 1 and going up to the total number of cells. This sounds like a job for a for loop from 1 to SIZE*SIZE:

    for (int num = 1; num <= SIZE * SIZE; num++) {
      //  insert num into the next cell
    }

The algorithm begins by placing the 1 in the center column of the bottom row, so we initialize row and column indicies to those values:

    int row = SIZE - 1;             // row and column for placing "1"
    int col = SIZE / 2;

Notice that SIZE/2 gives us the middle column since we know that SIZE is odd. When we do integer division, the fractional part is dropped, giving 5 / 2 = 2, for example.

Now, assuming that row and col are updated each time around the loop, we can refine our design to:

   for (int num = 1; num <= SIZE * SIZE; num++) {
      magicIntArray[row][col] = num;
      
      // compute next value for row and col
   }

The next step is to compute the next value for the row and the column. The algorithm says to try placing the next value down and to the right. This corresponds to an increment the row and the column indices. If that puts us outside the grid, we want to wrap around to the first row/column. We accomplish this with modulo arithmetic, just like the slide show example.

   // go to right and down, wrapping if necessary
   int newRow = (row + 1) % SIZE;                     
   int newCol = (col + 1) % SIZE;

   // if empty next number goes here
   if (magicIntArray[newRow][newCol] == 0) {
     row = newRow;
     col = newCol;
   } else {
     // use the cell above the previous cell
   }

Now we just need to take care of the case where the cell position computed above already contains a number. Then, we put the next number in the cell above our previous number - at row-1+SIZE%SIZE.

Here is the complete method for filling in the array:

private void fillArray() {
  // Could initialize to all 0s, but not necessary
       
  // Set the indices for the middle of the bottom row
  int row = SIZE - 1;             
  int col = SIZE / 2;
        
  // Fill each element of the array using the magic array
  for (int num = 1; num <= SIZE*SIZE; num++) {
    magicIntArray[row][col] = num;

    // Find the next cell, wrapping around if necessary.
    int newRow = (row + 1) % SIZE;                      
    int newCol = (col + 1) % SIZE;

    // If the cell is empty, remember those indices for the
    // next assignment.
    if (magicIntArray[newRow][newCol] == 0) {
      row = newRow;
      col = newCol;
    } else {             
      // The cell was full.  Use the cell above the previous one.
      row = (row - 1 + SIZE) % SIZE;
    }
  }
}

Notice that we need to introduce the variables newRow and newCol instead of modifying row and col directly so that we still have the previous values in case that cell already contains a value.

Using Multiple Listening Objects

You may have noticed that the cells in our magic square demo look like buttons. They are, but without listeners, they can't do anything. Suppose that we wanted to verify that the labels on the buttons matched the values in our arrays. We could write listeners for our buttons so that when a button is clicked it reports its row and column position and also the array value for that cell. The button doesn't know anything about its position in the array, so it cannot do that directly. We could make our Magic class implement ActionListener, but then we would end up with a real mess as we try to determine which button was pressed and where it is in our array of buttons.

A better solution is to create a different listener object for each button. When the listener is created, it is told the row and the column of the button it is listening to. Each listener is attached to a different button:

   public void begin() {
     // Set to be GridLayout so show in array
     setLayout(new GridLayout(SIZE,SIZE,2,2));
	
     magicIntArray = new int[SIZE][SIZE];
        
     fillArray();            // fill in array with "magic" integers

     Button newButton;
	
     // put magic integers in buttons
     for (int rowNum = 0; rowNum < SIZE; rowNum++) {
       for (int colNum = 0; colNum < SIZE; colNum++) {
         // add button with label from magicIntArray
         newButton = new Button(""+magicIntArray[rowNum][colNum]);
         add(newButton);
         newButton.addActionListener(new MagicListener(rowNum, colNum, this));
       }
    }
                    
    checkSums();    // Check sums of rows, columns and diags to show "magic"
   }

When the button is clicked, we want it to print the value in the corresponding array cell. The magic square knows this information, and it is available from the getCellValue method of the Magic class. Hence, we pass "this" to the MagicListener constructor, and save it, along with the row and column values, in instance variable for use in the actionPerformed method.

/**
 * Class which listens to clicks on buttons in Magic square
 * Written 10/26/99 by Kim Bruce
 * Modified 04/15/02 by Jim Teresco
 */
 
import java.awt.*;
import java.awt.event.*;
   
public class MagicListener implements ActionListener {
    private int row, col;    // Row and column of button listened to
    private Magic square;    // The object containing the collection of buttons
    
    public MagicListener(int arow, int acol, Magic asquare) {
        row = arow;
        col = acol;
        square = asquare;
    }
    
    // when button is clicked print the value of the corresponding array elt
    public void actionPerformed(ActionEvent evt) {
        int cellValue = square.getCellValue (row, col);
        if (cellValue != -1) {
            System.out.println ("The cell at (" + row + ", " +
                                    col + ") = " + cellValue);
        } else {
            System.out.println ("Index (" + row + ", " +
                                    col + ") is  out of bounds!");
        }
    }
}

The definition of method getCellValue is relatively straightforward.

We have considered only two-dimensional arrays, but Java lets us define arrays of any dimension. Add a pair of square brackets for each dimension to get Java to construct "arrays of arrays of arrays..." Typically, an n-dimensional array is visited by a nesting of n for loops.

Handling key events

Next week's lab will involve controlling a snake using the arrow keys on the keyboard, so let's look at how you can get such input from the keyboard. Much like our familiar mouse events and the AWT events, a key press generates an event that can be handled by your program.

If an object wishes to be a listener for key events, it must implement the interface KeyListener. KeyListener has three methods:

    public void keyPressed(KeyEvent evt);
    public void keyTyped(KeyEvent evt);
    public void keyReleased(KeyEvent evt);

Any class implementing KeyListener must define all three methods, even if only one is needed in a program. Unused methods should be defined with empty bodies (i.e., { }).

The program below is a simple demo program that extends Applet, printing out for each key an integer key code, and the actual character generated. The key code is obtained by sending the getKeyCode() message to the event while the character generated is obtained by sending the getKeyChar() message to the event.

Demo: Key Event Demo

If you try out the program, you will notice that getKeyCode works as expected with keyPress and keyReleased methods, but not with keyTyped. With keyTyped events, it always just returns 0. Most useful key codes are given as constants in the KeyEvent class. For example VK_UP is the key code for the up arrow, while VK_DOWN, VK_LEFT, and VK_RIGHT represent the codes for the other arrow keys.

On the other hand, keys that don't generate characters on the screen will not generate interesting characters with the getKeyChar() method. Thus pressing arrow keys, shift, control, option, etc., generate no useful character output.

Suppose that you wish to have the user control a program using the arrow keys on the keyboard. You might write a keyPressed method as follows:

   public void keyPressed(KeyEvent evt) {
     if (evt.getKeyCode() == VK_UP) {
       doUpStuff();
     } else if (evt.getKeyCode() == VK_DOWN) {
       doDownStuff();
     } else if (evt.getKeyCode() == VK_LEFT) {
       doLeftStuff();
     } else if (evt.getKeyCode() == VK_RIGHT) {
       doRightStuff();
     }
   }
In the above, the methods doUpStuff(), etc., would be the actual code to be executed if the user presses the appropriate button.