1.4 Arrays

In this section, we introduce you to the idea of a data structure and to your first data structure, the array. The primary purpose of an array is to facilitate storing and manipulating large quantities of data. Arrays play an essential role in many data processing tasks. They also correspond to vectors and matrices, which are widely used in science and in scientific programming. We will consider basic properties of arrays in Java, with many examples illustrating why they are useful.

A data structure is a way to organize data in a computer (usually to save time or space). Data structures play an essential role in computer programming—indeed, CHAPTER 4 of this book is devoted to the study of classic data structures of all sorts.

A one-dimensional array (or array) is a data structure that stores a sequence of values, all of the same type. We refer to the components of an array as its elements. We use indexing to refer to the array elements: If we have n elements in an array, we think of the elements as being numbered from 0 to n-1 so that we can unambiguously specify an element with an integer index in this range.

An illustration shows the arrangement of data in a one-dimensional array.

A two-dimensional array is an array of one-dimensional arrays. Whereas the elements of a one-dimensional array are indexed by a single integer, the elements of a two-dimensional array are indexed by a pair of integers: the first index specifies the row, and the second index specifies the column.

Often, when we have a large amount of data to process, we first put all of the data into one or more arrays. Then we use indexing to refer to individual elements and to process the data. We might have exam scores, stock prices, nucleotides in a DNA strand, or characters in a book. Each of these examples involves a large number of values that are all of the same type. We consider such applications when we discuss input/output in SECTION 1.5 and in the case study that is the subject of SECTION 1.6. In this section, we expose the basic properties of arrays by considering examples where our programs first populate arrays with values computed from experimental studies and then process them.

Arrays in Java

Making an array in a Java program involves three distinct steps:

• Declare the array.

• Create the array.

• Initialize the array elements.

To declare an array, you need to specify a name and the type of data it will contain. To create it, you need to specify its length (the number of elements). To initialize it, you need to assign a value to each of its elements. For example, the following code makes an array of n elements, each of type double and initialized to 0.0:

double[] a;                    // declare the array
a = new double[n];             // create the array
for (int i = 0; i < n; i++)    // initialize the array
   a[i] = 0.0;

The first statement is the array declaration. It is just like a declaration of a variable of the corresponding primitive type except for the square brackets following the type name, which specify that we are declaring an array. The second statement creates the array; it uses the keyword new to allocate memory to store the specified number of elements. This action is unnecessary for variables of a primitive type, but it is needed for all other types of data in Java (see SECTION 3.1). The for loop assigns the value 0.0 to each of the n array elements. We refer to an array element by putting its index in square brackets after the array name: the code a[i] refers to element i of array a[]. (In the text, we use the notation a[] to indicate that variable a is an array, but we do not use a[] in Java code.)

The obvious advantage of using arrays is to define many variables without explicitly naming them. For example, if you wanted to process eight variables of type double, you could declare them with

double a0, a1, a2, a3, a4, a5, a6, a7;

and then refer to them as a0, a1, a2, and so forth. Naming dozens of individual variables in this way is cumbersome and naming millions is untenable. Instead, with arrays, you can declare n variables with the statement double[] a = new double[n] and refer to them as a[0], a[1], a[2], and so forth. Now, it is easy to define dozens or millions of variables. Moreover, since you can use a variable (or other expression computed at run time) as an array index, you can process arbitrarily many elements in a single loop, as we do above. You should think of each array element as an individual variable, which you can use in an expression or as the left-hand side of an assignment statement.

As our first example, we use arrays to represent vectors. We consider vectors in detail in SECTION 3.3; for the moment, think of a vector as a sequence of real numbers. The dot product of two vectors (of the same length) is the sum of the products of their corresponding elements. The dot product of two vectors that are represented as one-dimensional arrays x[] and y[], each of length 3, is the expression x[0]*y[0] + x[1]*y[1] + x[2]*y[2]. More generally, if each array is of length n, then the following code computes their dot product:

double sum = 0.0;
for (int i = 0; i < n; i++)
   sum += x[i]*y[i];

The simplicity of coding such computations makes the use of arrays the natural choice for all kinds of applications.

i

x[i]

y[i]

x[i]*y[i]

sum

 

 

 

 

0.00

0

0.30

0.50

0.15

0.15

1

0.60

0.10

0.06

0.21

2

0.10

0.40

0.04

0.25

 

 

 

 

0.25

Trace of dot product computation

The table on the facing page has many examples of array-processing code, and we will consider even more examples later in the book, because arrays play a central role in processing data in many applications. Before considering more sophisticated examples, we describe a number of important characteristics of programming with arrays.

Zero-based indexing

The first element of an array a[] is a[0], the second element is a[1], and so forth. It might seem more natural to you to refer to the first element as a[1], the second element as a[2], and so forth, but starting the indexing with 0 has some advantages and has emerged as the convention used in most modern programming languages. Misunderstanding this convention often leads to off-by one-errors that are notoriously difficult to avoid and debug, so be careful!

Array length

Once you create an array in Java, its length is fixed. One reason that you need to explicitly create arrays at run time is that the Java compiler cannot always know how much space to reserve for the array at compile time (because its length may not be known until run time). You do not need to explicitly allocate memory for variables of type int or double because their size is fixed, and known at compile time. You can use the code a.length to refer to the length of an array a[]. Note that the last element of an array a[] is always a[a.length-1]. For convenience, we often keep the array length in an integer variable n.

create an array with random values

double[] a = new double[n];
for (int i = 0; i < n; i++)
   a[i] = Math.random();

print the array values, one per line

for (int i = 0; i < n; i++)
   System.out.println(a[i]);

find the maximum of the array values

double max = Double.NEGATIVE_INFINITY;
for (int i = 0; i < n; i++)
   if (a[i] > max) max = a[i];

compute the average of the array values

double sum = 0.0;
for (int i = 0; i < n; i++)
   sum += a[i];
double average = sum / n;

reverse the values within an array

for (int i = 0; i < n/2; i++)
{
   double temp = a[i];
   a[i] = a[n-1-i];
   a[n-i-1] = temp;
}

copy a sequence of values to another array

double[] b = new double[n];
for (int i = 0; i < n; i++)
   b[i] = a[i];

Typical array-processing code (for an array a[] of n double values)

Default array initialization

For economy in code, we often take advantage of Java’s default array initialization to declare, create, and initialize an array in a single statement. For example, the following statement is equivalent to the code at the top of page 91:

double[] a = new double[n];

The code to the left of the equals sign constitutes the declaration; the code to the right constitutes the creation. The for loop is unnecessary in this case because Java automatically initializes array elements of any primitive type to zero (for numeric types) or false (for the type boolean). Java automatically initializes array elements of type String (and other nonprimitive types) to null, a special value that you will learn about in CHAPTER 3.

Memory representation

Arrays are fundamental data structures in that they have a direct correspondence with memory systems on virtually all computers. The elements of an array are stored consecutively in memory, so that it is easy to quickly access any array value. Indeed, we can view memory itself as a giant array. On modern computers, memory is implemented in hardware as a sequence of memory locations, each of which can be quickly accessed with an appropriate index. When referring to computer memory, we normally refer to a location’s index as its address. It is convenient to think of the name of the array—say, a—as storing the memory address of the first element of the array a[0]. For the purposes of illustration, suppose that the computer’s memory is organized as 1,000 values, with addresses from 000 to 999. (This simplified model bypasses the fact that array elements can occupy differing amounts of memory depending on their type, but you can ignore such details for the moment.) Now, suppose that an array of eight elements is stored in memory locations 523 through 530. In such a situation, Java would store the memory address (index) of the first array element somewhere else in memory, along with the array length. We refer to the address as a pointer and think of it as pointing to the referenced memory location. When we specify a[i], the compiler generates code that accesses the desired value by adding the index i to the memory address of the array a[]. For example, the Java code a[4] would generate machine code that finds the value at memory location 523 + 4 = 527. Accessing element i of an array is an efficient operation because it simply requires adding two integers and then referencing memory—just two elementary operations.

An illustration shows the Memory representation.
Memory allocation

When you use the keyword new to create an array, Java reserves sufficient space in memory to store the specified number of elements. This process is called memory allocation. The same process is required for all variables that you use in a program (but you do not use the keyword new with variables of primitive types because Java knows how much memory to allocate). We call attention to it now because it is your responsibility to create an array before accessing any of its elements. If you fail to adhere to this rule, you will get an uninitialized variable error at compile time.

Bounds checking

As already indicated, you must be careful when programming with arrays. It is your responsibility to use valid indices when referring to an array element. If you have created an array of length n and use an index whose value is less than 0 or greater than n-1, your program will terminate with an ArrayIndexOutOfBoundsException at run time. (In many programming languages, such buffer overflow conditions are not checked by the system. Such unchecked errors can and do lead to debugging nightmares, but it is also not uncommon for such an error to go unnoticed and remain in a finished program. You might be surprised to know that such a mistake can be exploited by a hacker to take control of a system, even your personal computer, to spread viruses, steal personal information, or wreak other malicious havoc.) The error messages provided by Java may seem annoying to you at first, but they are small price to pay to have a more secure program.

Setting array values at compile time

When we have a small number of values that we want to keep in array, we can declare, create, and initialize the array by listing the values between curly braces, separated by commas. For example, we might use the following code in a program that processes playing cards:

String[] SUITS = { "Clubs", "Diamonds", "Hearts", "Spades" };

String[] RANKS =
{
   "2", "3", "4", "5", "6", "7", "8", "9", "10",
   "Jack", "Queen", "King", "Ace"
};

Now, we can use the two arrays to print a random card name, such as Queen of Clubs, as follows:

int i = (int) (Math.random() * RANKS.length);
int j = (int) (Math.random() * SUITS.length);
System.out.println(RANKS[i] + " of " + SUITS[j]);

This code uses the idiom introduced in SECTION 1.2 to generate random indices and then uses the indices to pick strings out of the two arrays. Whenever the values of all array elements are known (and the length of the array is not too large), it makes sense to use this method of initializing the array—just put all the values in curly braces on the right-hand side of the equals sign in the array declaration. Doing so implies array creation, so the new keyword is not needed.

Setting array values at run time

A more typical situation is when we wish to compute the values to be stored in an array. In this case, we can use an array name with indices in the same way we use a variable names on the left-hand side of an assignment statement. For example, we might use the following code to initialize an array of length 52 that represents a deck of playing cards, using the two arrays just defined:

String[] deck = new String[RANKS.length * SUITS.length];
for (int i = 0; i < RANKS.length; i++)
   for (int j = 0; j < SUITS.length; j++)
      deck[SUITS.length*i + j] = RANKS[i] + " of " + SUITS[j];

After this code has been executed, if you were to print the contents of deck[] in order from deck[0] through deck[51], you would get

2 of Clubs
2 of Diamonds
2 of Hearts
2 of Spades
3 of Clubs
3 of Diamonds
...
Ace of Hearts
Ace of Spades
Exchanging two values in an array

Frequently, we wish to exchange the values of two elements in an array. Continuing our example with playing cards, the following code exchanges the cards at indices i and j using the same idiom that we traced as our first example of the use of assignment statements in SECTION 1.2:

String temp = deck[i];
deck[i] = deck[j];
deck[j] = temp;

For example, if we were to use this code with i equal to 1 and j equal to 4 in the deck[] array of the previous example, it would leave 3 of Clubs in deck[1] and 2 of Diamonds in deck[4]. You can also verify that the code leaves the array unchanged when i and j are equal. So, when we use this code, we are assured that we are perhaps changing the order of the values in the array but not the set of values in the array.

Shuffling an array

The following code shuffles the values in our deck of cards:

int n = deck.length;
for (int i = 0; i < n; i++)
{
   int r = i + (int) (Math.random() * (n-i));
   String temp = deck[i];
   deck[i] = deck[r];
   deck[r] = temp;
}

Proceeding from left to right, we pick a random card from deck[i] through deck[n-1] (each card equally likely) and exchange it with deck[i]. This code is more sophisticated than it might seem: First, we ensure that the cards in the deck after the shuffle are the same as the cards in the deck before the shuffle by using the exchange idiom. Second, we ensure that the shuffle is random by choosing uniformly from the cards not yet chosen.

Sampling without replacement

In many situations, we want to draw a random sample from a set such that each member of the set appears at most once in the sample. Drawing numbered ping-pong balls from a basket for a lottery is an example of this kind of sample, as is dealing a hand from a deck of cards. Sample (PROGRAM 1.4.1) illustrates how to sample, using the basic operation underlying shuffling. It takes two command-line arguments m and n and creates a permutation of length n (a rearrangement of the integers from 0 to n-1) whose first m elements comprise a random sample. The accompanying trace of the contents of the perm[] array at the end of each iteration of the main loop (for a run where the values of m and n are 6 and 16, respectively) illustrates the process.

i

r

perm[]

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

 

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

0

9

9

1

2

3

4

5

6

7

8

0

10

11

12

13

14

15

1

5

9

5

2

3

4

1

6

7

8

0

10

11

12

13

14

15

2

13

9

5

13

3

4

1

6

7

8

0

10

11

12

2

14

15

3

5

9

5

13

1

4

3

6

7

8

0

10

11

12

2

14

15

4

11

9

5

13

1

11

3

6

7

8

0

10

4

12

2

14

15

5

8

9

5

13

1

11

8

6

7

3

0

10

4

12

2

14

15

 

 

9

5

13

1

11

8

6

7

3

0

10

4

12

2

14

15

Trace of java Sample 6 16


Program 1.4.1 Sampling without replacement

public class Sample
{
   public static void main(String[] args)
   {  // Print a random sample of m integers
      // from 0 ... n-1 (no duplicates).
      int m = Integer.parseInt(args[0]);
      int n = Integer.parseInt(args[1]);
      int[] perm = new int[n];

      // Initialize perm[].
      for (int j = 0; j < n; j++)
          perm[j] = j;

      // Take sample.
      for (int i = 0; i < m; i++)
      {  // Exchange perm[i] with a random element to its right.
         int r = i + (int) (Math.random() * (n-i));
         int t = perm[r];
         perm[r] = perm[i];
         perm[i] = t;
      }

      // Print sample.
      for (int i = 0; i < m; i++)
         System.out.print(perm[i] + " ");
      System.out.println();
   }
}

  m    | sample size
  n    | range
perm[] | permutation of 0 to n-1

This program takes two command-line arguments m and n and produces a sample of m of the integers from 0 to n-1. This process is useful not just in state and local lotteries, but in scientific applications of all sorts. If the first argument is equal to the second, the result is a random permutation of the integers from 0 to n-1. If the first argument is greater than the second, the program will terminate with an ArrayOutOfBoundsException.


% java Sample 6 16
9 5 13 1 11 8
% java Sample 10 1000
656 488 298 534 811 97 813 156 424 109
% java Sample 20 20
6 12 9 8 13 19 0 2 4 5 18 1 14 16 17 3 7 11 10 15

If the values of r are chosen such that each value in the given range is equally likely, then the elements perm[0] through perm[m-1] are a uniformly random sample at the end of the process (even though some values might move multiple times) because each element in the sample is assigned a value uniformly at random from those values not yet sampled. One important reason to explicitly compute the permutation is that we can use it to print a random sample of any array by using the elements of the permutation as indices into the array. Doing so is often an attractive alternative to actually rearranging the array because it may need to be in order for some other reason (for instance, a company might wish to draw a random sample from a list of customers that is kept in alphabetical order).

To see how this trick works, suppose that we wish to draw a random poker hand from our deck[] array, constructed as just described. We use the code in Sample with n = 52 and m = 5 and replace perm[i] with deck[perm[i]] in the System.out.print() statement (and change it to println()), resulting in output such as the following:

3 of Clubs
Jack of Hearts
6 of Spades
Ace of Clubs
10 of Diamonds

Sampling like this is widely used as the basis for statistical studies in polling, scientific research, and many other applications, whenever we want to draw conclusions about a large population by analyzing a small random sample.

Precomputed values

One simple application of arrays is to save values that you have computed for later use. As an example, suppose that you are writing a program that performs calculations using small values of the harmonic numbers (see PROGRAM 1.3.5). An efficient approach is to save the values in an array, as follows:

double[] harmonic = new double[n];
for (int i = 1; i < n; i++)
   harmonic[i] = harmonic[i-1] + 1.0/i;

Then you can just use the code harmonic[i] to refer to the ith harmonic number. Precomputing values in this way is an example of a space–time tradeoff: by investing in space (to save the values), we save time (since we do not need to recompute them). This method is not effective if we need values for huge n, but it is very effective if we need values for small n many different times.

Simplifying repetitive code

As an example of another simple application of arrays, consider the following code fragment, which prints the name of a month given its number (1 for January, 2 for February, and so forth):

if      (m ==  1) System.out.println("Jan");
else if (m ==  2) System.out.println("Feb");
else if (m ==  3) System.out.println("Mar");
else if (m ==  4) System.out.println("Apr");
else if (m ==  5) System.out.println("May");
else if (m ==  6) System.out.println("Jun");
else if (m ==  7) System.out.println("Jul");
else if (m ==  8) System.out.println("Aug");
else if (m ==  9) System.out.println("Sep");
else if (m == 10) System.out.println("Oct");
else if (m == 11) System.out.println("Nov");
else if (m == 12) System.out.println("Dec");

We could also use a switch statement, but a much more compact alternative is to use an array of strings, consisting of the names of each month:

String[] MONTHS =
{
   "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
       "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
System.out.println(MONTHS[m]);

This technique would be especially useful if you needed to access the name of a month by its number in several different places in your program. Note that we intentionally waste one slot in the array (element 0) to make MONTHS[1] correspond to January, as required.

With these basic definitions and examples out of the way, we can now consider two applications that both address interesting classical problems and illustrate the fundamental importance of arrays in efficient computation. In both cases, the idea of using an expression to index into an array plays a central role and enables a computation that would not otherwise be feasible.

Coupon collector

Suppose that you have a deck of cards and you turn up cards uniformly at random (with replacement) one by one. How many cards do you need to turn up before you have seen one of each suit? How many cards do you need to turn up before seeing one of each value? These are examples of the famous coupon collector problem. In general, suppose that a trading card company issues trading cards with n different possible cards: how many do you have to collect before you have all n possibilities, assuming that each possibility is equally likely for each card that you collect?

An illustration shows a deck of cards arranged one by one.

Coupon collecting is no toy problem. For example, scientists often want to know whether a sequence that arises in nature has the same characteristics as a random sequence. If so, that fact might be of interest; if not, further investigation may be warranted to look for patterns that might be of importance. For example, such tests are used by scientists to decide which parts of genomes are worth studying. One effective test for whether a sequence is truly random is the coupon collector test: compare the number of elements that need to be examined before all values are found against the corresponding number for a uniformly random sequence. CouponCollector (PROGRAM 1.4.2) is an example program that simulates this process and illustrates the utility of arrays. It takes a command-line argument n and generates a sequence of random integers between 0 and n-1 using the code (int) (Math.random() * n)—see PROGRAM 1.2.5. Each integer represents a card: for each card, we want to know if we have seen that card before. To maintain that knowledge, we use an array isCollected[], which uses the card as an index; isCollected[i] is true if we have seen a card i and false if we have not. When we get a new card that is represented by the integer r, we check whether we have seen it before by accessing isCollected[r]. The computation consists of keeping count of the number of distinct cards seen and the number of cards generated, and printing the latter when the former reaches n.

r

isCollected[]

distinct

count

0 1 2 3 4 5

 

F F F F F F

0

0

2

F F T F F F

1

1

0

T F T F F F

2

2

4

T F T F T F

3

3

0

T F T F T F

3

4

1

T T T F T F

4

5

2

T T T F T F

4

6

5

T T T F T T

5

7

0

T T T F T T

5

8

1

T T T F T T

5

9

3

T T T T T T

6

10

Trace for a typical run of java CouponCollector 6

As usual, the best way to understand a program is to consider a trace of the values of its variables for a typical run. It is easy to add code to CouponCollector that produces a trace that gives the values of the variables at the end of the while loop. In the accompanying figure, we use F for the value false and T for the value true to make the trace easier to follow. Tracing programs that use large arrays can be a challenge: when you have an array of length n in your program, it represents n variables, so you have to list them all. Tracing programs that use Math.random() also can be a challenge because you get a different trace every time you run the program. Accordingly, we check relationships among variables carefully. Here, note that distinct always is equal to the number of true values in isCollected[].


Program 1.4.2 Coupon collector simulation

public class CouponCollector
{
   public static void main(String[] args)
   {
      // Generate random values in [0..n) until finding each one.
      int n = Integer.parseInt(args[0]);
      boolean[] isCollected = new boolean[n];
      int count = 0;
      int distinct = 0;

      while (distinct < n)
      {
         // Generate another coupon.
         int r = (int) (Math.random() * n);
         count++;
         if (!isCollected[r])
         {
            distinct++;
            isCollected[r] = true;
         }
      }  // n distinct coupons found.
      System.out.println(count);
   }
}

       n       | # coupon values (0 to n-1)
isCollected[i] | has coupon i been collected?
     count     | # coupons
   distinct    | # distinct coupons
       r       | random coupon

This program takes an integer command-line argument n and simulates coupon collection by generating random numbers between 0 and n-1 until getting every possible value.


% java CouponCollector 1000
6583
% java CouponCollector 1000
6477
% java CouponCollector 1000000
12782673

Without arrays, we could not contemplate simulating the coupon collector process for huge n; with arrays, it is easy to do so. We will see many examples of such processes throughout the book.

Sieve of Eratosthenes

Prime numbers play an important role in mathematics and computation, including cryptography. A prime number is an integer greater than 1 whose only positive divisors are 1 and itself. The prime counting function π(n) is the number of primes less than or equal to n. For example, π(25) = 9 since the first nine primes are 2, 3, 5, 7, 11, 13, 17, 19, and 23. This function plays a central role in number theory.

One approach to counting primes is to use a program like Factors (PROGRAM 1.3.9). Specifically, we could modify the code in Factors to set a boolean variable to true if a given number is prime and false otherwise (instead of printing out factors), then enclose that code in a loop that increments a counter for each prime number. This approach is effective for small n, but becomes too slow as n grows.

PrimeSieve (PROGRAM 1.4.3) takes a command-line integer n and computes the prime count using a technique known as the Sieve of Eratosthenes. The program uses a boolean array isPrime[] to record which integers are prime. The goal is to set isPrime[i] to true if the integer i is prime, and to false otherwise. The sieve works as follows: Initially, set all array elements to true, indicating that no factors of any integer have yet been found. Then, repeat the following steps as long as i <= n/i:

• Find the next smallest integer i for which no factors have been found.

• Leave isPrime[i] as true since i has no smaller factors.

• Set the isPrime[] elements for all multiples of i to false.

When the nested for loop ends, isPrime[i] is true if and only if integer i is prime. With one more pass through the array, we can count the number of primes less than or equal to n.


Program 1.4.3 Sieve of Eratosthenes

public class PrimeSieve
{
   public static void main(String[] args)
   {  // Print the number of primes <= n.
      int n = Integer.parseInt(args[0]);
      boolean[] isPrime = new boolean[n+1];
      for (int i = 2; i <= n; i++)
         isPrime[i] = true;

      for (int i = 2; i <= n/i; i++)
      {  if (isPrime[i])
         {  // Mark multiples of i as nonprime.
            for (int j = i; j <= n/i; j++)
               isPrime[i * j] = false;
         }
      }

      // Count the primes.
      int primes = 0;
      for (int i = 2; i <= n; i++)
         if (isPrime[i]) primes++;
      System.out.println(primes);
   }
}

    n      | argument
isPrime[i] | is i prime?
  primes   | prime counter

This program takes an integer command-line argument n and computes the number of primes less than or equal to n. To do so, it computes a boolean array with isPrime[i] set to true if i is prime, and to false otherwise. First, it sets to true all array elements to indicate that no numbers are initially known to be nonprime. Then it sets to false array elements corresponding to indices that are known to be nonprime (multiples of known primes). If a[i] is still true after all multiples of smaller primes have been set to false, then we know i to be prime. The termination test in the second for loop is i <= n/i instead of the naive i <= n because any number with no factor less than n/i has no factor greater than n/i, so we do not have to look for such factors. This improvement makes it possible to run the program for large n.


% java PrimeSieve 25
9
% java PrimeSieve 100
25
% java PrimeSieve 1000000000
50847534

As usual, it is easy to add code to print a trace. For programs such as PrimeSieve, you have to be a bit careful—it contains a nested for-if-for, so you have to pay attention to the curly braces to put the print code in the correct place. Note that we stop when i > n/i, as we did for Factors.

i

isPrime[]

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

 

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

T

2

T

T

F

T

F

T

F

T

F

T

F

T

F

T

F

T

F

T

F

T

F

T

F

T

3

T

T

F

T

F

T

F

F

F

T

F

T

F

F

F

T

F

T

F

F

F

T

F

T

5

T

T

F

T

F

T

F

F

F

T

F

T

F

F

F

T

F

T

F

F

F

T

F

F

 

T

T

F

T

F

T

F

F

F

T

F

T

F

F

F

T

F

T

F

F

F

T

F

F

Trace of java PrimeSieve 25

With PrimeSieve, we can compute π(n) for large n, limited primarily by the maximum array length allowed by Java. This is another example of a space–time tradeoff. Programs like PrimeSieve play an important role in helping mathematicians to develop the theory of numbers, which has many important applications.

Two-dimensional arrays

In many applications, a convenient way to store information is to use a table of numbers organized in a rectangle and refer to rows and columns in the table. For example, a teacher might need to maintain a table with rows corresponding to students and columns corresponding to exams, a scientist might need to maintain a table of experimental data with rows corresponding to experiments and columns corresponding to various outcomes, or a programmer might want to prepare an image for display by setting a table of pixels to various grayscale values or colors.

The mathematical abstraction corresponding to such tables is a matrix; the corresponding Java construct is a two-dimensional array. You are likely to have already encountered many applications of matrices and two-dimensional arrays, and you will certainly encounter many others in science, engineering, and computing applications, as we will demonstrate with examples throughout this book. As with vectors and one-dimensional arrays, many of the most important applications involve processing large amounts of data, and we defer considering those applications until we introduce input and output, in SECTION 1.5.

An illustration depicts the anatomy of a two-dimensional array.

Extending Java array constructs to handle two-dimensional arrays is straightforward. To refer to the element in row i and column j of a two-dimensional array a[][], we use the notation a[i][j]; to declare a two-dimensional array, we add another pair of square brackets; and to create the array, we specify the number of rows followed by the number of columns after the type name (both within square brackets), as follows:

double[][] a = new double[m][n];

We refer to such an array as an m-by-n array. By convention, the first dimension is the number of rows and the second is the number of columns. As with one-dimensional arrays, Java initializes all elements in arrays of numbers to zero and in boolean arrays to false.

Default initialization

Default initialization of two-dimensional arrays is useful because it masks more code than for one-dimensional arrays. The following code is equivalent to the single-line create-and-initialize idiom that we just considered:

double[][] a;
a = new double[m][n];
for (int i = 0; i < m; i++)
{  // Initialize the ith row.
   for (int j = 0; j < n; j++)
      a[i][j] = 0.0;
}

This code is superfluous when initializing the elements of a two-dimensional array to zero, but the nested for loops are needed to initialize the elements to some other value(s). As you will see, this code is a model for the code that we use to access or modify each element of a two-dimensional array.

Output

We use nested for loops for many two-dimensional array-processing operations. For example, to print an m-by-n array in the tabular format, we can use the following code:

for (int i = 0; i < m; i++)
{  // Print the ith row.
   for (int j = 0; j < n; j++)
      System.out.print(a[i][j] + " ");
   System.out.println();
}

If desired, we could add code to embellish the output with row and column indices (see EXERCISE 1.4.6). Java programmers typically tabulate two-dimensional arrays with row indices running top to bottom from 0 and column indices running left to right from 0.

An illustration shows a ten by three array.
Memory representation

Java represents a two-dimensional array as an array of arrays. That is, a two-dimensional array with m rows and n columns is actually an array of length m, each element of which is a one-dimensional array of length n. In a two-dimensional Java array a[][], you can use the code a[i] to refer to row i (which is a one-dimensional array), but there is no corresponding way to refer to column j.

Setting values at compile time

The Java method for initializing an array of values at compile time follows immediately from the representation. A two-dimensional array is an array of rows, each row initialized as a one-dimensional array. To initialize a two-dimensional array, we enclose in curly braces a list of terms to initialize the rows, separated by commas. Each term in the list is itself a list: the values for the array elements in the row, enclosed in curly braces and separated by commas.

double[][] a =
{
   { 99.0, 85.0, 98.0,  0.0 },
   { 98.0, 57.0, 79.0,  0.0 },
   { 92.0, 77.0, 74.0,  0.0 },
   { 94.0, 62.0, 81.0,  0.0 },
   { 99.0, 94.0, 92.0,  0.0 },
   { 80.0, 76.5, 67.0,  0.0 },
   { 76.0, 58.5, 90.5,  0.0 },
   { 92.0, 66.0, 91.0,  0.0 },
   { 97.0, 70.5, 66.5,  0.0 },
   { 89.0, 89.5, 81.0,  0.0 },
   {  0.0,  0.0,  0.0,  0.0 }
};

Compile-time initialization of a of an 11-by-4 double array

Spreadsheets

One familiar use of arrays is a spreadsheet for maintaining a table of numbers. For example, a teacher with m students and n test grades for each student might maintain an (m +1)-by-(n +1) array, reserving the last column for each student’s average grade and the last row for the average test grades. Even though we typically do such computations within specialized applications, it is worthwhile to study the underlying code as an introduction to array processing. To compute the average grade for each student (average values for each row), sum the elements for each row and divide by n. The row-by-row order in which this code processes the matrix elements is known as row-major order. Similarly, to compute the average test grade (average values for each column), sum the elements for each column and divide by m. The column-by-column order in which this code processes the matrix elements is known as column-major order.

An illustration shows the typical spreadsheet calculations.
Matrix operations

Typical applications in science and engineering involve representing matrices as two-dimensional arrays and then implementing various mathematical operations with matrix operands. Again, even though such processing is often done within specialized applications, it is worthwhile for you to understand the underlying computation. For example, you can add two n-by-n matrices as follows:

double[][] c = new double[n][n];
for (int i = 0; i < n; i++)
   for (int j = 0; j < n; j++)
      c[i][j] = a[i][j] + b[i][j];
An illustration shows the matrix addition.

Similarly, you can multiply two matrices. You may have learned matrix multiplication, but if you do not recall or are not familiar with it, the Java code below for multiplying two square matrices is essentially the same as the mathematical definition. Each element c[i][j] in the product of a[][] and b[][] is computed by taking the dot product of row i of a[][] with column j of b[][].

double[][] c = new double[n][n];
for (int i = 0; i < n; i++)
{
   for (int j = 0; j < n; j++)
   {
      // Dot product of row i and column j.
      for (int k = 0; k < n; k++)
         c[i][j] += a[i][k]*b[k][j];
   }
}
An illustration shows the matrix multiplication.
Special cases of matrix multiplication

Two special cases of matrix multiplication are important. These special cases occur when one of the dimensions of one of the matrices is 1, so it may be viewed as a vector. We have matrix–vector multiplication, where we multiply an m-by-n matrix by a column vector (an n-by-1 matrix) to get an m-by-1 column vector result (each element in the result is the dot product of the corresponding row in the matrix with the operand vector). The second case is vector–matrix multiplication, where we multiply a row vector (a 1-by-m matrix) by an m-by-n matrix to get a 1-by-n row vector result (each element in the result is the dot product of the operand vector with the corresponding column in the matrix).

These operations provide a succinct way to express numerous matrix calculations. For example, the row-average computation for such a spreadsheet with m rows and n columns is equivalent to a matrix–vector multiplication where the row vector has n elements all equal to 1/n. Similarly, the column-average computation in such a spreadsheet is equivalent to a vector–matrix multiplication where the column vector has m elements all equal to 1/m. We return to vector–matrix multiplication in the context of an important application at the end of this chapter.

An illustration with two sections shows the matrix-vector and vector-matrix multiplication.
Ragged arrays

There is actually no requirement that all rows in a two-dimensional array have the same length—an array with rows of nonuniform length is known as a ragged array (see EXERCISE 1.4.34 for an example application). The possibility of ragged arrays creates the need for more care in crafting array-processing code. For example, this code prints the contents of a ragged array:

for (int i = 0; i < a.length; i++)
{
   for (int j = 0; j < a[i].length; j++)
      System.out.print(a[i][j] + " ");
   System.out.println();
}

This code tests your understanding of Java arrays, so you should take the time to study it. In this book, we normally use square or rectangular arrays, whose dimension are given by the variable m or n. Code that uses a[i].length in this way is a clear signal to you that an array is ragged.

Multidimensional arrays

The same notation extends to allow us to write code using arrays that have any number of dimensions. For instance, we can declare and initialize a three-dimensional array with the code

double[][][] a = new double[n][n][n];

and then refer to an element with code like a[i][j][k], and so forth.

Two-dimensional arrays provide a natural representation for matrices, which are omnipresent in science, mathematics, and engineering. They also provide a natural way to organize large amounts of data—a key component in spreadsheets and many other computing applications. Through Cartesian coordinates, two- and three-dimensional arrays also provide the basis for models of the physical world. We consider their use in all three arenas throughout this book.

Example: self-avoiding random walks

Suppose that you leave your dog in the middle of a large city whose streets form a familiar grid pattern. We assume that there are n north–south streets and n east–west streets all regularly spaced and fully intersecting in a pattern known as a lattice. Trying to escape the city, the dog makes a random choice of which way to go at each intersection, but knows by scent to avoid visiting any place previously visited. But it is possible for the dog to get stuck in a dead end where there is no choice but to revisit some intersection. What is the chance that this will happen? This amusing problem is a simple example of a famous model known as the self-avoiding random walk, which has important scientific applications in the study of polymers and in statistical mechanics, among many others. For example, you can see that this process models a chain of material growing a bit at a time, until no growth is possible. To better understand such processes, scientists seek to understand the properties of self-avoiding walks.

Figure shows two grid patterns for self-avoiding walks.

The dog’s escape probability is certainly dependent on the size of the city. In a tiny 5-by-5 city, it is easy to convince yourself that the dog is certain to escape. But what are the chances of escape when the city is large? We are also interested in other parameters. For example, how long is the dog’s path, on the average? How often does the dog come within one block of escaping? These sorts of properties are important in the various applications just mentioned.

SelfAvoidingWalk (PROGRAM 1.4.4) is a simulation of this situation that uses a two-dimensional boolean array, where each element represents an intersection. The value true indicates that the dog has visited the intersection; false indicates that the dog has not visited the intersection. The path starts in the center and takes random steps to places not yet visited until getting stuck or escaping at a boundary. For simplicity, the code is written so that if a random choice is made to go to a spot that has already been visited, it takes no action, trusting that some subsequent random choice will find a new place (which is assured because the code explicitly tests for a dead end and terminates the loop in that case).

Note that the code depends on Java initializing all of the array elements to false for each experiment. It also exhibits an important programming technique where we code the loop exit test in the while statement as a guard against an illegal statement in the body of the loop. In this case, the while loop-continuation condition serves as a guard against an out-of-bounds array access within the loop. This corresponds to checking whether the dog has escaped. Within the loop, a successful dead-end test results in a break out of the loop.


Program 1.4.4 Self-avoiding random walks

public class SelfAvoidingWalk
{
   public static void main(String[] args)

   {  // Do trials random self-avoiding
      // walks in an n-by-n lattice.
      int n = Integer.parseInt(args[0]);
      int trials = Integer.parseInt(args[1]);
      int deadEnds = 0;
      for (int t = 0; t < trials; t++)
      {
         boolean[][] a = new boolean[n][n];
         int x = n/2, y = n/2;
         while (x > 0 && x < n-1 && y > 0 && y < n-1)
         {  // Check for dead end and make a random move.
            a[x][y] = true;
            if (a[x-1][y] && a[x+1][y] && a[x][y-1] && a[x][y+1])
            {  deadEnds++; break;  }
            double r = Math.random();
            if      (r < 0.25) { if (!a[x+1][y]) x++; }
            else if (r < 0.50) { if (!a[x-1][y]) x--; }
            else if (r < 0.75) { if (!a[x][y+1]) y++; }
            else if (r < 1.00) { if (!a[x][y-1]) y--; }
         }
      }
      System.out.println(100*deadEnds/trials + "% dead ends");
   }
}

    n    | lattice size
 trials  | # trials
deadEnds | # trials resulting in a dead end
 a[][]   | intersections visited
  x, y   | current position
    r    | random number in (0, 1)

This program takes command-line arguments n and trials and computes trials self-avoiding walks in an n-by-n lattice. For each walk, it creates a boolean array, starts the walk in the center, and continues until either a dead end or a boundary is reached. The result of the computation is the percentage of dead ends. Increasing the number of experiments increases the precision.


% java SelfAvoidingWalk 5 100
0% dead ends
% java SelfAvoidingWalk 20 100
36% dead ends
% java SelfAvoidingWalk 40 100
80% dead ends
% java SelfAvoidingWalk 80 100
98% dead ends

% java SelfAvoidingWalk 5 1000
0% dead ends
% java SelfAvoidingWalk 20 1000
32% dead ends
% java SelfAvoidingWalk 40 1000
70% dead ends
% java SelfAvoidingWalk 80 1000
95% dead ends

Figure shows a self-avoiding random walk in a 21-by-21 grid in eight rows and six columns, each.

As you can see from the sample runs on the facing page, the unfortunate truth is that your dog is nearly certain to get trapped in a dead end in a large city. If you are interested in learning more about self-avoiding walks, you can find several suggestions in the exercises. For example, the dog is virtually certain to escape in the three-dimensional version of the problem. While this is an intuitive result that is confirmed by our tests, the development of a mathematical model that explains the behavior of self-avoiding walks is a famous open problem; despite extensive research, no one knows a succinct mathematical expression for the escape probability, the average length of the path, or any other important parameter.

Summary

Arrays are the fourth basic element (after assignments, conditionals, and loops) found in virtually every programming language, completing our coverage of basic Java constructs. As you have seen with the sample programs that we have presented, you can write programs that can solve all sorts of problems using just these constructs.

Arrays are prominent in many of the programs that we consider, and the basic operations that we have discussed here will serve you well in addressing many programming tasks. When you are not using arrays explicitly (and you are sure to do so frequently), you will be using them implicitly, because all computers have a memory that is conceptually equivalent to an array.

The fundamental ingredient that arrays add to our programs is a potentially huge increase in the size of a program’s state. The state of a program can be defined as the information you need to know to understand what a program is doing. In a program without arrays, if you know the values of the variables and which statement is the next to be executed, you can normally determine what the program will do next. When we trace a program, we are essentially tracking its state. When a program uses arrays, however, there can be too huge a number of values (each of which might be changed in each statement) for us to effectively track them all. This difference makes writing programs with arrays more of a challenge than writing programs without them.

Arrays directly represent vectors and matrices, so they are of direct use in computations associated with many basic problems in science and engineering. Arrays also provide a succinct notation for manipulating a potentially huge amount of data in a uniform way, so they play a critical role in any application that involves processing large amounts of data, as you will see throughout this book.

Q&A

Q. Some Java programmers use int a[] instead of int[] a to declare arrays. What’s the difference?

A. In Java, both are legal and essentially equivalent. The former is how arrays are declared in C. The latter is the preferred style in Java since the type of the variable int[] more clearly indicates that it is an array of integers.

Q. Why do array indices start at 0 instead of 1?

A. This convention originated with machine-language programming, where the address of an array element would be computed by adding the index to the address of the beginning of an array. Starting indices at 1 would entail either a waste of space at the beginning of the array or a waste of time to subtract the 1.

Q. What happens if I use a negative integer to index an array?

A. The same thing as when you use an index that is too large. Whenever a program attempts to index an array with an index that is not between 0 and the array length minus 1, Java will issue an ArrayIndexOutOfBoundsException.

Q. Must the entries in an array initializer be literals?

A. No. The entries in an array initializer can be arbitrary expressions (of the specified type), even if their values are not known at compile time. For example, the following code fragment initializes a two-dimensional array using a command-line argument theta:

double theta = Double.parseDouble(args[0]);
double[][] rotation =
{
   { Math.cos(theta), -Math.sin(theta) },
   { Math.sin(theta),  Math.cos(theta) },
};

Q. Is there a difference between an array of characters and a String?

A. Yes. For example, you can change the individual characters in a char[] but not in a String. We will consider strings in detail in SECTION 3.1.

Q. What happens when I compare two arrays with (a == b)?

A. The expression evaluates to true if and only if a[] and b[] refer to the same array (memory address), not if they store the same sequence of values. Unfortunately, this is rarely what you want. Instead, you can use a loop to compare the corresponding elements.

Q. What happens when I use an array in an assignment statement like a = b?

A. The assignment statement makes the variable a refer to the same array as b—it does not copy the values from the array b to the array a, as you might expect. For example, consider the following code fragment:

int[] a = { 1, 2, 3, 4 };
int[] b = { 5, 6, 7, 8 };
a = b;
a[0] = 9;

After the assignment statement a = b, we have a[0] equal to 5, a[1] equal to 6, and so forth, as expected. That is, the arrays correspond to the same sequence of values. However, they are not independent arrays. For example, after the last statement, not only is a[0] equal to 9, but b[0] is equal to 9 as well. This is one of the key differences between primitive types (such as int and double) and nonprimitive types (such as arrays). We will revisit this subtle (but fundamental) distinction in more detail when we consider passing arrays to functions in SECTION 2.1 and reference types in SECTION 3.1.

Q. If a[] is an array, why does System.out.println(a) print something like @f62373, instead of the sequence of values in the array?

A. Good question. It prints the memory address of the array (as a hexadecimal integer), which, unfortunately, is rarely what you want.

Q. Which other pitfalls should I watch out for when using arrays?

A. It is very important to remember that Java automatically initializes arrays when you create them, so that creating an array takes time proportional to its length.

Exercises

1.4.1 Write a program that declares, creates, and initializes an array a[] of length 1000 and accesses a[1000]. Does your program compile? What happens when you run it?

1.4.2 Describe and explain what happens when you try to compile a program with the following statement:

int n = 1000;
int[] a = new int[n*n*n*n];

1.4.3 Given two vectors of length n that are represented with one-dimensional arrays, write a code fragment that computes the Euclidean distance between them (the square root of the sums of the squares of the differences between corresponding elements).

1.4.4 Write a code fragment that reverses the order of the values in a one-dimensional string array. Do not create another array to hold the result. Hint: Use the code in the text for exchanging the values of two elements.

1.4.5 What is wrong with the following code fragment?

int[] a;
for (int i = 0; i < 10; i++)
   a[i] = i * i;

1.4.6 Write a code fragment that prints the contents of a two-dimensional boolean array, using * to represent true and a space to represent false. Include row and column indices.

1.4.7 What does the following code fragment print?

int[] a = new int[10];
for (int i = 0; i < 10; i++)
   a[i] = 9 - i;
for (int i = 0; i < 10; i++)
   a[i] = a[a[i]];
for (int i = 0; i < 10; i++)
   System.out.println(a[i]);

1.4.8 Which values does the following code put in the array a[]?

int n = 10;
int[] a = new int[n];
a[0] = 1;
a[1] = 1;
for (int i = 2; i < n; i++)
   a[i] = a[i-1] + a[i-2];

1.4.9 What does the following code fragment print?

int[] a = { 1, 2, 3 };
int[] b = { 1, 2, 3 };
System.out.println(a == b);

1.4.10 Write a program Deal that takes an integer command-line argument n and prints n poker hands (five cards each) from a shuffled deck, separated by blank lines.

1.4.11 Write a program HowMany that takes a variable number of command-line arguments and prints how many there are.

1.4.12 Write a program DiscreteDistribution that takes a variable number of integer command-line arguments and prints the integer i with probability proportional to the ith command-line argument.

1.4.13 Write code fragments to create a two-dimensional array b[][] that is a copy of an existing two-dimensional array a[][], under each of the following assumptions:

a. a[][] is square

b. a[][] is rectangular

c. a[][] may be ragged

Your solution to b should work for a, and your solution to c should work for both b and a, and your code should get progressively more complicated.

1.4.14 Write a code fragment to print the transposition (rows and columns exchanged) of a square two-dimensional array. For the example spreadsheet array in the text, you code would print the following:

99  98  92  94  99  90  76  92  97  89
85  57  77  32  34  46  59  66  71  29
98  78  76  11  22  54  88  89  24  38

1.4.15 Write a code fragment to transpose a square two-dimensional array in place without creating a second array.

1.4.16 Write a program that takes an integer command-line argument n and creates an n-by-n boolean array a[][] such that a[i][j] is true if i and j are relatively prime (have no common factors), and false otherwise. Use your solution to EXERCISE 1.4.6 to print the array. Hint: Use sieving.

1.4.17 Modify the spreadsheet code fragment in the text to compute a weighted average of the rows, where the weights of each exam score are in a one-dimensional array weights[]. For example, to assign the last of the three exams in our example to be twice the weight of the first two, you would use

double[] weights = { 0.25, 0.25, 0.50 };

Note that the weights should sum to 1.

1.4.18 Write a code fragment to multiply two rectangular matrices that are not necessarily square. Note: For the dot product to be well defined, the number of columns in the first matrix must be equal to the number of rows in the second matrix. Print an error message if the dimensions do not satisfy this condition.

1.4.19 Write a program that multiplies two square boolean matrices, using the or operation instead of + and the and operation instead of *.

1.4.20 Modify SelfAvoidingWalk (PROGRAM 1.4.4) to calculate and print the average length of the paths as well as the dead-end probability. Keep separate the average lengths of escape paths and dead-end paths.

1.4.21 Modify SelfAvoidingWalk to calculate and print the average area of the smallest axis-aligned rectangle that encloses the dead-end paths.

Creative Exercises

1.4.22 Dice simulation. The following code computes the exact probability distribution for the sum of two dice:

int[] frequencies = new int[13];
for (int i = 1; i <= 6; i++)
   for (int j = 1; j <= 6; j++)
      frequencies[i+j]++;

double[] probabilities = new double[13];
for (int k = 1; k <= 12; k++)
   probabilities[k] = frequencies[k] / 36.0;

The value probabilities[k] is the probability that the dice sum to k. Run experiments that validate this calculation by simulating n dice throws, keeping track of the frequencies of occurrence of each value when you compute the sum of two uniformly random integers between 1 and 6. How large does n have to be before your empirical results match the exact results to three decimal places?

1.4.23 Longest plateau. Given an array of integers, find the length and location of the longest contiguous sequence of equal values for which the values of the elements just before and just after this sequence are smaller.

1.4.24 Empirical shuffle check. Run computational experiments to check that our shuffling code works as advertised. Write a program ShuffleTest that takes two integer command-line arguments m and n, does n shuffles of an array of length m that is initialized with a[i] = i before each shuffle, and prints an m-by-m table such that row i gives the number of times i wound up in position j for all j. All values in the resulting array should be close to n / m.

1.4.25 Bad shuffling. Suppose that you choose a random integer between 0 and n-1 in our shuffling code instead of one between i and n-1. Show that the resulting order is not equally likely to be one of the n! possibilities. Run the test of the previous exercise for this version.

1.4.26 Music shuffling. You set your music player to shuffle mode. It plays each of the n songs before repeating any. Write a program to estimate the likelihood that you will not hear any sequential pair of songs (that is, song 3 does not follow song 2, song 10 does not follow song 9, and so on).

1.4.27 Minima in permutations. Write a program that takes an integer command-line argument n, generates a random permutation, prints the permutation, and prints the number of left-to-right minima in the permutation (the number of times an element is the smallest seen so far). Then write a program that takes two integer command-line arguments m and n, generates m random permutations of length n, and prints the average number of left-to-right minima in the permutations generated. Extra credit: Formulate a hypothesis about the number of left-to-right minima in a permutation of length n, as a function of n.

1.4.28 Inverse permutation. Write a program that reads in a permutation of the integers 0 to n-1 from n command-line arguments and prints the inverse permutation. (If the permutation is in an array a[], its inverse is the array b[] such that a[b[i]] = b[a[i]] = i.) Be sure to check that the input is a valid permutation.

1.4.29 Hadamard matrix. The n-by-n Hadamard matrix H(n) is a boolean matrix with the remarkable property that any two rows differ in exactly n / 2 values. (This property makes it useful for designing error-correcting codes.) H(1) is a 1-by-1 matrix with the single element true, and for n > 1, H(2n) is obtained by aligning four copies of H(n) in a large square, and then inverting all of the values in the lower right n-by-n copy, as shown in the following examples (with T representing true and F representing false, as usual).

H(1)

H(2)

H(4)

T

T T

T T T T

 

T F

T F T F

 

 

T T F F

 

 

T F F T

Write a program that takes an integer command-line argument n and prints H(n). Assume that n is a power of 2.

1.4.30 Rumors. Alice is throwing a party with n other guests, including Bob. Bob starts a rumor about Alice by telling it to one of the other guests. A person hearing this rumor for the first time will immediately tell it to one other guest, chosen uniformly at random from all the people at the party except Alice and the person from whom they heard it. If a person (including Bob) hears the rumor for a second time, he or she will not propagate it further. Write a program to estimate the probability that everyone at the party (except Alice) will hear the rumor before it stops propagating. Also calculate an estimate of the expected number of people to hear the rumor.

1.4.31 Counting primes. Compare PrimeSieve with the method that we used to demonstrate the break statement, at the end of SECTION 1.3. This is a classic example of a space–time tradeoff: PrimeSieve is fast, but requires a boolean array of length n; the other approach uses only two integer variables, but is substantially slower. Estimate the magnitude of this difference by finding the value of n for which this second approach can complete the computation in about the same time as java PrimeSeive 1000000.

1.4.32 Minesweeper. Write a program that takes three command-line arguments m, n, and p and produces an m-by-n boolean array where each element is occupied with probability p. In the minesweeper game, occupied cells represent bombs and empty cells represent safe cells. Print out the array using an asterisk for bombs and a period for safe cells. Then, create an integer two-dimensional array with the number of neighboring bombs (above, below, left, right, or diagonal).

* * . . .       * * 1 0 0
. . . . .       3 3 2 0 0
. * . . .       1 * 1 0 0

Write your code so that you have as few special cases as possible to deal with, by using an (m+2)-by-(n+2) boolean array.

1.4.33 Find a duplicate. Given an integer array of length n, with each value between 1 and n, write a code fragment to determine whether there are any duplicate values. You may not use an extra array (but you do not need to preserve the contents of the given array.)

1.4.34 Self-avoiding walk length. Suppose that there is no limit on the size of the grid. Run experiments to estimate the average path length.

1.4.35 Three-dimensional self-avoiding walks. Run experiments to verify that the dead-end probability is 0 for a three-dimensional self-avoiding walk and to compute the average path length for various values of n.

1.4.36 Random walkers. Suppose that n random walkers, starting in the center of an n-by-n grid, move one step at a time, choosing to go left, right, up, or down with equal probability at each step. Write a program to help formulate and test a hypothesis about the number of steps taken before all cells are touched.

1.4.37 Bridge hands. In the game of bridge, four players are dealt hands of 13 cards each. An important statistic is the distribution of the number of cards in each suit in a hand. Which is the most likely, 5–3-3–2, 4–4-3–2, or 4–3–3–3?

1.4.38 Birthday problem. Suppose that people enter an empty room until a pair of people share a birthday. On average, how many people will have to enter before there is a match? Run experiments to estimate the value of this quantity. Assume birthdays to be uniform random integers between 0 and 364.

1.4.39 Coupon collector. Run experiments to validate the classical mathematical result that the expected number of coupons needed to collect n values is approximately n Hn, where Hn in the nth harmonic number. For example, if you are observing the cards carefully at the blackjack table (and the dealer has enough decks randomly shuffled together), you will wait until approximately 235 cards are dealt, on average, before seeing every card value.

1.4.40 Riffle shuffle. Compose a program to rearrange a deck of n cards using the Gilbert–Shannon–Reeds model of a riffle shuffle. First, generate a random integer r according to a binomial distribution: flip a fair coin n times and let r be the number of heads. Now, divide the deck into two piles: the first r cards and the remaining nr cards. To complete the shuffle, repeatedly take the top card from one of the two piles and put it on the bottom of a new pile. If there are n1 cards remaining in the first pile and n2 cards remaining in the second pile, choose the next card from the first pile with probability n1 / (n1 + n2) and from the second pile with probability n2 / (n1 + n2). Investigate how many riffle shuffles you need to apply to a deck of 52 cards to produce a (nearly) uniformly shuffled deck.

1.4.41 Binomial distribution. Write a program that takes an integer commandline argument n and creates a two-dimensional ragged array a[][] such that a[n][k] contains the probability that you get exactly k heads when you toss a fair coin n times. These numbers are known as the binomial distribution: if you multiply each element in row i by 2n, you get the binomial coefficients—the coefficients of xk in (x+1)n—arranged in Pascal’s triangle. To compute them, start with a[n][0] = 0.0 for all n and a[1][1] = 1.0, then compute values in successive rows, left to right, with a[n][k] = (a[n-1][k] + a[n-1][k-1]) / 2.0.

Pascal’s triangle

binomial distribution

1

1

1 1

1/2 1/2

1 2 1

1/4 1/2 1/4

1 3 3 1

1/8 3/8 3/8 1/8

1 4 6 4 1

1/16 1/4 3/8 1/4 1/16

1.5 Input and Output

In this section we extend the set of simple abstractions (command-line arguments and standard output) that we have been using as the interface between our Java programs and the outside world to include standard input, standard drawing, and standard audio. Standard input makes it convenient for us to write programs that process arbitrary amounts of input and to interact with our programs; standard drawing makes it possible for us to work with graphical representations of images, freeing us from having to encode everything as text; and standard audio adds sound. These extensions are easy to use, and you will find that they bring you to yet another new world of programming.

The abbreviation I/O is universally understood to mean input/output, a collective term that refers to the mechanisms by which programs communicate with the outside world. Your computer’s operating system controls the physical devices that are connected to your computer. To implement the standard I/O abstractions, we use libraries of methods that interface to the operating system.

You have already been accepting arguments from the command line and printing strings in a terminal window; the purpose of this section is to provide you with a much richer set of tools for processing and presenting data. Like the System.out.print() and System.out.println() methods that you have been using, these methods do not implement pure mathematical functions—their purpose is to cause some side effect, either on an input device or an output device. Our prime concern is using such devices to get information into and out of our programs.

An essential feature of standard I/O mechanisms is that there is no limit on the amount of input or output, from the point of view of the program. Your programs can consume input or produce output indefinitely.

One use of standard I/O mechanisms is to connect your programs to files on your computer’s external storage. It is easy to connect standard input, standard output, standard drawing, and standard audio to files. Such connections make it easy to have your Java programs save or load results to files for archival purposes or for later reference by other programs or other applications.

Bird’s-eye view

The conventional model that we have been using for Java programming has served us since SECTION 1.1. To build context, we begin by briefly reviewing the model.

A Java program takes input strings from the command line and prints a string of characters as output. By default, both command-line arguments and standard output are associated with the application that takes commands (the one in which you have been typing the java and javac commands). We use the generic term terminal window to refer to this application. This model has proved to be a convenient and direct way for us to interact with our programs and data.

Command-line arguments

This mechanism, which we have been using to provide input values to our programs, is a standard part of Java programming. All of our classes have a main() method that takes a String array args[] as its argument. That array is the sequence of command-line arguments that we type, provided to Java by the operating system. By convention, both Java and the operating system process the arguments as strings, so if we intend for an argument to be a number, we use a method such as Integer.parseInt() or Double.parseDouble() to convert it from String to the appropriate type.

Standard output

To print output values in our programs, we have been using the system methods System.out.println() and System.out.print(). Java puts the results of a program’s sequence of these method calls into the form of an abstract stream of characters known as standard output. By default, the operating system connects standard output to the terminal window. All of the output in our programs so far has been appearing in the terminal window.

For reference, and as a starting point, RandomSeq (PROGRAM 1.5.1) is a program that uses this model. It takes a command-line argument n and produces an output sequence of n random numbers between 0 and 1.

Now we are going to complement command-line arguments and standard output with three additional mechanisms that address their limitations and provide us with a far more useful programming model. These mechanisms give us a new bird’s-eye view of a Java program in which the program converts a standard input stream and a sequence of command-line arguments into a standard output stream, a standard drawing, and a standard audio stream.


Program 1.5.1 Generating a random sequence

public class RandomSeq
{
   public static void main(String[] args)
   {  // Print a random sequence of n real values in [0, 1)
      int n = Integer.parseInt(args[0]);
      for (int i = 0; i < n; i++)
         System.out.println(Math.random());
   }
}

This program illustrates the conventional model that we have been using so far for Java programming. It takes a command-line argument n and prints n random numbers between 0.0 and 1.0. From the program’s point of view, there is no limit on the length of the output sequence.


% java RandomSeq 1000000
0.2498362534343327
0.5578468691774513
0.5702167639727175
0.32191774192688727
0.6865902823177537
...

Standard input

Our class StdIn is a library that implements a standard input abstraction to complement the standard output abstraction. Just as you can print a value to standard output at any time during the execution of your program, so you can read a value from a standard input stream at any time.

Standard drawing

Our class StdDraw allows you to create drawings with your programs. It uses a simple graphics model that allows you to create drawings consisting of points and lines in a window on your computer. StdDraw also includes facilities for text, color, and animation.

Standard audio

Our class StdAudio allows you to create sound with your programs. It uses a standard format to convert arrays of numbers into sound.

To use both command-line arguments and standard output, you have been using built-in Java facilities. Java also has built-in facilities that support abstractions like standard input, standard drawing, and standard audio, but they are somewhat more complicated to use, so we have developed a simpler interface to them in our StdIn, StdDraw, and StdAudio libraries. To logically complete our programming model, we also include a StdOut library. To use these libraries, you must make StdIn.java, StdOut.java, StdDraw.java, and StdAudio.java available to Java (see the Q&A at the end of this section for details).

The standard input and standard output abstractions date back to the development of the UNIX operating system in the 1970s and are found in some form on all modern systems. Although they are primitive by comparison to various mechanisms developed since then, modern programmers still depend on them as a reliable way to connect data to programs. We have developed for this book standard drawing and standard audio in the same spirit as these earlier abstractions to provide you with an easy way to produce visual and aural output.

An illustration shows a bird’s-eye view of a Java program.

Standard output

Java’s System.out.print() and System.out.println() methods implement the basic standard output abstraction that we need. Nevertheless, to treat standard input and standard output in a uniform manner (and to provide a few technical improvements), starting in this section and continuing through the rest of the book, we use similar methods that are defined in our StdOut library. StdOut.print() and StdOut.println() are nearly the same as the Java methods that you have been using (see the booksite for a discussion of the differences, which need not concern you now). The StdOut.printf() method is a main topic of this section and will be of interest to you now because it gives you more control over the appearance of the output. It was a feature of the C language of the early 1970s that still survives in modern languages because it is so useful.

Since the first time that we printed double values, we have been distracted by excessive precision in the printed output. For example, when we use System.out.print(Math.PI) we get the output 3.141592653589793, even though we might prefer to see 3.14 or 3.14159. The print() and println() methods present each number to up to 15 decimal places even when we would be happy with only a few. The printf() method is more flexible. For example, it allows us to specify the number of decimal places when converting floating-point numbers to strings for output. We can write StdOut.printf("%7.5f", Math.PI) to get 3.14159, and we can replace System.out.print(t) with

StdOut.printf("The square root of %.1f is %.6f", c, t);

in Newton (PROGRAM 1.3.6) to get output like

The square root of 2.0 is 1.414214

public class StdOut

void print(String s)

print s to standard output

void println(String s)

print s and a newline to standard output

void println()

print a newline to standard output

void printf(String format, ... )

print the arguments to standard output, as specified by the format string format

API for our library of static methods for standard output

Next, we describe the meaning and operation of these statements, along with extensions to handle the other built-in types of data.

Formatted printing basics

In its simplest form, printf() takes two arguments. The first argument is called the format string. It contains a conversion specification that describes how the second argument is to be converted to a string for output. A conversion specification has the form %w.pc, where w and p are integers and c is a character, to be interpreted as follows:

w is the field width, the number of characters that should be written. If the number of characters to be written exceeds (or equals) the field width, then the field width is ignored; otherwise, the output is padded with spaces on the left. A negative field width indicates that the output instead should be padded with spaces on the right.

.p is the precision. For floating-point numbers, the precision is the number of digits that should be written after the decimal point; for strings, it is the number of characters of the string that should be printed. The precision is not used with integers.

c is the conversion code. The conversion codes that we use most frequently are d (for decimal values from Java’s integer types), f (for floating-point values), e (for floating-point values using scientific notation), s (for string values), and b (for boolean values).

The anatomy of a formatted print statement is displayed.

The field width and precision can be omitted, but every specification must have a conversion code.

The most important thing to remember about using printf() is that the conversion code and the type of the corresponding argument must match. That is, Java must be able to convert from the type of the argument to the type required by the conversion code. Every type of data can be converted to String, but if you write StdOut.printf("%12d", Math.PI) or StdOut.printf("%4.2f", 512), you will get an IllegalFormatConversionException run-time error.

Format string

The format string can contain characters in addition to those for the conversion specification. The conversion specification is replaced by the argument value (converted to a string as specified) and all remaining characters are passed through to the output. For example, the statement

StdOut.printf("PI is approximately %.2f.
", Math.PI);

prints the line

PI is approximately 3.14.

Note that we need to explicitly include the newline character in the format string to print a new line with printf().

Multiple arguments

The printf() method can take more than two arguments. In this case, the format string will have an additional conversion specification for each additional argument, perhaps separated by other characters to pass through to the output. For example, if you were making payments on a loan, you might use code whose inner loop contains the statements

String formats = "%3s  $%6.2f   $%7.2f   $%5.2f
";
StdOut.printf(formats, month[i], pay, balance, interest);

to print the second and subsequent lines in a table like this (see EXERCISE 1.5.13):

     payment    balance  interest
Jan  $299.00   $9742.67   $41.67
Feb  $299.00   $9484.26   $40.59
Mar  $299.00   $9224.78   $39.52
...

Formatted printing is convenient because this sort of code is much more compact than the string-concatenation code that we have been using to create output strings. We have described only the basic options; see the booksite for more details.

type

code

typical literal

sample format strings

converted string values for output

int

d

512

"%14d"
"%-14d"

"           512"
"512           "

double

f

e

1595.1680010754388

"%14.2f"
"%.7f"
"%14.4e"

"       1595.17"
"1595.1680011"
"    1.5952e+03"

String

s

"Hello, World"

"%14s"
"%-14s"
"%-14.5s"

"  Hello, World"
"Hello, World  "
"Hello         "

boolean

b

true

"%b"

"true"

Format conventions for printf() (see the booksite for many other options)

Standard input

Our StdIn library takes data from a standard input stream that may be empty or may contain a sequence of values separated by whitespace (spaces, tabs, newline characters, and the like). Each value is a string or a value from one of Java’s primitive types. One of the key features of the standard input stream is that your program consumes values when it reads them. Once your program has read a value, it cannot back up and read it again. This assumption is restrictive, but it reflects the physical characteristics of some input devices. The API for StdIn appears on the facing page. The methods fall into one of four categories:

• Those for reading individual values, one at a time

• Those for reading lines, one at a time

• Those for reading characters, one at a time

• Those for reading a sequence of values of the same type

Generally, it is best not to mix functions from the different categories in the same program. These methods are largely self-documenting (the names describe their effect), but their precise operation is worthy of careful consideration, so we will consider several examples in detail.

public class StdIn

methods for reading individual tokens from standard input

boolean

isEmpty()

is standard input empty (or only whitespace)?

int

readInt()

read a token, convert it to an int, and return it

double

readDouble()

read a token, convert it to a double, and return it

boolean

readBoolean()

read a token, convert it to a boolean, and return it

String

readString()

read a token and return it as a String

methods for reading characters from standard input

boolean

hasNextChar()

does standard input have any remaining characters?

char

readChar()

read a character from standard input and return it

methods for reading lines from standard input

boolean

hasNextLine()

does standard input have a next line?

String

readLine()

read the rest of the line and return it as a String

methods for reading the rest of standard input

int[]

readAllInts()

read all remaining tokens and return them as an int array

double[]

readAllDoubles()

read all remaining tokens and return them as a double array

boolean[]

readAllBooleans()

read all remaining tokens and return them as a boolean array

String[]

readAllStrings()

read all remaining tokens and return them as a String array

String[]

readAllLines()

read all remaining lines and return them as a String array

String

readAll()

read the rest of the input and return it as a String

Note 1: A token is a maximal sequence of non-whitespace characters.

Note 2: Before reading a token, any leading whitespace is discarded.

Note 3: Analogous methods are available for reading values of type byte, short, long, and float.

Note 4: Each method that reads input throws a run-time exception if it cannot read in the next value, either because there is no more input or because the input does not match the expected type.

API for our library of static methods for standard input

Typing input

When you use the java command to invoke a Java program from the command line, you actually are doing three things: (1) issuing a command to start executing your program, (2) specifying the command-line arguments, and (3) beginning to define the standard input stream. The string of characters that you type in the terminal window after the command line is the standard input stream. When you type characters, you are interacting with your program. The program waits for you to type characters in the terminal window.

For example, consider the program AddInts, which takes a command-line argument n, then reads n numbers from standard input, adds them, and prints the result to standard output. When you type java AddInts 4, after the program takes the command-line argument, it calls the method StdIn.readInt() and waits for you to type an integer. Suppose that you want 144 to be the first value. As you type 1, then 4, and then 4, nothing happens, because StdIn does not know that you are done typing the integer. But when you then type <Return> to signify the end of your integer, StdIn.readInt() immediately returns the value 144, which your program adds to sum and then calls StdIn.readInt() again. Again, nothing happens until you type the second value: if you type 2, then 3, then 3, and then <Return> to end the number, StdIn.readInt() returns the value 233, which your program again adds to sum. After you have typed four numbers in this way, AddInts expects no more input and prints the sum, as desired.

The program AddInts and the anatomy of a command are depicted.
Input format

If you type abc or 12.2 or true when StdIn.readInt() is expecting an int, it will respond with an InputMismatchException. The format for each type is essentially the same as you have been using to specify literals within Java programs. For convenience, StdIn treats strings of consecutive whitespace characters as identical to one space and allows you to delimit your numbers with such strings. It does not matter how many spaces you put between numbers, or whether you enter numbers on one line or separate them with tab characters or spread them out over several lines, (except that your terminal application processes standard input one line at a time, so it will wait until you type <Return> before sending all of the numbers on that line to standard input). You can mix values of different types in an input stream, but whenever the program expects a value of a particular type, the input stream must have a value of that type.

Interactive user input

TwentyQuestions (PROGRAM 1.5.2) is a simple example of a program that interacts with its user. The program generates a random integer and then gives clues to a user trying to guess the number. (As a side note, by using binary search, you can always get to the answer in at most 20 questions. See SECTION 4.2.) The fundamental difference between this program and others that we have written is that the user has the ability to change the control flow while the program is executing. This capability was very important in early applications of computing, but we rarely write such programs nowadays because modern applications typically take such input through the graphical user interface, as discussed in CHAPTER 3. Even a simple program like TwentyQuestions illustrates that writing programs that support user interaction is potentially very difficult because you have to plan for all possible user inputs.


Program 1.5.2 Interactive user input

public class TwentyQuestions
{
   public static void main(String[] args)
   {  // Generate a number and answer questions
      // while the user tries to guess the value.
      int secret = 1 + (int) (Math.random() * 1000000);
      StdOut.print("I'm thinking of a number ");
      StdOut.println("between 1 and 1,000,000");
      int guess = 0;
      while (guess != secret)
      {  // Solicit one guess and provide one answer.
         StdOut.print("What's your guess? ");
         guess = StdIn.readInt();
         if (guess == secret) StdOut.println("You win!");
         if (guess < secret)  StdOut.println("Too low ");
         if (guess > secret)  StdOut.println("Too high");
      }
   }
}

secret | secret value
 guess | user's guess

This program plays a simple guessing game. You type numbers, each of which is an implicit question (“Is this the number?”) and the program tells you whether your guess is too high or too low. You can always get it to print You win! with fewer than 20 questions. To use this program, you StdIn and StdOut must be available to Java (see the first Q&A at the end of this section).


% java TwentyQuestions
I'm thinking of a number between 1 and 1,000,000
What's your guess? 500000
Too high
What's your guess? 250000
Too low
What's your guess? 375000
Too high
What's your guess? 312500
Too high
What's your guess? 300500
Too low
...

Processing an arbitrary-size input stream

Typically, input streams are finite: your program marches through the input stream, consuming values until the stream is empty. But there is no restriction of the size of the input stream, and some programs simply process all the input presented to them. Average (PROGRAM 1.5.3) is an example that reads in a sequence of floating-point numbers from standard input and prints their average. It illustrates a key property of using an input stream: the length of the stream is not known to the program. We type all the numbers that we have, and then the program averages them. Before reading each number, the program uses the method StdIn.isEmpty() to check whether there are any more numbers in the input stream. How do we signal that we have no more data to type? By convention, we type a special sequence of characters known as the end-of-file sequence. Unfortunately, the terminal applications that we typically encounter on modern operating systems use different conventions for this critically important sequence. In this book, we use <Ctrl-D> (many systems require <Ctrl-D> to be on a line by itself); the other widely used convention is <Ctrl-Z> on a line by itself. Average is a simple program, but it represents a profound new capability in programming: with standard input, we can write programs that process an unlimited amount of data. As you will see, writing such programs is an effective approach for numerous data-processing applications.

Standard input is a substantial step up from the command-line-arguments model that we have been using, for two reasons, as illustrated by TwentyQuestions and Average. First, we can interact with our program—with command-line arguments, we can provide data to the program only before it begins execution. Second, we can read in large amounts of data—with command-line arguments, we can enter only values that fit on the command line. Indeed, as illustrated by Average, the amount of data can be potentially unlimited, and many programs are made simpler by that assumption. A third raison d’être for standard input is that your operating system makes it possible to change the source of standard input, so that you do not have to type all the input. Next, we consider the mechanisms that enable this possibility.


Program 1.5.3 Averaging a stream of numbers

public class Average
{
   public static void main(String[] args)
   {  // Average the numbers on standard input.
      double sum = 0.0;
      int n = 0;
      while (!StdIn.isEmpty())
      {  // Read a number from standard input and add to sum.
         double value = StdIn.readDouble();
         sum += value;
         n++;
      }
      double average = sum / n;
      StdOut.println("Average is " + average);
   }
}

 n  | count of numbers read
sum | cumulated sum

This program reads in a sequence of floating-point numbers from standard input and prints their average on standard output (provided that the sum does not overflow). From its point of view, there is no limit on the size of the input stream. The commands on the right below use redirection and piping (discussed in the next subsection) to provide 100,000 numbers to average.


% java Average
10.0 5.0 6.0
3.0
7.0 32.0
<Ctrl-D>
Average is 10.5

% java RandomSeq 100000 > data.txt
% java Average < data.txt
Average is 0.5010473676174824
% java RandomSeq 100000 | java Average
Average is 0.5000499417963857

Redirection and piping

For many applications, typing input data as a standard input stream from the terminal window is untenable because our program’s processing power is then limited by the amount of data that we can type (and our typing speed). Similarly, we often want to save the information printed on the standard output stream for later use. To address such limitations, we next focus on the idea that standard input is an abstraction—the program expects to read data from an input stream but it has no dependence on the source of that input stream. Standard output is a similar abstraction. The power of these abstractions derives from our ability (through the operating system) to specify various other sources for standard input and standard output, such as a file, the network, or another program. All modern operating systems implement these mechanisms.

Redirecting standard output to a file

By adding a simple directive to the command that invokes a program, we can redirect its standard output stream to a file, either for permanent storage or for input to another program at a later time. For example,

% java RandomSeq 1000 > data.txt

specifies that the standard output stream is not to be printed in the terminal window, but instead is to be written to a text file named data.txt. Each call to System.out.print() or System.out.println() appends text at the end of that file. In this example, the end result is a file that contains 1,000 random values. No output appears in the terminal window: it goes directly into the file named after the > symbol. Thus, we can save away information for later retrieval. Note that we do not have to change RandomSeq (PROGRAM 1.5.1) in any way for this mechanism to work—it uses the standard output abstraction and is unaffected by our use of a different implementation of that abstraction. You can use redirection to save output from any program that you write. Once you have expended a significant amount of effort to obtain a result, you often want to save the result for later reference. In a modern system, you can save some information by using cut-and-paste or some similar mechanism that is provided by the operating system, but cut-and-paste is inconvenient for large amounts of data. By contrast, redirection is specifically designed to make it easy to handle large amounts of data.

A statement and a flowchart showing the redirecting of standard output to a file are shown.

Program 1.5.4 A simple filter

public class RangeFilter
{
   public static void main(String[] args)
   {  // Filter out numbers not between lo and hi.
      int lo = Integer.parseInt(args[0]);
      int hi = Integer.parseInt(args[1]);
      while (!StdIn.isEmpty())
      {  // Process one number.
         int value = StdIn.readInt();
         if (value >= lo && value <= hi)
            StdOut.print(value + " ");
      }
      StdOut.println();
   }
}

  lo  | lower bound of range
  hi  | upper bound of range
value | current number

This filter copies to the output stream the numbers from the input stream that fall inside the range given by the command-line arguments. There is no limit on the length of the streams.


% java RangeFilter 100 400
358 1330 55 165 689 1014 3066 387 575 843 203 48 292 877 65 998
358 165 387 203 292
<Ctrl-D>

Redirecting from a file to standard input

Similarly, we can redirect the standard input stream so that StdIn reads data from a file instead of the terminal window:

% java Average < data.txt

This command reads a sequence of numbers from the file data.txt and computes their average value. Specifically, the < symbol is a directive that tells the operating system to implement the standard input stream by reading from the text file data.txt instead of waiting for the user to type something into the terminal window. When the program calls StdIn.readDouble(), the operating system reads the value from the file. The file data.txt could have been created by any application, not just a Java program—many applications on your computer can create text files. This facility to redirect from a file to standard input enables us to create data-driven code where you can change the data processed by a program without having to change the program at all. Instead, you can keep data in files and write programs that read from the standard input stream.

A statement and a flowchart showing the redirecting from a file to standard input are shown.
Connecting two programs

The most flexible way to implement the standard input and standard output abstractions is to specify that they are implemented by our own programs! This mechanism is called piping. For example, the command

% java RandomSeq 1000 | java Average

specifies that the standard output stream for RandomSeq and the standard input stream for Average are the same stream. The effect is as if RandomSeq were typing the numbers it generates into the terminal window while Average is running. This example also has the same effect as the following sequence of commands:

% java RandomSeq 1000 > data.txt
% java Average < data.txt

In this case, the file data.txt is not created. This difference is profound, because it removes another limitation on the size of the input and output streams that we can process. For example, you could replace 1000 in the example with 1000000000, even though you might not have the space to save a billion numbers on our computer (you, however, do need the time to process them). When RandomSeq calls System.out.println(), a string is added to the end of the stream; when Average calls StdIn.readInt(), a string is removed from the beginning of the stream. The timing of precisely what happens is up to the operating system: it might run RandomSeq until it produces some output, and then run Average to consume that output, or it might run Average until it needs some input, and then run RandomSeq until it produces the needed input. The end result is the same, but your programs are freed from worrying about such details because they work solely with the standard input and standard output abstractions.

A statement and a flowchart showing piping output of one program to the input of another are displayed.
Filters

Piping, a core feature of the original Unix system of the early 1970s, still survives in modern systems because it is a simple abstraction for communicating among disparate programs. Testimony to the power of this abstraction is that many Unix programs are still being used today to process files that are thousands or millions of times larger than imagined by the programs’ authors. We can communicate with other Java programs via method calls, but standard input and standard output allow us to communicate with programs that were written at another time and, perhaps, in another language. With standard input and standard output, we are agreeing on a simple interface to the outside world.

For many common tasks, it is convenient to think of each program as a filter that converts a standard input stream to a standard output stream in some way, with piping as the command mechanism to connect programs together. For example, RangeFilter (PROGRAM 1.5.4) takes two command-line arguments and prints on standard output those numbers from standard input that fall within the specified range. You might imagine standard input to be measurement data from some instrument, with the filter being used to throw away data outside the range of interest for the experiment at hand.

Several standard filters that were designed for Unix still survive (sometimes with different names) as commands in modern operating systems. For example, the sort filter puts the lines on standard input in sorted order:

% java RandomSeq 6 | sort
0.035813305516568916
0.14306638757584322
0.348292877655532103
0.5761644592016527
0.7234592733392126
0.9795908813988247

We discuss sorting in SECTION 4.2. A second useful filter is grep, which prints the lines from standard input that match a given pattern. For example, if you type

% grep lo < RangeFilter.java

you get the result

     // Filter out numbers not between lo and hi.
     int lo = Integer.parseInt(args[0]);
         if (value >= lo && value <= hi)

Programmers often use tools such as grep to get a quick reminder of variable names or language usage details. A third useful filter is more, which reads data from standard input and displays it in your terminal window one screenful at a time. For example, if you type

% java RandomSeq 1000 | more

you will see as many numbers as fit in your terminal window, but more will wait for you to hit the space bar before displaying each succeeding screenful. The term filter is perhaps misleading: it was meant to describe programs like RangeFilter that write some subsequence of standard input to standard output, but it is now often used to describe any program that reads from standard input and writes to standard output.

Multiple streams

For many common tasks, we want to write programs that take input from multiple sources and/or produce output intended for multiple destinations. In SECTION 3.1 we discuss our Out and In libraries, which generalize StdOut and StdIn to allow for multiple input and output streams. These libraries include provisions for redirecting these streams not only to and from files, but also from web pages.

Processing large amounts of information plays an essential role in many applications of computing. A scientist may need to analyze data collected from a series of experiments, a stock trader may wish to analyze information about recent financial transactions, or a student may wish to maintain collections of music and movies. In these and countless other applications, data-driven programs are the norm. Standard output, standard input, redirection, and piping provide us with the capability to address such applications with our Java programs. We can collect data into files on our computer through the web or any of the standard devices and use redirection and piping to connect data to our programs. Many (if not most) of the programming examples that we consider throughout this book have this ability.

Standard drawing

Up to this point, our input/output abstractions have focused exclusively on text strings. Now we introduce an abstraction for producing drawings as output. This library is easy to use and allows us to take advantage of a visual medium to work with far more information than is possible with mere text.

As with StdIn and StdOut, our standard drawing abstraction is implemented in a library StdDraw that you will need to make available to Java (see the first Q&A at the end of this section). Standard drawing is very simple. We imagine an abstract drawing device capable of drawing lines and points on a two-dimensional canvas. The device is capable of responding to the commands that our programs issue in the form of calls to methods in StdDraw such as the following:

public class StdDraw (basic drawing commands)

void line(double x0, double y0, double x1, double y1)

void point(double x, double y)

Like the methods for standard input and standard output, these methods are nearly self-documenting: StdDraw.line() draws a straight line segment connecting the point (x0, y0) with the point (x1, y1) whose coordinates are given as arguments. StdDraw.point() draws a spot centered on the point (x, y) whose coordinates are given as arguments. The default scale is the unit square (all x- and y-coordinates between 0 and 1). StdDraw displays the canvas in a window on your computer’s screen, with black lines and points on a white background. The window includes a menu option to save your drawing to a file, in a format suitable for publishing on paper or on the web.

A statement and the output after executing the statement are shown.
Your first drawing

The HelloWorld equivalent for graphics programming with StdDraw is to draw an equilateral triangle with a point inside. To form the triangle, we draw three line segments: one from the point (0, 0) at the lower-left corner to the point (1, 0), one from that point to the third point at (1/2, 3/2), and one from that point back to (0, 0). As a final flourish, we draw a spot in the middle of the triangle. Once you have successfully compiled and run Triangle, you are off and running to write your own programs that draw figures composed of line segments and points. This ability literally adds a new dimension to the output that you can produce.

When you use a computer to create drawings, you get immediate feedback (the drawing) so that you can refine and improve your program quickly. With a computer program, you can create drawings that you could not contemplate making by hand. In particular, instead of viewing our data as merely numbers, we can use pictures, which are far more expressive. We will consider other graphics examples after we discuss a few other drawing commands.

A program and the output of the program are displayed.
Control commands

The default canvas size is 512-by-512 pixels; if you want to change it, call setCanvasSize() before any drawing commands. The default coordinate system for standard drawing is the unit square, but we often want to draw plots at different scales. For example, a typical situation is to use coordinates in some range for the x-coordinate, or the y-coordinate, or both. Also, we often want to draw line segments of different thickness and points of different size from the standard. To accommodate these needs, StdDraw has the following methods:

public class StdDraw (basic control commands)

void setCanvasSize(int w, int h)

create canvas in screen window of width w and height h (in pixels)

void setXscale(double x0, double x1)

reset x-scale to (x0, x1)

void setYscale(double y0, double y1)

reset y-scale to (y0, y1)

void setPenRadius(double radius)

set pen radius to radius

Note: Methods with the same names but no arguments reset to default values the unit square for the x- and y-scales, 0.002 for the pen radius.

For example, the two-call sequence

StdDraw.setXscale(x0, x1);
StdDraw.setYscale(y0, y1);

sets the drawing coordinates to be within a bounding box whose lower-left corner is at (x0, y0) and whose upper-right corner is at (x1, y1). Scaling is the simplest of the transformations commonly used in graphics. In the applications that we consider in this chapter, we use it in a straightforward way to match our drawings to our data.

A program and the output of the program are shown.

The pen is circular, so that lines have rounded ends, and when you set the pen radius to r and draw a point, you get a circle of radius r. The default pen radius is 0.002 and is not affected by coordinate scaling. This default is about 1/500 the width of the default window, so that if you draw 200 points equally spaced along a horizontal or vertical line, you will be able to see individual circles, but if you draw 250 such points, the result will look like a line. When you issue the command StdDraw.setPenRadius(0.01), you are saying that you want the thickness of the line segments and the size of the points to be five times the 0.002 standard.

Filtering data to standard drawing

One of the simplest applications of standard drawing is to plot data, by filtering it from standard input to standard drawing. PlotFilter (PROGRAM 1.5.5) is such a filter: it reads from standard input a sequence of points defined by (x, y) coordinates and draws a spot at each point. It adopts the convention that the first four numbers on standard input specify the bounding box, so that it can scale the plot without having to make an extra pass through all the points to determine the scale. The graphical representation of points plotted in this way is far more expressive (and far more compact) than the numbers themselves. The image that is produced by PROGRAM 1.5.5 makes it far easier for us to infer properties of the points (such as, for example, clustering of population centers when plotting points that represent city locations) than does a list of the coordinates. Whenever we are processing data that represents the physical world, a visual image is likely to be one of the most meaningful ways that we can use to display output. PlotFilter illustrates how easily you can create such an image.


Program 1.5.5 Standard input-to-drawing filter

public class PlotFilter
{
   public static void main(String[] args)
   {
      // Scale as per first four values.
      double x0 = StdIn.readDouble();
      double y0 = StdIn.readDouble();
      double x1 = StdIn.readDouble();
      double y1 = StdIn.readDouble();
      StdDraw.setXscale(x0, x1);
      StdDraw.setYscale(y0, y1);

     // Read the points and plot to standard drawing.
      while (!StdIn.isEmpty())
      {
         double x = StdIn.readDouble();
         double y = StdIn.readDouble();
         StdDraw.point(x, y);
      }
   }
}

 x0  | left bound
 y0  | bottom bound
 x1  | right bound
 y1  | top bound
x, y | current point

This program reads a sequence of points from standard input and plots them to standard drawing. (By convention, the first four numbers are the minimum and maximum x- and y-coordinates.) The file USA.txt contains the coordinates of 13,509 cities in the United States


Map of the United States is shown.

Plotting a function graph

Another important use of standard drawing is to plot experimental data or the values of a mathematical function. For example, suppose that we want to plot values of the function y = sin(4x) + sin(20x) in the interval [0, π]. Accomplishing this task is a prototypical example of sampling: there are an infinite number of points in the interval but we have to make do with evaluating the function at a finite number of such points. We sample the function by choosing a set of x-values, then computing y-values by evaluating the function at each of these x-value. Plotting the function by connecting successive points with lines produces what is known as a piecewise linear approximation. The simplest way to proceed is to evenly space the x-values. First, we decide ahead of time on a sample size, then we space the x-values by the interval size divided by the sample size. To make sure that the values we plot fall in the visible canvas, we scale the x-axis corresponding to the interval and the y-axis corresponding to the maximum and minimum values of the function within the interval. The smoothness of the curve depends on properties of the function and the size of the sample. If the sample size is too small, the rendition of the function may not be at all accurate (it might not be very smooth, and it might miss major fluctuations); if the sample is too large, producing the plot may be time-consuming, since some functions are time-consuming to compute. (In SECTION 2.4, we will look at a method for plotting a smooth curve without using an excessive number of points.) You can use this same technique to plot the function graph of any function you choose. That is, you can decide on an x-interval where you want to plot the function, compute function values evenly spaced within that interval, determine and set the y-scale, and draw the line segments.

A program for plotting a function graph and the associated output are displayed.
Outline and filled shapes

StdDraw also includes methods to draw circles, squares, rectangles, and arbitrary polygons. Each shape defines an outline. When the method name is the name of a shape, that outline is traced by the drawing pen. When the name begins with filled, the named shape is filled solid, not traced. As usual, we summarize the available methods in an API:

public class StdDraw (shapes)

void circle(double x, double y, double radius)

void filledCircle(double x, double y, double radius)

void square(double x, double y, double r)

void filledSquare(double x, double y, double r)

void rectangle(double x, double y, double r1, double r2)

void filledRectangle(double x, double y, double r1, double r2)

void polygon(double[] x, double[] y)

void filledPolygon(double[] x, double[] y)

The arguments for circle() and filledCircle() define a circle of radius r centered at (x, y); the arguments for square() and filledSquare() define a square of side length 2r centered at (x, y); the arguments for rectangle() and filledRectangle() define a rectangle of width 2r1 and height 2r2, centered at (x, y); and the arguments for polygon() and filledPolygon() define a sequence of points that are connected by line segments, including one from the last point to the first point.

The output for the three functions is shown in three sections.
Text and color

Occasionally, you may wish to annotate or highlight various elements in your drawings. StdDraw has a method for drawing text, another for setting parameters associated with text, and another for changing the color of the ink in the pen. We make scant use of these features in this book, but they can be very useful, particularly for drawings on your computer screen. You will find many examples of their use on the booksite.

public class StdDraw (text and color commands)

void text(double x, double y, String s)

void setFont(Font font)

void setPenColor(Color color)

In this code, Font and Color are nonprimitive types that you will learn about in SECTION 3.1. Until then, we leave the details to StdDraw. The available pen colors are BLACK, BLUE, CYAN, DARK_GRAY, GRAY, GREEN, LIGHT_GRAY, MAGENTA, ORANGE, PINK, RED, WHITE, YELLOW, and BOOK_BLUE, all of which are defined as constants within StdDraw. For example, the call StdDraw.setPenColor(StdDraw.GRAY) changes the pen to use gray ink. The default ink color is BLACK. The default font in StdDraw suffices for most of the drawings that you need (you can find information on using other fonts on the booksite). For example, you might wish to use these methods to annotate function graphs to highlight relevant values, and you might find it useful to develop similar methods to annotate other parts of your drawings.

A program shows the usage of the function StdDraw and the associated output.

Shapes, color, and text are basic tools that you can use to produce a dizzying variety of images, but you should use them sparingly. Use of such artifacts usually presents a design challenge, and our StdDraw commands are crude by the standards of modern graphics libraries, so that you are likely to need an extensive number of calls to them to produce the beautiful images that you may imagine. By comparison, using color or labels to help focus on important information in drawings is often worthwhile, as is using color to represent data values.

Double buffering and computer animations.

StdDraw supports a powerful computer graphics feature known as double buffering. When double buffering is enabled by calling enableDoubleBuffering(), all drawing takes place on the offscreen canvas. The offscreen canvas is not displayed; it exists only in computer memory. Only when you call show() does your drawing get copied from the offscreen canvas to the onscreen canvas, where it is displayed in the standard drawing window. You can think of double buffering as collecting all of the lines, points, shapes, and text that you tell it to draw, and then drawing them all simultaneously, upon request. Double buffering enables you to precisely control when the drawing takes place.

One reason to use double buffering is for efficiency when performing a large number of drawing commands. Incrementally displaying a complex drawing while it is being created can be intolerably inefficient on many computer systems. For example, you can dramatically speed up PROGRAM 1.5.5 by adding a call to enableDoubleBuffering() before the while loop and a call to show() after the while loop. Now, the points appear all at once (instead of one at a time).

Our most important use of double buffering is to produce computer animations, where we create the illusion of motion by rapidly displaying static drawings. Such effects can provide compelling and dynamic visualizations of scientific phenomenon. We can produce animations by repeating the following four steps:

• Clear the offscreen canvas.

• Draw objects on the offscreen canvas.

• Copy the offscreen canvas to the onscreen canvas.

• Wait for a short while.

In support of the first and last of these steps, StdDraw provides three additional methods. The clear() methods clear the canvas, either to white or to a specified color. To control the apparent speed of an animation, the pause() method takes an argument dt and tells StdDraw to wait for dt milliseconds before processing additional commands.

public class StdDraw (advanced control commands)

void enableDoubleBuffering()

enable double buffering

void disableDoubleBuffering()

disable double buffering

void show()

copy the offscreen canvas to the onscreen canvas

void clear()

clear the canvas to white (default)

void clear(Color color)

clear the canvas to color color

void pause(double dt)

pause dt milliseconds

Bouncing ball

The “Hello, World” program for animation is to produce a black ball that appears to move around on the canvas, bouncing off the boundary according to the laws of elastic collision. Suppose that the ball is at position (rx, ry) and we want to create the impression of moving it to a nearby position, say, (rx + 0.01, ry + 0.02). We do so in four steps:

• Clear the offscreen canvas to white.

Draw a black ball at the new position on the offscreen canvas.

• Copy the offscreen canvas to the onscreen canvas.

• Wait for a short while.

To create the illusion of movement, we iterate these steps for a whole sequence of positions of the ball (one that will form a straight line, in this case). Without double buffering, the image of the ball will rapidly flicker between black and white instead of creating a smooth animation.

BouncingBall (PROGRAM 1.5.6) implements these steps to create the illusion of a ball moving in the 2-by-2 box centered at the origin. The current position of the ball is (rx, ry), and we compute the new position at each step by adding vx to rx and vy to ry. Since (vx, vy) is the fixed distance that the ball moves in each time unit, it represents the velocity. To keep the ball in the standard drawing window, we simulate the effect of the ball bouncing off the walls according to the laws of elastic collision. This effect is easy to implement: when the ball hits a vertical wall, we change the velocity in the x-direction from vx to –vx, and when the ball hits a horizontal wall, we change the velocity in the y-direction from vy to –vy. Of course, you have to download the code from the booksite and run it on your computer to see motion. To make the image clearer on the printed page, we modified BouncingBall to use a gray background that also shows the track of the ball as it moves (see EXERCISE 1.5.34).

Standard drawing completes our programming model by adding a “picture is worth a thousand words” component. It is a natural abstraction that you can use to better open up your programs to the outside world. With it, you can easily produce the function graphs and visual representations of data that are commonly used in science and engineering. We will put it to such uses frequently throughout this book. Any time that you spend now working with the sample programs on the last few pages will be well worth the investment. You can find many useful examples on the booksite and in the exercises, and you are certain to find some outlet for your creativity by using StdDraw to meet various challenges. Can you draw an n-pointed star? Can you make our bouncing ball actually bounce (by adding gravity)? You may be surprised at how easily you can accomplish these and other tasks.


Program 1.5.6 Bouncing ball

public class BouncingBall
{
   public static void main(String[] args)
   {  // Simulate the motion of a bouncing ball.
      StdDraw.setXscale(-1.0, 1.0);
      StdDraw.setYscale(-1.0, 1.0);
StdDraw.enableDoubleBuffering();
      double rx = 0.480, ry = 0.860;
      double vx = 0.015, vy = 0.023;
      double radius = 0.05;
      while(true)
      {  // Update ball position and draw it.
         if (Math.abs(rx + vx) + radius > 1.0) vx = -vx;
         if (Math.abs(ry + vy) + radius > 1.0) vy = -vy;
         rx += vx;
         ry += vy;
         StdDraw.clear();
         StdDraw.filledCircle(rx, ry, radius);
         StdDraw.show();
         StdDraw.pause(20);
      }
   }
}

rx, ry | position
vx, vy | velocity
  dt   | wait time
radius | ball radius

This program simulates the motion of a bouncing ball in the box with coordinates between –1 and +1. The ball bounces off the boundary according to the laws of inelastic collision. The 20-millisecond wait for StdDraw.pause() keeps the black image of the ball persistent on the screen, even though most of the ball’s pixels alternate between black and white. The images below, which show the track of the ball, are produced by a modified version of this code (see EXERCISE 1.5.34).


An illustration with three figures shows the path of motion of a bouncing ball in a box.

This API table summarizes the StdDraw methods that we have considered:

public class StdDraw

drawing commands

void line(double x0, double y0, double x1, double y1)

void point(double x, double y)

void circle(double x, double y, double radius)

void filledCircle(double x, double y, double radius)

void square(double x, double y, double radius)

void filledSquare(double x, double y, double radius)

void rectangle(double x, double y, double r1, double r2)

void filledRectangle(double x, double y, double r1, double r2)

void polygon(double[] x, double[] y)

void filledPolygon(double[] x, double[] y)

void text(double x, double y, String s)

control commands

void setXscale(double x0, double x1)

reset x-scale to (x0, x1)

void setYscale(double y0, double y1)

reset y-scale to (y0, y1)

void setPenRadius(double radius)

set pen radius to radius

void setPenColor(Color color)

set pen color to color

void setFont(Font font)

set text font to font

void setCanvasSize(int w, int h)

set canvas size to w-by-h

void enableDoubleBuffering()

enable double buffering

void disableDoubleBuffering()

disable double buffering

void show()

copy the offscreen canvas to the onscreen canvas

void clear(Color color)

clear the canvas to color color

void pause(int dt)

pause dt milliseconds

void save(String filename)

save to a .jpg or .png file

Note: Methods with the same names but no arguments reset to default values.

API for our library of static methods for standard drawing

Standard audio

As a final example of a basic abstraction for output, we consider StdAudio, a library that you can use to play, manipulate, and synthesize sound. You probably have used your computer to process music. Now you can write programs to do so. At the same time, you will learn some concepts behind a venerable and important area of computer science and scientific computing: digital signal processing. We will merely scratch the surface of this fascinating subject, but you may be surprised at the simplicity of the underlying concepts.

Concert A

Sound is the perception of the vibration of molecules—in particular, the vibration of our eardrums. Therefore, oscillation is the key to understanding sound. Perhaps the simplest place to start is to consider the musical note A above middle C, which is known as concert A. This note is nothing more than a sine wave, scaled to oscillate at a frequency of 440 times per second. The function sin(t) repeats itself once every 2π units, so if we measure t in seconds and plot the function sin(2πt × 440), we get a curve that oscillates 440 times per second. When you play an A by plucking a guitar string, pushing air through a trumpet, or causing a small cone to vibrate in a speaker, this sine wave is the prominent part of the sound that you hear and recognize as concert A. We measure frequency in hertz (cycles per second). When you double or halve the frequency, you move up or down one octave on the scale. For example, 880 hertz is one octave above concert A and 110 hertz is two octaves below concert A. For reference, the frequency range of human hearing is about 20 to 20,000 hertz. The amplitude (y-value) of a sound corresponds to the volume. We plot our curves between –1 and +1 and assume that any devices that record and play sound will scale as appropriate, with further scaling controlled by you when you turn the volume knob.

An illustration with three sections shows the Notes, numbers, and waves.
Other notes

A simple mathematical formula characterizes the other notes on the chromatic scale. There are 12 notes on the chromatic scale, evenly spaced on a logarithmic (base 2) scale. We get the i th note above a given note by multiplying its frequency by the (i/12)th power of 2. In other words, the frequency of each note in the chromatic scale is precisely the frequency of the previous note in the scale multiplied by the twelfth root of 2 (about 1.06). This information suffices to create music! For example, to play the tune Frère Jacques, play each of the notes A B C# A by producing sine waves of the appropriate frequency for about half a second each, and then repeat the pattern. The primary method in the StdAudio library, StdAudio.play(), allows you to do exactly this.

Sampling

For digital sound, we represent a curve by sampling it at regular intervals, in precisely the same manner as when we plot function graphs. We sample sufficiently often that we have an accurate representation of the curve—a widely used sampling rate for digital sound is 44,100 samples per second. For concert A, that rate corresponds to plotting each cycle of the sine wave by sampling it at about 100 points. Since we sample at regular intervals, we only need to compute the y-coordinates of the sample points. It is that simple: we represent sound as an array of real numbers (between –1 and +1). The method StdAudio.play() takes an array as its argument and plays the sound represented by that array on your computer.

For example, suppose that you want to play concert A for 10 seconds. At 44,100 samples per second, you need a double array of length 441,001. To fill in the array, use a for loop that samples the function sin(2πt × 440) at t = 0/44,100, 1/44,100, 2/44,100, 3/44,100, ..., 441,000/44,100. Once we fill the array with these values, we are ready for StdAudio.play(), as in the following code:

int SAMPLING_RATE = 44100;       // samples per second
int hz = 440;                    // concert A
double duration = 10.0;          // ten seconds
int n = (int) (SAMPLING_RATE * duration);
double[] a = new double[n+1];
for (int i = 0; i <= n; i++)
   a[i] = Math.sin(2 * Math.PI * i * hz / SAMPLING_RATE);
StdAudio.play(a);

This code is the “Hello, World” of digital audio. Once you use it to get your computer to play this note, you can write code to play other notes and make music! The difference between creating sound and plotting an oscillating curve is nothing more than the output device. Indeed, it is instructive and entertaining to send the same numbers to both standard drawing and standard audio (see EXERCISE 1.5.27).

Saving to a file

Music can take up a lot of space on your computer. At 44,100 samples per second, a four-minute song corresponds to 4 × 60 × 44100 = 10,584,000 numbers. Therefore, it is common to represent the numbers corresponding to a song in a binary format that uses less space than the string-of-digits representation that we use for standard input and output. Many such formats have been developed in recent years—StdAudio uses the .wav format. You can find some information about the .wav format on the booksite, but you do not need to know the details, because StdAudio takes care of the conversions for you. Our standard library for audio allows you to read .wav files, write .wav files, and convert .wav files to arrays of double values for processing.

An illustration with two sections shows the different ways of sampling a sine wave.

PlayThatTune (PROGRAM 1.5.7) is an example that shows how you can use StdAudio to turn your computer into a musical instrument. It takes notes from standard input, indexed on the chromatic scale from concert A, and plays them on standard audio. You can imagine all sorts of extensions on this basic scheme, some of which are addressed in the exercises.

We include standard audio in our basic arsenal of programming tools because sound processing is one important application of scientific computing that is certainly familiar to you. Not only has the commercial application of digital signal processing had a phenomenal impact on modern society, but the science and engineering behind it combine physics and computer science in interesting ways. We will study more components of digital signal processing in some detail later in the book. (For example, you will learn in SECTION 2.1 how to create sounds that are more musical than the pure sounds produced by PlayThatTune.)


Program 1.5.7 Digital signal processing

public class PlayThatTune
{
   public static void main(String[] args)
   {  // Read a tune from StdIn and play it.
      int SAMPLING_RATE = 44100;
      while (!StdIn.isEmpty())
      {  // Read and play one note.
         int pitch = StdIn.readInt();
         double duration = StdIn.readDouble();
         double hz = 440 * Math.pow(2, pitch / 12.0);
         int n = (int) (SAMPLING_RATE * duration);
         double[] a = new double[n+1];
         for (int i = 0; i <= n; i++)
            a[i] = Math.sin(2*Math.PI * i * hz / SAMPLING_RATE);
         StdAudio.play(a);
      }
   }
}

 pitch   | distance from A
duration | note play time
   hz    | frequency
   n     | number of samples
  a[]    | sampled sine wave

This data-driven program turns your computer into a musical instrument. It reads notes and durations from standard input and plays a pure tone corresponding to each note for the specified duration on standard audio. Each note is specified as a pitch (distance from concert A). After reading each note and duration, the program creates an array by sampling a sine wave of the specified frequency and duration at 44,100 samples per second, and plays it using StdAudio.play().


% more elise.txt
7 0.25
6 0.25
7 0.25
6 0.25
7 0.25
2 0.25
5 0.25
3 0.25
0 0.50

An illustration shows a musical note with the statement that reads “% java PlayThatTune < elise.txt.”

The API table below summarizes the methods in StdAudio:

public class StdAudio

void

play(String filename)

play the given .wav file

void

play(double[] a)

play the given sound wave

void

play(double x)

play sample for 1/44,100 second

void

save(String filename, double[] a)

save to a .wav file

double[]

read(String filename)

read from a .wav file

API for our library of static methods for standard audio

Summary

I/O is a compelling example of the power of abstraction because standard input, standard output, standard drawing, and standard audio can be tied to different physical devices at different times without making any changes to programs. Although devices may differ dramatically, we can write programs that can do I/O without depending on the properties of specific devices. From this point forward, we will use methods from StdOut, StdIn, StdDraw, and/or StdAudio in nearly every program in this book. For economy, we collectively refer to these libraries as Std*. One important advantage of using such libraries is that you can switch to new devices that are faster, are cheaper, or hold more data without changing your program at all. In such a situation, the details of the connection are a matter to be resolved between your operating system and the Std* implementations. On modern systems, new devices are typically supplied with software that resolves such details automatically both for the operating system and for Java.

Q&A

Q. How can I make StdIn, StdOut, StdDraw, and StdAudio available to Java?

A. If you followed the step-by-step instructions on the booksite for installing Java, these libraries should already be available to Java. Alternatively, you can copy the files StdIn.java, StdOut.java, StdDraw.java, and StdAudio.java from the booksite and put them in the same directory as the programs that use them.

Q. What does the error message Exception in thread "main" java.lang.NoClassDefFoundError: StdIn mean?

A. The library StdIn is not available to Java.

Q. Why are we not using the standard Java libraries for input, graphics, and sound?

A. We are using them, but we prefer to work with simpler abstract models. The Java libraries behind StdIn, StdDraw, and StdAudio are built for production programming, and the libraries and their APIs are a bit unwieldy. To get an idea of what they are like, look at the code in StdIn.java, StdDraw.java, and StdAudio.java.

Q. So, let me get this straight. If I use the format %2.4f for a double value, I get two digits before the decimal point and four digits after, right?

A. No, that specifies 4 digits after the decimal point. The first value is the width of the whole field. You want to use the format %7.2f to specify 7 characters in total, 4 before the decimal point, the decimal point itself, and 2 digits after the decimal point.

Q. Which other conversion codes are there for printf()?

A. For integer values, there is o for octal and x for hexadecimal. There are also numerous formats for dates and times. See the booksite for more information.

Q. Can my program reread data from standard input?

A. No. You get only one shot at it, in the same way that you cannot undo a println() command.

Q. What happens if my program attempts to read data from standard input after it is exhausted?

A. You will get an error. StdIn.isEmpty() allows you to avoid such an error by checking whether there is more input available.

Q. Why does StdDraw.square(x, y, r) draw a square of width 2*r instead of r?

A. This makes it consistent with the function StdDraw.circle(x, y, r), in which the third argument is the radius of the circle, not the diameter. In this context, r is the radius of the biggest circle that can fit inside the square.

Q. My terminal window hangs at the end of a program using StdAudio. How can I avoid having to use <Ctrl-C> to get a command prompt?

A. Add a call to System.exit(0) as the last line in main(). Don’t ask why.

Q. Can I use negative integers to specify notes below concert A when making input files for PlayThatTune?

A. Yes. Actually, our choice to put concert A at 0 is arbitrary. A popular standard, known as the MIDI Tuning Standard, starts numbering at the C five octaves below concert A. By that convention, concert A is 69 and you do not need to use negative numbers.

Q. Why do I hear weird results on standard audio when I try to sonify a sine wave with a frequency of 30,000 hertz (or more)?

A. The Nyquist frequency, defined as one-half the sampling frequency, represents the highest frequency that can be reproduced. For standard audio, the sampling frequency is 44,100 hertz, so the Nyquist frequency is 22,050 hertz.

Exercises

1.5.1 Write a program that reads in integers (as many as the user enters) from standard input and prints the maximum and minimum values.

1.5.2 Modify your program from the previous exercise to insist that the integers must be positive (by prompting the user to enter positive integers whenever the value entered is not positive).

1.5.3 Write a program that takes an integer command-line argument n, reads n floating-point numbers from standard input, and prints their mean (average value) and sample standard deviation (square root of the sum of the squares of their differences from the average, divided by n-1).

1.5.4 Extend your program from the previous exercise to create a filter that reads n floating-point numbers from standard input, and prints those that are further than 1.5 standard deviations from the mean.

1.5.5 Write a program that reads in a sequence of integers and prints both the integer that appears in a longest consecutive run and the length of that run. For example, if the input is 1 2 2 1 5 1 1 7 7 7 7 1 1, then your program should print Longest run: 4 consecutive 7s.

1.5.6 Write a filter that reads in a sequence of integers and prints the integers, removing repeated values that appear consecutively. For example, if the input is 1 2 2 1 5 1 1 7 7 7 7 1 1 1 1 1 1 1 1 1, your program should print 1 2 1 5 1 7 1.

1.5.7 Write a program that takes an integer command-line argument n, reads in n-1 distinct integers between 1 and n, and determines the missing value.

1.5.8 Write a program that reads in positive floating-point numbers from standard input and prints their geometric and harmonic means. The geometric mean of n positive numbers x1, x2, ..., xn is (x1 × x2 × ... × xn)1/n. The harmonic mean is n / (1/x1 + 1/x2 + ... + 1/xn). Hint: For the geometric mean, consider taking logarithms to avoid overflow.

1.5.9 Suppose that the file input.txt contains the two strings F and F. What does the following command do (see EXERCISE 1.2.35)?

% java Dragon < input.txt | java Dragon | java Dragon

public class Dragon
{
   public static void main(String[] args)
   {
      String dragon = StdIn.readString();
      String nogard = StdIn.readString();
      StdOut.print(dragon + "L" + nogard);
      StdOut.print(" ");
      StdOut.print(dragon + "R" + nogard);
      StdOut.println();
   }
}

1.5.10 Write a filter TenPerLine that reads from standard input a sequence of integers between 0 and 99 and prints them back, 10 integers per line, with columns aligned. Then write a program RandomIntSeq that takes two integer command-line arguments m and n and prints n random integers between 0 and m-1. Test your programs with the command java RandomIntSeq 200 100 | java TenPerLine.

1.5.11 Write a program that reads in text from standard input and prints the number of words in the text. For the purpose of this exercise, a word is a sequence of non-whitespace characters that is surrounded by whitespace.

1.5.12 Write a program that reads in lines from standard input with each line containing a name and two integers and then uses printf() to print a table with a column of the names, the integers, and the result of dividing the first by the second, accurate to three decimal places. You could use a program like this to tabulate batting averages for baseball players or grades for students.

1.5.13 Write a program that prints a table of the monthly payments, remaining principal, and interest paid for a loan, taking three numbers as command-line arguments: the number of years, the principal, and the interest rate (see EXERCISE 1.2.24).

1.5.14 Which of the following require saving all the values from standard input (in an array, say), and which could be implemented as a filter using only a fixed number of variables? For each, the input comes from standard input and consists of n real numbers between 0 and 1.

• Print the maximum and minimum numbers.

• Print the sum of the squares of the n numbers.

• Print the average of the n numbers.

• Print the median of the n numbers.

• Print the percentage of numbers greater than the average.

• Print the n numbers in increasing order.

• Print the n numbers in random order.

1.5.15 Write a program that takes three double command-line arguments x, y, and z, reads from standard input a sequence of point coordinates (xi, yi, zi), and prints the coordinates of the point closest to (x, y, z). Recall that the square of the distance between (x, y, z) and (xi, yi, zi) is (xxi)2 + (yyi)2 + (zzi)2. For efficiency, do not use Math.sqrt().

1.5.16 Given the positions and masses of a sequence of objects, write a program to compute their center-of-mass, or centroid. The centroid is the average position of the n objects, weighted by mass. If the positions and masses are given by (xi, yi, mi), then the centroid (x, y, m) is given by

m = m1 + m2 + ... + mn
x
= (m1 x1 + ... + mn xn) / m
y
= (m1 y1 + ... + mn yn) / m

1.5.17 Write a program that reads in a sequence of real numbers between –1 and +1 and prints their average magnitude, average power, and the number of zero crossings. The average magnitude is the average of the absolute values of the data values. The average power is the average of the squares of the data values. The number of zero crossings is the number of times a data value transitions from a strictly negative number to a strictly positive number, or vice versa. These three statistics are widely used to analyze digital signals.

1.5.18 Write a program that takes an integer command-line argument n and plots an n-by-n checkerboard with red and black squares. Color the lower-left square red.

1.5.19 Write a program that takes as command-line arguments an integer n and a floating-point number p (between 0 and 1), plots n equally spaced points on the circumference of a circle, and then, with probability p for each pair of points, draws a gray line connecting them.

The output to be obtained from a program is shown in four sections.

1.5.20 Write code to draw hearts, spades, clubs, and diamonds. To draw a heart, draw a filled diamond, then attach two filled semicircles to the upper left and upper right sides.

1.5.21 Write a program that takes an integer command-line argument n and plots a rose with n petals (if n is odd) or 2n petals (if n is even), by plotting the polar coordinates (r, θ) of the function r = sin(n θ) for θ ranging from 0 to 2π radians.

An illustration with four sections shows flowers with a varying number of petals.

1.5.22 Write a program that takes a string command-line argument s and displays it in banner style on the screen, moving from left to right and wrapping back to the beginning of the string as the end is reached. Add a second command-line argument to control the speed.

1.5.23 Modify PlayThatTune to take additional command-line arguments that control the volume (multiply each sample value by the volume) and the tempo (multiply each note’s duration by the tempo).

1.5.24 Write a program that takes the name of a .wav file and a playback rate r as command-line arguments and plays the file at the given rate. First, use StdAudio.read() to read the file into an array a[]. If r = 1, play a[]; otherwise, create a new array b[] of approximate size r times the length of a[]. If r < 1, populate b[] by sampling from the original; if r > 1, populate b[] by interpolating from the original. Then play b[].

1.5.25 Write programs that uses StdDraw to create each of the following designs.

An illustration with four sections shows four different types of designs.

1.5.26 Write a program Circles that draws filled circles of random radii at random positions in the unit square, producing images like those below. Your program should take four command-line arguments: the number of circles, the probability that each circle is black, the minimum radius, and the maximum radius.

An illustration with four sections shows squares filled with circles.

Creative Exercises

1.5.27 Visualizing audio. Modify PlayThatTune to send the values played to standard drawing, so that you can watch the sound waves as they are played. You will have to experiment with plotting multiple curves in the drawing canvas to synchronize the sound and the picture.

1.5.28 Statistical polling. When collecting statistical data for certain political polls, it is very important to obtain an unbiased sample of registered voters. Assume that you have a file with n registered voters, one per line. Write a filter that prints a uniformly random sample of size m (see PROGRAM 1.4.1).

1.5.29 Terrain analysis. Suppose that a terrain is represented by a two-dimensional grid of elevation values (in meters). A peak is a grid point whose four neighboring cells (left, right, up, and down) have strictly lower elevation values. Write a program Peaks that reads a terrain from standard input and then computes and prints the number of peaks in the terrain.

1.5.30 Histogram. Suppose that the standard input stream is a sequence of double values. Write a program that takes an integer n and two real numbers lo and hi as command-line arguments and uses StdDraw to plot a histogram of the count of the numbers in the standard input stream that fall in each of the n intervals defined by dividing (lo, hi) into n equal-sized intervals.

1.5.31 Spirographs. Write a program that takes three double command-line arguments R, r, and a and draws the resulting spirograph. A spirograph (technically, an epicycloid) is a curve formed by rolling a circle of radius r around a larger fixed circle of radius R. If the pen offset from the center of the rolling circle is (r+a), then the equation of the resulting curve at time t is given by

x(t) = (R + r) cos (t) – (r + a) cos ((R + r)t /r)
y(t) = (R + r) sin (t) – (r + a) sin ((R + r)t /r)

Such curves were popularized by a best-selling toy that contains discs with gear teeth on the edges and small holes that you could put a pen in to trace spirographs.

1.5.32 Clock. Write a program that displays an animation of the second, minute, and hour hands of an analog clock. Use the method StdDraw.pause(1000) to update the display roughly once per second.

1.5.33 Oscilloscope. Write a program that simulates the output of an oscilloscope and produces Lissajous patterns. These patterns are named after the French physicist, Jules A. Lissajous, who studied the patterns that arise when two mutually perpendicular periodic disturbances occur simultaneously. Assume that the inputs are sinusoidal, so that the following parametric equations describe the curve:

x(t) = Ax sin (wx t + θx)
y(t) = A y sin (wy t + θy)

Take the six arguments Ax,, wx,, θx, Ay,, wy, and θy from the command line.

1.5.34 Bouncing ball with tracks. Modify BouncingBall to produce images like the ones shown in the text, which show the track of the ball on a gray background.

1.5.35 Bouncing ball with gravity. Modify BouncingBall to incorporate gravity in the vertical direction. Add calls to StdAudio.play() to add a sound effect when the ball hits a wall and a different sound effect when it hits the floor.

1.5.36 Random tunes. Write a program that uses StdAudio to play random tunes. Experiment with keeping in key, assigning high probabilities to whole steps, repetition, and other rules to produce reasonable melodies.

1.5.37 Tile patterns. Using your solution to EXERCISE 1.5.25, write a program TilePattern that takes an integer command-line argument n and draws an n-by-n pattern, using the tile of your choice. Add a second command-line argument that adds a checkerboard option. Add a third command-line argument for color selection. Using the patterns on the facing page as a starting point, design a tile floor. Be creative! Note: These are all designs from antiquity that you can find in many ancient (and modern) buildings.

An illustration shows eight tile patterns each of which is represented in a checkerboard.

1.6 Case Study: Random Web Surfer

Communicating across the web has become an integral part of everyday life. This communication is enabled in part by scientific studies of the structure of the web, a subject of active research since its inception. We next consider a simple model of the web that has proved to be a particularly successful approach to understanding some of its properties. Variants of this model are widely used and have been a key factor in the explosive growth of search applications on the web.

The model is known as the random surfer model, and is simple to describe. We consider the web to be a fixed set of web pages, with each page containing a fixed set of hyperlinks, and each link a reference to some other page. (For brevity, we use the terms pages and links.) We study what happens to a web surfer who randomly moves from page to page, either by typing a page name into the address bar or by clicking a link on the current page.

The mathematical model that underlies the link structure of the web is known as the graph, which we will consider in detail at the end of the book (in SECTION 4.5). We defer discussion about processing graphs until then. Instead, we concentrate on calculations associated with a natural and well-studied probabilistic model that accurately describes the behavior of the random surfer.

An illustration depicts a set of pages and links between them. .

The first step in studying the random surfer model is to formulate it more precisely. The crux of the matter is to specify what it means to randomly move from page to page. The following intuitive 9010 rule captures both methods of moving to a new page: Assume that 90% of the time the random surfer clicks a random link on the current page (each link chosen with equal probability) and that 10% of the time the random surfer goes directly to a random page (all pages on the web chosen with equal probability).

You can immediately see that this model has flaws, because you know from your own experience that the behavior of a real web surfer is not quite so simple:

• No one chooses links or pages with equal probability.

• There is no real potential to surf directly to each page on the web.

• The 90–10 (or any fixed) breakdown is just a guess.

• It does not take the back button or bookmarks into account.

Despite these flaws, the model is sufficiently rich that computer scientists have learned a great deal about properties of the web by studying it. To appreciate the model, consider the small example on the previous page. Which page do you think the random surfer is most likely to visit?

Each person using the web behaves a bit like the random surfer, so understanding the fate of the random surfer is of intense interest to people building web infrastructure and web applications. The model is a tool for understanding the experience of each of the billions of web users. In this section, you will use the basic programming tools from this chapter to study the model and its implications.

Input format

We want to be able to study the behavior of the random surfer on various graphs, not just one example. Consequently, we want to write data-driven code, where we keep data in files and write programs that read the data from standard input. The first step in this approach is to define an input format that we can use to structure the information in the input files. We are free to define any convenient input format.

An illustration with two sections shows the Random surfer input format.

Later in the book, you will learn how to read web pages in Java programs (SECTION 3.1) and to convert from names to numbers (SECTION 4.4) as well as other techniques for efficient graph processing. For now, we assume that there are n web pages, numbered from 0 to n-1, and we represent links with ordered pairs of such numbers, the first specifying the page containing the link and the second specifying the page to which it refers. Given these conventions, a straightforward input format for the random surfer problem is an input stream consisting of an integer (the value of n) followed by a sequence of pairs of integers (the representations of all the links). StdIn treats all sequences of whitespace characters as a single delimiter, so we are free to either put one link per line or arrange them several to a line.

Transition matrix

We use a two-dimensional matrix, which we refer to as the transition matrix, to completely specify the behavior of the random surfer. With n web pages, we define an n-by-n matrix such that the value in row i and column j is the probability that the random surfer moves to page j when on page i. Our first task is to write code that can create such a matrix for any given input. By the 90–10 rule, this computation is not difficult. We do so in three steps:

• Read n, and then create arrays counts[][] and outDegrees[].

• Read the links and accumulate counts so that counts[i][j] counts the links from i to j and outDegrees[i] counts the links from i to anywhere.

• Use the 90–10 rule to compute the probabilities.

The first two steps are elementary, and the third is not much more difficult: multiply counts[i][j] by 0.90/outDegree[i] if there is a link from i to j (take a random link with probability 0.9), and then add 0.10/n to each element (go to a random page with probability 0.1). Transition (PROGRAM 1.6.1) performs this calculation: it is a filter that reads a graph from standard input and prints the associated transition matrix to standard output.

The transition matrix is significant because each row represents a discrete probability distribution—the elements fully specify the behavior of the random surfer’s next move, giving the probability of surfing to each page. Note in particular that the elements sum to 1 (the surfer always goes somewhere).

The output of Transition defines another file format, one for matrices: the numbers of rows and columns followed by the values of the matrix elements, in row-major order. Now, we can write programs that read and process transition matrices.

The computation of Transition matrix is shown.

Program 1.6.1 Computing the transition matrix

public class Transition
{
   public static void main(String[] args)
   {
      int n = StdIn.readInt();
      int[][] counts = new int[n][n];
      int[] outDegrees = new int[n];
      while (!StdIn.isEmpty())
      {  // Accumulate link counts.
         int i = StdIn.readInt();
         int j = StdIn.readInt();
         outDegrees[i]++;
         counts[i][j]++;
      }

      StdOut.println(n + " " + n);
      for (int i = 0; i < n; i++)
      {  // Print probability distribution for row i.
         for (int j = 0; j < n; j++)
         {  // Print probability for row i and column j.
            double p = 0.9*counts[i][j]/outDegrees[i] + 0.1/n;
            StdOut.printf("%8.5f", p);
         }
         StdOut.println();
      }
   }
}

      n       | number of pages
 counts[i][j] | count of links from page i to page j
outDegrees[i] | count of links from page i to anywhere
      p       | transition probability

This program is a filter that reads links from standard input and produces the corresponding transition matrix on standard output. First it processes the input to count the outlinks from each page. Then it applies the 90–10 rule to compute the transition matrix (see text). It assumes that there are no pages that have no outlinks in the input (see EXERCISE 1.6.3).


% more tinyG.txt
5
0 1
1 2  1 2
1 3  1 3  1 4
2 3
3 0
4 0  4 2

% java Transition < tinyG.txt
5 5
 0.02000 0.92000 0.02000 0.02000 0.02000
 0.02000 0.02000 0.38000 0.38000 0.20000
 0.02000 0.02000 0.02000 0.92000 0.02000
 0.92000 0.02000 0.02000 0.02000 0.02000
 0.47000 0.02000 0.47000 0.02000 0.02000

Simulation

Given the transition matrix, simulating the behavior of the random surfer involves surprisingly little code, as you can see in RandomSurfer (PROGRAM 1.6.2). This program reads a transition matrix from standard input and surfs according to the rules, starting at page 0 and taking the number of moves as a command-line argument. It counts the number of times that the surfer visits each page. Dividing that count by the number of moves yields an estimate of the probability that a random surfer winds up on the page. This probability is known as the page’s rank. In other words, RandomSurfer computes an estimate of all page ranks.

One random move

The key to the computation is the random move, which is specified by the transition matrix. We maintain a variable page whose value is the current location of the surfer. Row page of the matrix gives, for each j, the probability that the surfer next goes to j. In other words, when the surfer is at page, our task is to generate a random integer between 0 and n-1 according to the distribution given by row page in the transition matrix. How can we accomplish this task? We use a technique known as roulette-wheel selection. We use Math.random() to generate a random number r between 0 and 1, but how does that help us get to a random page? One way to answer this question is to think of the probabilities in row page as defining a set of n intervals in (0, 1), with each probability corresponding to an interval length. Then our random variable r falls into one of the intervals, with probability precisely specified by the interval length. This reasoning leads to the following code:

double sum = 0.0;
for (int j = 0; j < n; j++)
{  // Find interval containing r.
   sum += p[page][j];
   if (r < sum) { page = j; break; }
}
An illustration with two sections shows generating a random integer from a discrete distribution.

The variable sum tracks the endpoints of the intervals defined in row page, and the for loop finds the interval containing the random value r. For example, suppose that the surfer is at page 4 in our example. The transition probabilities are 0.47, 0.02, 0.47, 0.02, and 0.02, and sum takes on the values 0.0, 0.47, 0.49, 0.96, 0.98, and 1.0. These values indicate that the probabilities define the five intervals (0, 0.47), (0.47, 0.49), (0.49, 0.96), (0.96, 0.98), and (0.98, 1), one for each page. Now, suppose that Math.random() returns the value 0.71. We increment j from 0 to 1 to 2 and stop there, which indicates that 0.71 is in the interval (0.49, 0.96), so we send the surfer to page 2. Then, we perform the same computation start at page 2, and the random surfer is off and surfing. For large n, we can use binary search to substantially speed up this computation (see EXERCISE 4.2.38). Typically, we are interested in speeding up the search in this situation because we are likely to need a huge number of random moves, as you will see.


Program 1.6.2 Simulating a random surfer

public class RandomSurfer
{
   public static void main(String[] args)
   {  // Simulate random surfer.
      int trials = Integer.parseInt(args[0]);
      int n = StdIn.readInt();
      StdIn.readInt();

      // Read transition matrix.
      double[][] p = new double[n][n];
      for (int i = 0; i < n; i++)
         for (int j = 0; j < n; j++)
            p[i][j] = StdIn.readDouble();

      int page = 0;
      int[] freq = new int[n];
      for (int t = 0; t < trials; t++)
      {  // Make one random move to next page.
         double r = Math.random();
         double sum = 0.0;
         for (int j = 0; j < n; j++)
         {  // Find interval containing r.
            sum += p[page][j];
            if (r < sum) { page = j; break; }
         }
         freq[page]++;
      }

      for (int i = 0; i < n; i++)     // Print page ranks.
         StdOut.printf("%8.5f", (double) freq[i] / trials);
      StdOut.println();
    }
}

 trials | number of moves
   n    | number of pages
  page  | current page
p[i][j] | probability that the surfer moves from page i to page j
freq[i] | number of times the surfer hits page i

This program uses a transition matrix to simulate the behavior of a random surfer. It takes the number of moves as a command-line argument, reads the transition matrix, performs the indicated number of moves as prescribed by the matrix, and prints the relative frequency of hitting each page. The key to the computation is the random move to the next page (see text).


% java Transition < tinyG.txt | java RandomSurfer 100
 0.24000 0.23000 0.16000 0.25000 0.12000
% java Transition < tinyG.txt | java RandomSurfer 1000000
0.27324 0.26568 0.14581 0.24737 0.06790

Markov chains

The random process that describes the surfer’s behavior is known as a Markov chain, named after the Russian mathematician Andrey Markov, who developed the concept in the early 20th century. Markov chains are widely applicable and well studied, and they have many remarkable and useful properties. For example, you may have wondered why RandomSurfer starts the random surfer at page 0—you might have expected a random choice. A basic limit theorem for Markov chains says that the surfer could start anywhere, because the probability that a random surfer eventually winds up on any particular page is the same for all starting pages! No matter where the surfer starts, the process eventually stabilizes to a point where further surfing provides no further information. This phenomenon is known as mixing. Though this phenomenon is perhaps counterintuitive at first, it explains coherent behavior in a situation that might seem chaotic. In the present context, it captures the idea that the web looks pretty much the same to everyone after surfing for a sufficiently long time. However, not all Markov chains have this mixing property. For example, if we eliminate the random leap from our model, certain configurations of web pages can present problems for the surfer. Indeed, there exist on the web sets of pages known as spider traps, which are designed to attract incoming links but have no outgoing links. Without the random leap, the surfer could get stuck in a spider trap. The primary purpose of the 90–10 rule is to guarantee mixing and eliminate such anomalies.

Page ranks

The RandomSurfer simulation is straightforward: it loops for the indicated number of moves, randomly surfing through the graph. Because of the mixing phenomenon, increasing the number of iterations gives increasingly accurate estimates of the probability that the surfer lands on each page (the page ranks). How do the results compare with your intuition when you first thought about the question? You might have guessed that page 4 was the lowest-ranked page, but did you think that pages 0 and 1 would rank higher than page 3? If we want to know which page is the highest rank, we need more precision and more accuracy. RandomSurfer needs 10n moves to get answers precise to n decimal places and many more moves for those answers to stabilize to an accurate value. For our example, it takes tens of thousands of iterations to get answers accurate to two decimal places and millions of iterations to get answers accurate to three places (see EXERCISE 1.6.5). The end result is that page 0 beats page 1 by 27.3% to 26.6%. That such a tiny difference would appear in such a small problem is quite surprising: if you guessed that page 0 is the most likely spot for the surfer to end up, you were lucky!

Accurate page rank estimates for the web are valuable in practice for many reasons. First, using them to put in order the pages that match the search criteria for web searches proved to be vastly more in line with people’s expectations than previous methods. Next, this measure of confidence and reliability led to the investment of huge amounts of money in web advertising based on page ranks. Even in our tiny example, page ranks might be used to convince advertisers to pay up to four times as much to place an ad on page 0 as on page 4. Computing page ranks is mathematically sound, an interesting computer science problem, and big business, all rolled into one.

The input graph and page ranks along with a histogram are displayed.
Visualizing the histogram

With StdDraw, it is also easy to create a visual representation that can give you a feeling for how the random surfer visit frequencies converge to the page ranks. If you enable double buffering; scale the x- and y-coordinates appropriately; add this code

StdDraw.clear();
for (int i = 0; i < n; i++)
   StdDraw.filledRectangle(i, freq[i]/2.0, 0.25, freq[i]/2.0);
StdDraw.show();
StdDraw.pause(10);

to the random move loop; and run RandomSurfer for a large number of trials, then you will see a drawing of the frequency histogram that eventually stabilizes to the page ranks. After you have used this tool once, you are likely to find yourself using it every time you want to study a new model (perhaps with some minor adjustments to handle larger models).

Studying other models

RandomSurfer and Transition are excellent examples of data-driven programs. You can easily define a graph by creating a file like tiny.txt that starts with an integer n and then specifies pairs of integers between 0 and n-1 that represent links connecting pages. You are encouraged to run it for various data models as suggested in the exercises, or to make up some graphs of your own to study. If you have ever wondered how web page ranking works, this calculation is your chance to develop better intuition about what causes one page to be ranked more highly than another. Which kind of page is likely to be rated highly? One that has many links to other pages, or one that has just a few links to other pages? The exercises in this section present many opportunities to study the behavior of the random surfer. Since RandomSurfer uses standard input, you can also write simple programs that generate large graphs, pipe their output through both Transition and RandomSurfer, and in this way study the random surfer on large graphs. Such flexibility is an important reason to use standard input and standard output.

Directly simulating the behavior of a random surfer to understand the structure of the web is appealing, but it has limitations. Think about the following question: could you use it to compute page ranks for a web graph with millions (or billions!) of web pages and links? The quick answer to this question is no, because you cannot even afford to store the transition matrix for such a large number of pages. A matrix for millions of pages would have trillions of elements. Do you have that much space on your computer? Could you use RandomSurfer to find page ranks for a smaller graph with, say, thousands of pages? To answer this question, you might run multiple simulations, record the results for a large number of trials, and then interpret those experimental results. We do use this approach for many scientific problems (the gambler’s ruin problem is one example; SECTION 2.4 is devoted to another), but it can be very time-consuming, as a huge number of trials may be necessary to get the desired accuracy. Even for our tiny example, we saw that it takes millions of iterations to get the page ranks accurate to three or four decimal places. For larger graphs, the required number of iterations to obtain accurate estimates becomes truly huge.

Mixing a Markov chain

It is important to remember that the page ranks are a property of the transition matrix, not any particular approach for computing them. That is, RandomSurfer is just one way to compute page ranks. Fortunately, a simple computational model based on a well-studied area of mathematics provides a far more efficient approach than simulation to the problem of computing page ranks. That model makes use of the basic arithmetic operations on two-dimensional matrices that we considered in SECTION 1.4.

Squaring a Markov chain

What is the probability that the random surfer will move from page i to page j in two moves? The first move goes to an intermediate page k, so we calculate the probability of moving from i to k and then from k to j for all possible k and add up the results. For our example, the probability of moving from 1 to 2 in two moves is the probability of moving from 1 to 0 to 2 (0.02 × 0.02), plus the probability of moving from 1 to 1 to 2 (0.02 × 0.38), plus the probability of moving from 1 to 2 to 2 (0.38 × 0.02), plus the probability of moving from 1 to 3 to 2 (0.38 × 0.02), plus the probability of moving from 1 to 4 to 2 (0.20 × 0.47), which adds up to a grand total of 0.1172. The same process works for each pair of pages. This calculation is one that we have seen before, in the definition of matrix multiplication: the element in row i and column j in the result is the dot product of row i and column j in the original. In other words, the result of multiplying p[][] by itself is a matrix where the element in row i and column j is the probability that the random surfer moves from page i to page j in two moves. Studying the elements of the two-move transition matrix for our example is well worth your time and will help you better understand the movement of the random surfer. For instance, the largest value in the square is the one in row 2 and column 0, reflecting the fact that a surfer starting on page 2 has only one link out, to page 3, where there is also only one link out, to page 0. Therefore, by far the most likely outcome for a surfer starting on page 2 is to end up in page 0 after two moves. All of the other two-move routes involve more choices and are less probable. It is important to note that this is an exact computation (up to the limitations of Java’s floating-point precision); in contrast, RandomSurfer produces an estimate and needs more iterations to get a more accurate estimate.

An illustration depicts squaring a Markov chain.
The power method

We might then calculate the probabilities for three moves by multiplying by p[][] again, and for four moves by multiplying by p[][] yet again, and so forth. However, matrix–matrix multiplication is expensive, and we are actually interested in a vector–matrix multiplication. For our example, we start with the vector

[1.0 0.0 0.0 0.0 0.0 ]

which specifies that the random surfer starts on page 0. Multiplying this vector by the transition matrix gives the vector

[.02 .92 .02 .02 .02 ]

which is the probabilities that the surfer winds up on each of the pages after one step. Now, multiplying this vector by the transition matrix gives the vector

[.05 .04 .36 .37 .19 ]

which contains the probabilities that the surfer winds up on each of the pages after two steps. For example, the probability of moving from 0 to 2 in two moves is the probability of moving from 0 to 0 to 2 (0.02 × 0.02), plus the probability of moving from 0 to 1 to 2 (0.92 × 0.38), plus the probability of moving from 0 to 2 to 2 (0.02 × 0.02), plus the probability of moving from 0 to 3 to 2 (0.02 × 0.02), plus the probability of moving from 0 to 4 to 2 (0.02 × 0.47), which adds up to a grand total of 0.36. From these initial calculations, the pattern is clear: the vector giving the probabilities that the random surfer is at each page after t steps is precisely the product of the corresponding vector for t1 steps and the transition matrix. By the basic limit theorem for Markov chains, this process converges to the same vector no matter where we start; in other words, after a sufficient number of moves, the probability that the surfer ends up on any given page is independent of the starting point. Markov (PROGRAM 1.6.3) is an implementation that you can use to check convergence for our example. For instance, it gets the same results (the page ranks accurate to two decimal places) as RandomSurfer, but with just 20 matrix–vector multiplications instead of the tens of thousands of iterations needed by RandomSurfer. Another 20 multiplications gives the results accurate to three decimal places, as compared with millions of iterations for RandomSurfer, and just a few more give the results to full precision (see EXERCISE 1.6.6).

The power method for computing page ranks (limit values of transition probabilities) is shown.

Program 1.6.3 Mixing a Markov chain

public class Markov
{  //  Compute page ranks after trials moves.
   public static void main(String[] args)
   {
      int trials = Integer.parseInt(args[0]);
      int n = StdIn.readInt();
      StdIn.readInt();

      // Read transition matrix.
      double[][] p = new double[n][n];
      for (int i = 0; i < n; i++)
         for (int j = 0; j < n; j++)
            p[i][j] = StdIn.readDouble();

      // Use the power method to compute page ranks.
      double[] ranks = new double[n];
      ranks[0] = 1.0;
      for (int t = 0; t < trials; t++)
      {  // Compute effect of next move on page ranks.
         double[] newRanks = new double[n];
         for (int j = 0; j < n; j++)
         {  //  New rank of page j is dot product
            //  of old ranks and column j of p[][].
            for (int k = 0; k < n; k++)
               newRanks[j] += ranks[k]*p[k][j];
         }

         for (int j = 0; j < n; j++)  // Update ranks[].
            ranks[j] = newRanks[j];
      }

      for (int i = 0; i < n; i++)     // Print page ranks.
         StdOut.printf("%8.5f", ranks[i]);
      StdOut.println();
   }
}

   trials  | number of moves
     n     | number of pages
   p[][]   | transition matrix
  ranks[]  | page ranks
newRanks[] | new page ranks

This program reads a transition matrix from standard input and computes the probabilities that a random surfer lands on each page (page ranks) after the number of steps specified as command-line argument.


% java Transition < tinyG.txt | java Markov 20
 0.27245 0.26515 0.14669 0.24764 0.06806
% java Transition < tinyG.txt | java Markov 40
 0.27303 0.26573 0.14618 0.24723 0.06783

An input graph along with page ranks and a histogram for a larger example are shown.

Markov chains are well studied, but their impact on the web was not truly felt until 1998, when two graduate students—Sergey Brin and Lawrence Page—had the audacity to build a Markov chain and compute the probabilities that a random surfer hits each page for the whole web. Their work revolutionized web search and is the basis for the page ranking method used by Google, the highly successful web search company that they founded. Specifically, their idea was to present to the user a list of web pages related to their search query in decreasing order of page rank. Page ranks (and related techniques) now predominate because they provide users with more relevant web pages for typical searches than earlier techniques (such as ordering pages by the number of incoming links). Computing page ranks is an enormously time-consuming task, due to the huge number of pages on the web, but the result has turned out to be enormously profitable and well worth the expense.

Lessons

Developing a full understanding of the random surfer model is beyond the scope of this book. Instead, our purpose is to show you an application that involves writing a bit more code than the short programs that we have been using to teach specific concepts. Which specific lessons can we learn from this case study?

We already have a full computational model

Primitive types of data and strings, conditionals and loops, arrays, and standard input/output/drawing/audio enable you to address interesting problems of all sorts. Indeed, it is a basic precept of theoretical computer science that this model suffices to specify any computation that can be performed on any reasonable computing device. In the next two chapters, we discuss two critical ways in which the model has been extended to drastically reduce the amount of time and effort required to develop large and complex programs.

Data-driven code is prevalent

The concept of using the standard input and output streams and saving data in files is a powerful one. We write filters to convert from one kind of input to another, generators that can produce huge input files for study, and programs that can handle a wide variety of models. We can save data for archiving or later use. We can also process data derived from some other source and then save it in a file, whether it is from a scientific instrument or a distant website. The concept of data-driven code is an easy and flexible way to support this suite of activities.

Accuracy can be elusive

It is a mistake to assume that a program produces accurate answers simply because it can print numbers to many decimal places of precision. Often, the most difficult challenge that we face is ensuring that we have accurate answers.

Uniform random numbers are only a start

When we speak informally about random behavior, we often are thinking of something more complicated than the “every value equally likely” model that Math.random() gives us. Many of the problems that we consider involve working with random numbers from other distributions, such as RandomSurfer.

Efficiency matters

It is also a mistake to assume that your computer is so fast that it can do any computation. Some problems require much more computational effort than others. For example, the method used in Markov is far more efficient than directly simulating the behavior of a random surfer, but it is still too slow to compute page ranks for the huge web graphs that arise in practice. CHAPTER 4 is devoted to a thorough discussion of evaluating the performance of the programs that you write. We defer detailed consideration of such issues until then, but remember that you always need to have some general idea of the performance requirements of your programs.

Perhaps the most important lesson to learn from writing programs for complicated problems like the example in this section is that debugging is difficult. The polished programs in the book mask that lesson, but you can rest assured that each one is the product of a long bout of testing, fixing bugs, and running the programs on numerous inputs. Generally we avoid describing bugs and the process of fixing them in the text because that makes for a boring account and overly focuses attention on bad code, but you can find some examples and descriptions in the exercises and on the booksite.

Exercises

1.6.1 Modify Transition to take the leap probability as a command-line argument and use your modified version to examine the effect on page ranks of switching to an 80–20 rule or a 95–5 rule.

1.6.2 Modify Transition to ignore the effect of multiple links. That is, if there are multiple links from one page to another, count them as one link. Create a small example that shows how this modification can change the order of page ranks.

1.6.3 Modify Transition to handle pages with no outgoing links, by filling rows corresponding to such pages with the value 1/n, where n is the number of columns.

1.6.4 The code fragment in RandomSurfer that generates the random move fails if the probabilities in the row p[page] do not add up to 1. Explain what happens in that case, and suggest a way to fix the problem.

1.6.5 Determine, to within a factor of 10, the number of iterations required by RandomSurfer to compute page ranks accurate to 4 decimal places and to 5 decimal places for tiny.txt.

1.6.6 Determine the number of iterations required by Markov to compute page ranks accurate to 3 decimal places, to 4 decimal places, and to ten 10 places for tiny.txt.

1.6.7 Download the file medium.txt from the booksite (which reflects the 50-page example depicted in this section) and add to it links from page 23 to every other page. Observe the effect on the page ranks, and discuss the result.

1.6.8 Add to medium.txt (see the previous exercise) links to page 23 from every other page, observe the effect on the page ranks, and discuss the result.

1.6.9 Suppose that your page is page 23 in medium.txt. Is there a link that you could add from your page to some other page that would raise the rank of your page?

1.6.10 Suppose that your page is page 23 in medium.txt. Is there a link that you could add from your page to some other page that would lower the rank of that page?

1.6.11 Use Transition and RandomSurfer to determine the page ranks for the eight-page graph shown below.

1.6.12 Use Transition and Markov to determine the page ranks for the eight-page graph shown below.

An input graph for eight pages numbered 0 to 7 is shown.

Creative Exercises

1.6.13 Matrix squaring. Write a program like Markov that computes page ranks by repeatedly squaring the matrix, thus computing the sequence p, p2, p4, p8, p16, and so forth. Verify that all of the rows in the matrix converge to the same values.

1.6.14 Random web. Write a generator for Transition that takes as command-line arguments a page count n and a link count m and prints to standard output n followed by m random pairs of integers from 0 to n-1. (See SECTION 4.5 for a discussion of more realistic web models.)

1.6.15 Hubs and authorities. Add to your generator from the previous exercise a fixed number of hubs, which have links pointing to them from 10% of the pages, chosen at random, and authorities, which have links pointing from them to 10% of the pages. Compute page ranks. Which rank higher, hubs or authorities?

1.6.16 Page ranks. Design a graph in which the highest-ranking page has fewer links pointing to it than some other page.

1.6.17 Hitting time. The hitting time for a page is the expected number of moves between times the random surfer visits the page. Run experiments to estimate the hitting times for tiny.txt, compare hitting times with page ranks, formulate a hypothesis about the relationship, and test your hypothesis on medium.txt.

1.6.18 Cover time. Write a program that estimates the time required for the random surfer to visit every page at least once, starting from a random page.

1.6.19 Graphical simulation. Create a graphical simulation where the size of the dot representing each page is proportional to its page rank. To make your program data driven, design a file format that includes coordinates specifying where each page should be drawn. Test your program on medium.txt.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset