Week 10

Sorting/Modifying, And Returning Arrays, and Introduction to Multi-Dimensional Arrays
Version 0


 


The Awful Truth About Arrays

 

Before going any further with arrays it is necessary to explain a somewhat complicated truth about the way Java programs store them in memory, because it affects in serious ways the way in which arrays behave when they are used as parameters to or results of methods.

Recall that variable names are just shorthand for numerical memory addresses. The positions denoted by these addresses are all in a part of the computer's memory called the stack because new variables are added and removed as methods are entered and exited, like plates on a stack. With variables that store scalar values (small values like ints and doubles) the values are put in memory at the specified location on the stack.

Arrays Variables Are Just References But with array variables (and objects, as will be discussed in a couple of weeks) the situation is a bit different. Because we do not know how big an array is at the time it is declared, rather than storing the whole array at the position specified by the variable name, that position instead just holds, as a value, the address where the array actually resides. This address value is known as a reference to the array.

Initially, this reference just holds the address value 0, which, in the context of references, is called the null reference. When you allocate the array using a call to new the system looks for a block large enough to hold the array in another part of the computer's memory called the heap. Unlike the space set up on the stack for a method's variables and parameters, space allocated on the heap is not erased when the method that allocated it exits.

When ever we refer to an element of the array, the system first dereferences the array reference to find the actual array on the heap, and only then goes to the position in the array specified by the index. In contrast, when we use the array name without the square brackets and an index, and are refering to the whole array, we are really just talking about the value of the reference.

The implications of this small difference are far-reaching. For example, suppose we have the declaration:

int[] a = {3,5,7}, b = {4,6,8,10}; 
If we make the assignment:
a[1] = b[2]; 
then it is an ordinary copying of the third element of the array b, which is 8, to the second cell of the array a, overwriting the value 5 (all of which happens on the heap, where the two arrays are actually stored).

  If, however, we make the assignment:
a = b; 
it does not copy elements from b to a. Rather, it copies the address stored in the position on the stack b stands for into the position on the stack that a stands for. This makes a a reference to the same array that b is a reference to. So, a becomes a four element array with the values 4, 6, 8, and 10. Moreover, it is really the same array as b. If we change the third element of a, we will also be changing the third element of b! When this happens we say that a and b have become aliases of one another.

Passing Arrays To Methods As we mentioned in the lecture on methods, actual parameters are passed by value in Java, meaning that the value you send to a method is copied into a new position allocated for the formal parameter in the stack frame for the method.

But since the value on the stack for an array variable is just a reference to the actual array, it is this refernce that is copied when an array is passed as an actual parameter to a method. This sets up the same situation as in the example above: the array variable in the called method is just an alias for the array in the calling method.

  That means that any changes you make to the elements of the array in the called method will affect the original array in the calling method, since they are really just the same array. This can be seen in the behavior of the following program:

Click Here To Run This Program On Its Own Page
// Program: ArrayParam
// Author:  Joshua S. Hodas
// Date:    October 31, 1996
// Purpose: To demonstrate that Arrays are
//          passed by reference
 
import HMC.HMCSupport;
 
class ArrayParam {
 
  public static void main(String args[]) {
 
    int[] a = {5,2,8,4,1};
 
    HMCSupport.out.println("The array before increment:");
    printArray(a);
 
    incrementArray(a);
 
    HMCSupport.out.println("The array after increment:");
    printArray(a);
  }
 
 
  public static void incrementArray(int[] arr) {
 
    for (int i = 0 ; i < arr.length ; i++) {
 
      arr[i]++;
    }
  }
 
 
  public static void printArray(int[] arr) {
 
    HMCSupport.out.println();
 
    for (int i = 0 ; i < arr.length ; i++) {
 
      HMCSupport.out.print(arr[i] + " ");
    }
    HMCSupport.out.println();
    HMCSupport.out.println();
  }
 
}

The program's output is:

The array before increment:

5 2 8 4 1

The array after increment:

6 3 9 5 2 

In general, this is just as well. If the whole array were actually copied when it was passed to a method, the copying process could well become very cumbersome if the array were large. This is particularly true since a large number of array methods would only be reading from the array anyway, so the copying would be a waste of time.

Arrays Are Not Cast As a side effect of this way of managing arrays, it is important to realize that the rules that cause individual variables to be automatically cast do not apply to arrays. That is, while you can pass an int to a method that is expecting a double, having it automatically cast upwards, you cannot pass an array of ints to a method that is expecting an array of doubles. That would require automatically manipulating all of the cells of the array individually. If that is what you want, you will need to make your own type-cast copy of the array cell-by-cell.


Returning Arrays From Methods

  It is possible to return an array (or, more correctly, a reference to an array) from a method as well. This array can be one that was passed in to the method (in which case whatever the result is assigned to becomes an alias of the original array), or it can be an array that was declared and allocated inside the method. Since the space allocated on the heap is not disturbed when the method exits, this is not a problem.

The following method, which will create a copy of an array of ints it is passed, is an example of returning an array declared and allocated within a method.

Notice that the return type of the method is int[] to indicate that the method returns an array of ints, as opposed to an individual int.

public static int[] copyArray(int[] original) {
 
  int[] copy = new int[original.length];
 
  for (int i = 0 ; i < original.length ; i++) {
   
    copy[i] = original[i];
  }
  return copy;
}
 
If, given this method, we wrote:
a = copy_array(b); 
then instead of just assigning the reference in b directly to a (and hence making a an alias for b as before), a would still become an array with the values 4, 6, 8, and 10, but it would not be the same array as b. Subsequent changes to one would not affect the other.


Sorting An Array

  As we saw in the notes about searching for data in an array, keeping the data sorted can be very beneficial as it can significantly speed up searching for values in the data set. There are many other reasons we might want to keep a data set sorted. For example, when printing reports or otherwise presenting information to the user, we often want the data to be in sorted order. Given an unsorted data set, how do we get it in order? The two techniques we will consider are called selection sort, and insertion sort. These are simple, intuitive algorithms that correspond roughly to techniques you might use if you were asked to sort a stack of papers (for instance a stack of exams that are to be sorted by the students' names.).
Selection Sort Suppose you have a stack of papers you need to sort. One way to do it is to go through the stack and look for the one with the alphabetically earliest student name. Take that paper out of the stack and put it face down on the table. Now go through the papers again and select the earliest name again (i.e. the earliest name from what's left). Put that paper face down on top of the first, and repeat the process until you have them all face down on the stack in order. This is called "selection sort" because of how the effort is concentrated on selecting the next paper to set down on the stack. Once we have that paper it is put into the sorted stack in one simple step.

The following code implements this idea for sorting an array of data. In place of the face down stack we will use the beginning of the array. When we identify the smallest element in the array, we will move it into the first position, which is where it belongs. In order to avoid losing the element that was originally in the first position, that element is copied to the position that previously held the smallest element. This process repeats, swapping the second smallest element in the array with the element in the second position, and so on.

We have already written (in an earlier lab exercise) a method which searches for the smallest element in an array. Here we use a slightly modified version of that. This method takes an array, and a position in the array, and looks for the location of the smallest element in the part of the array that starts at the given position. This way we will be able to block off the lower part of the array which is already sorted.

// Method:  selectionSort
// Author:  Joshua S. Hodas
// Date:    November 22, 1996
// Purpose: To sort an array of numbers
 
  public static int findSmallest(double arr[], int startFrom) {
    
    int posOfSmallestSoFar = startFrom;       // Assume, to start, that  
                                              // 1st element is smallest
    for (int i = startFrom+1 ; i < arr.length ; i++) {
 
      if (arr[i] < arr[posOfSmallestSoFar]) { // If this element smaller
                                              // than smallestSoFar
        posOfSmallestSoFar = i;               // save its position.
        }
    }
    return posOfSmallestSoFar;
  }
 
 
  public static void selectionSort(double arr[]) {
    
    for (int posToFill = 0 ; posToFill < arr.length ; posToFill++) {
      
      int posOfSmallest = findSmallest(arr,posToFill);
      double temp = arr[posToFill];         // These three lines 
      arr[posToFill] = arr[posOfSmallest];  // swap the element in  
      arr[posOfSmallest] = temp;            // position to be filled  
    }                                       // and smallest one in 
  }                                         // unsorted part of array.

Insertion Sort Another way you might think of sorting a stack of papers would be to lay the stack face down and take the first paper off the stack and lay it face up on the table. Now, take a second paper off the stack and if it belongs before the paper on the table just put it on top, but if it belongs after it put it underneath. Repeat this process, taking a paper off the face-down stack and putting it in its proper position in the face up stack until you have done all the papers. This is called "insertion sort" because the effort is concentrated in inserting each paper into its proper position in the growing stack. The next paper is selected simply by pulling it off the top of the stack.

In implementing this as a program, it is not practical to actually use two arrays to represent the two stacks, as this would unreasonably increase the memory usage of the program. Fortunately, we know that the total size of the two stacks together must always be the same as the size of the original data set, since we are only moving values (or papers) around; none are being created or destroyed.

Therefore, to implement this as a method on arrays, we can use the front of the array as the face-up stack that is being constructed and the rest as the face-down stack. We just need to use an extra variable to keep track of the position that marks the end of the sorted (face-up) portion and the begining of the unsorted (face-down) portion. Think of it as keeping all the papers in one stack, but using a pink sheet of paper to separate the sorted from the unsorted papers.

Most of the work is done by the insert method which takes two parameters: an array, and the number of elements in the part of the array that is already sorted. It treats the first in the unsorted part as the element to be inserted into the sorted part. Once we copy this value off to a temporary storage variable, its position in the array becomes part of the sorted part, providing room for the insertion. The method first locates the position that the new element should go into in the sorted part. Then it moves all the higher elements one position upwards to make room for the new element. At the end of the method, the sorted portion is one element longer.

// Method:  insertionSort
// Author:  Joshua S. Hodas
// Date:    November 22, 1996 (modified 10/20/98) (bugs fixed 11/6/98)
// Purpose: To sort an array of numbers
 
  public static void insert(double arr[], int sortedSoFar) {
                  
    double insertVal = arr[sortedSoFar];
    int i = 0; 
    boolean found = false;
               
    // Loop until find first element greater than insertVal
    // or until reach past ended of sorted section.
    // That element is in the position insertVal should go in.
 
    while (i < sortedSoFar && (!found)) {
 
      if (arr[i] > insertVal) {
 
        found = true;
      }
      else {
 
        i++;
      }
    }  
 
    // Move other elements up out of way. Easier to do
    // coming down from above. If the last loop went past
    // the end of the sorted section, this loop is never entered.
 
    for (int j = sortedSoFar ; j > i ; j--) {
 
      arr[j] = arr[j-1];
    }
 
    // Insert the new value in its place
 
    arr[i] = insertVal;
  }
 

public static void insertionSort(double arr[]) { for (int sortedSoFar = 1 ;sortedSoFar < arr.length ;sortedSoFar++) { insert(arr,sortedSoFar); } }

The Cost Of Sorting

An important part of Computer Science involves studying the complexity (or cost, in time and memory usage) of different algorithms for solving a task. We have already done a bit of this sort of analysis in our discussion of the difference between linear search and binary search. In the case of sorting algorithms, one measures the complexity in terms of the amount of extra space needed above and beyond the original storage for the data set, and in terms of the number of times that two elements of the data set are compared, or have their positions in the data set swapped.

Suppose the array to be sorted has n elements in it. In the case of selection sort, determining the value that should go into the first position will require examining all n cells. That is, it will require that n comparisons be done. Once the element is found, we will need to exchange it with element in the first position. For the second position, we need to look at all but one of the elements of the array, since one element is already in final position. That is, we will do n-1 comparisons and one exchange. And so on.

In other words, to sort the array we will require n exchanges and (n + n-1 + n-2 + ... + 1) comparisons. This summation works out to ((n * (n+1))/2) comparisons and n exchanges. If we assume that comparisons and exchanges take roughly the same amount of time, then the total number of operations is 1/2(n2+3n). Since the size of the square term will quickly dwarf the linear term, that part is discarded when we estimate the execution cost of the algorithm. Hence, the time to produce the sorted result can be seen to grow roughly as n2. Thus if we double the size of the array sorting it will take four times as long. If we use an array ten times as large, sorting it will take one hundred times as long.

With insertion sort, to fill each position we must compare the new element to all the values in the lower portion of the array until the location is found, and then we must move the remaining operations out of the way. Thus if we are trying to insert the twentieth element, the total number of comparisons and exchanges will be twenty, though how many there are of each will depend on where the new element goes in the sorted part. So, the total number of operations for the whole sort process again follows the same summation as above, and the time to sort again grows as the square of the length of the array.

Big-Oh Notation As you may have noticed, computer scientists love precision and notation. Since they spend alot of time talking about execution costs of algorithms, they have come up with a whole raft of notations for those discussions. The most important (and basic) one is big-oh notation which is used for writing expressions indicating how the cost of executing an algorithm grows in comparison to growth in the size of the data set. For the two sorting algorithms we would write that they are O(n^2) algorithms. This is pronounced "order n squared", or sometimes even "big-oh of n squared". It simply means that the execution cost grows at least as fast as the square of the size of the data set.

Other Sorts of Sorts There are many other simple sorting algorithms, such as bubble sort, shaker sort, and shell sort that are based on simple intuitive sorting techniques. It turns out that they all have roughly the same computational cost. However, there is another whole class of sorting techniques that have much better growth properties. Like binarySearch they rely on successively dividing the array in half. In the case of sorting though, we do not ignore one half, we just treat the halves independently, and then combine the results afterwards. These techniques are called divide and conquer techniques and they all have roughly the same growth characteristics. They all grow as (n * (log n)) which grows much more slowly than the square of n. Unfortunately, the actual details of these algorithms are too complex to go into here, but it is important to know that they do exist (and are all that ever get used in real-life data manipulation programs these days). These techniques are typically covered in a second-semester programming class.


Two-Dimensional Arrays

  We motivated the introduction of arrays by discussing a situation where you needed to keep track of and manipulate the exam grades for a course. While individual variables could be used to hold each grade, this quickly became cumbersome. It was also inflexible, requiring that programs be significantly modified if the number of students changed (since you would need new variables for each new student).

Well, what happens if we want to track the grades on several exams? Certainly we could declare an array variable to hold the grades for each exam, but we soon find ourselves in the same situation at a different level. Fortunately, there is a natural solution.

While it may not have been apparent in the coverage so far, the elements of an array do not have to be simple values like ints and doubles. They can be any Java type. In particular, there is nothing keeping you from making an array each element of which is itself an array. All you need to do is write the declaration properly.

Declaring A 2-D Array Just as the declaration:
int[] examOneGrades; 
declares the variable examOneGrades to be an array whose cells hold ints (because, as we discussed in the introduction to one-dimensional arrays, the brackets, shown in boldface, come after the cell type int), the declaration:
int[][] examGrades; 
declares examGrades to be an array whose cells hold arrays of ints (since the brackets, shown in boldface, come after the cell type int[], which means that the cell type is array of int).

Allocat- ing A 2-D Array Once we have declared the array, we must, as with any array, allocate space for it. This can be done in a few ways. Most commonly, each of the element arrays will have the same number of elements. For instance we might want to allocate space for three exams, with ten scores each. In that case we would write:
examGrades = new int[3][10]; 

Initializ- ing A 2-D Array As with one-dimensional arrays, if you want to set the initial values in the cells of a two-dimensional array, you can replace the allocation step with an initialization. We could, for example, declare, allocate, and initialize the grade array with the statement:
int[][] examGrades = {{90,75,88,69,72,55,75,92,84,77},
                      {88,69,72,90,75,88,92,84,77,55},
                      {72,90,75,88,69,92,84,77,69,72}}; 

Using A 2-D Array As with one dimensional arrays, there are several levels at which you can refer to array variables. In the context of this example:
  • The name examGrades refers to an array of three arrays of ten int values each. We could use it in any context where an array of arrays of ints would be appropriate. In particular, we could pass this variable to any method that was expecting an array of arrays of ints. For example, assuming the method were appropriately defined, we could make the call:
    double courseAverage = overallAverage(examGrades); 
    

  • The name examGrades[1], for example, refers to a ten element array of int values: the scores for the second exam. We could use it in any context where an array of ints would be appropriate. In particular, we could pass this variable to any method expecting an array of ints. For example, we could make the call:
    double secondExamAverage = examAverage(examGrades[1]); 
    

  • The name examGrades[1][4], for example, refers to an int value: the fifth student's score on the second exam. We could use it in any context where an int would be appropriate. In particular, we could pass this variable to any method expecting an int as a parameter.

So, to reiterate, if we had the following method:

public static double averageArray(int[] arr) {
 
  double sum = 0.0;
 
  for (int i = 0 ; i < arr.length ; i++) {
 
    sum += arr[i];     
  }  
  return sum/arr.length;
} 
it would be reasonable to pass examGrades[2], the scores for the third exam, to it. To print the individual averages for each of the exams, we could use the method as in the following block of code:
for (int i = 0 ; i < examGrades.length ; i++) {
 
  HMCSupport.out.println("The average score on exam " + (i+1)
                         + " was " + averageArray(examGrades[i]));
 
} 

Notice that while it is easy to refer to all the scores on the second exam (i.e. examGrades[1]), there is no easy way to refer to all the scores for the fifth student. We can't write examGrades[][4]. It just isn't meaningful. (Remember the rule from the lecture on one-dimensional arrays: you NEVER use an empty pair of square braces other than in the declaration of an array variable.)

If we wanted to pass those values to a method expecting an array of ints we'd first have to copy them to a new one dimensional array. For instance, since examGrades.length is the number of exams, we can write:

int[] fivesScores = new int[examGrades.length];
for (int i = 0 ; i < examGrades.length ; i++) {
 
  fivesScores[i] = examGrades[i][4];
}
 
HMCSupport.out.println("The average of the fifth student's " +
                       "grades was" + averageArray(fivesScores)); 


Irregular 2-D Arrays

 
  So far we have shown only rectangular arrays with each element array the same length. It is however possible to allocate the element arrays individually. Suppose we want to allocate space for three exams, but the number of students starts at ten, rises to thirteen (during the add period) and then drops back to eleven (on drop day).

To allocate the overall array we would write:

examGrades = new int[3][]; 
This specifies that examOneGrades will hold three arrays of ints. The empty bracket is in a somewhat odd position. More logically, the 3 should be in the right pair of brackets. But Java's designers chose this notation to stay consistent with the way it is used (intuitively) when we are allocating the whole array at once, as above.

Of course, we have not yet made space for the exam scores themselves. The space for the scores for each of the exams is allocated individually, as in:

examGrades[0] = new int[10];  // The first exam has 10 scores
examGrades[1] = new int[13];  // The second exam has 13 scores
examGrades[2] = new int[11];  // The third exam has 11 scores 

  Later, if we want to know how many scores there are for the second exam, we would refer to examGrades[1].length. This is in contrast to finding out how many exams there are, which we do with eamGrades.length.


Rows And Columns

  To this point we have thought of something of type int[][] as an array of arrays of ints. When, on the other hand, we are thinking of the array as a two-dimensional grid, rather than as an array of arrays, there is a convention that says that the number in the first pair of brackets selects the row and the number in the second pair of brackets selects the column. (Note that this is contrary to the convention for graph coordinates where the x coordinate comes first.) This convention is known as representing the array in row-major order because, as above, it is easy to refer to a row in the array as a unit, but difficult to refer to a column as a unit.


Multi-Dimensional Arrays

  It probably won't suprise you to discover that this whole idea can be lifted to another level, and another, and another, and so on. Suppose we need to keep track of the exam scores for several classes during a term. We can create an array each of whose cells is a two-dimensional array of ints representing the grade sheet for one class, using the declaration:
int[][][] grades; 
You can visualize this structure as a cube full of grades. We would refer to a particular score with:
grades[courseNum][examNum][studentNum] 
.

Of course we can move on to dimensions more difficult to visualize without creating any problem for the computer. We can track grades over the years with the declaration:

int[][][][] grades; 
In this case, we would select a particular score with:
grades[yearNum][courseNum][examNum][studentNum] 
.

We can continue this construction as far as we'd like.

Last modified August 28 for Fall 99 cs5 by fleck@cs.hmc.edu


This page copyright ©1998 by Joshua S. Hodas. It was built with Frontier on a Macintosh . Last rebuilt on Fri, Nov 6, 1998 at 5:57:56 PM.
http://www.cs.hmc.edu/~hodas/courses/cs5/week_11/lecture/lecture.html