| |||
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
| |||
| 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 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: If we make the assignment: 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:
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:
The program's output is:
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
Notice that the return type of the method is If, given this method, we wrote: 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.
| ||
| 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
| ||
| 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
| |||
| Declaring A 2-D Array |
Just as the declaration:
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:
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:
| ||
| 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:
| ||
| 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:
So, to reiterate, if we had the following method: 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:
Notice that while it is easy to refer to all the scores on the second exam
(i.e.
If we wanted to
pass those values to a method expecting an array of
| ||
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: 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:
| |||
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:
You can visualize this structure as a cube full of grades. We would refer to a particular score with: . 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: In this case, we would select a particular score with: . 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 | |