• No results found

hello, world

BLACK , DARK_GRAY , GRAY , LIGHT_GRAY , WHITE

5.5 Algorithmic methods

In addition to their role as a tool for simplifying programs by allowing you to refer to a set of statements using a single name, methods are important to programming because they provide a basis for the implementation of algorithms, which were introduced briefly in Chapter 1. An algorithm is an abstract strategy; writing a method is the conventional way to express that algorithm in the context of a programming language. Thus, when you want to implement an algorithm as part of a program, you will typically write a method— which may in turn call other methods to handle part of its work—to carry out that algorithm.

Although you have seen several simple algorithms implemented in the context of the sample programs, you have not had a chance to focus on the nature of the algorithmic process itself. Most of the programming problems you have seen so far are simple enough that the appropriate solution technique springs immediately to mind. As problems become more complex, however, their solutions require more thought, and you will need to consider more than one strategy before writing the final program.

As an illustration of how algorithmic strategies take shape, the sections that follow consider two solutions to a problem from classical mathematics, which is finding the greatest common divisor of two integers. Given two integers x and y, the greatest common divisor (or gcd for short) is the largest integer that divides evenly into both. For example, the gcd of 49 and 35 is 7, the gcd of 6 and 18 is 6, and the gcd of 32 and 33 is 1.

Suppose that you have been asked to write a method that accepts the integers x and y as input and returns their greatest common divisor. From the caller’s point of view, what you want is a method gcd(x,y) that takes two integers as arguments and returns another integer that is their greatest common divisor. The header line for this method is therefore

int gcd(int x, int y)

How might you go about designing an algorithm to perform this calculation? The “brute force” approach

In many ways, the most obvious approach to calculating the gcd is simply to try every possibility. To start, you simply “guess” that gcd(x,y) is the smaller of x and y, because any larger value could not possibly divide evenly into a smaller number. You then proceed by dividing your guess into x and y and seeing if it divides evenly into both. If it does, you have the answer; if not, you subtract 1 from your guess and try again. A strategy that tries every possibility is often called a brute force approach.

int gcd(int x, int y) {

int guess = Math.min(x, y);

while (x % guess != 0 || y % guess != 0) { guess--;

}

return guess; }

Before you decide that this implementation is in fact a valid algorithm for computing the gcd function, you must ask yourself several questions about the code. Will the brute- force implementation of gcd always give the correct answer? Will it always terminate, or might the method continue forever?

To see that the program gives the correct answer, you need to look at the condition in the while loop

x % guess != 0 || y % guess != 0

As always, the while condition indicates under what circumstances the loop will continue. To find out what condition causes the loop to terminate, you have to negate the while condition. Negating a condition involving && or || can be tricky unless you remember how to apply De Morgan’s law, which was introduced in the section on “Logical operators” in Chapter 4. De Morgan’s law indicates that the following condition must hold when the while loop exits:

x % guess == 0 && y % guess == 0

From this condition, you can see immediately that the final value of guess is certainly a common divisor. To recognize that it is in fact the greatest common divisor, you have to think about the strategy embodied in the while loop. The critical factor to notice in the strategy is that the program counts backward through all the possibilities. The greatest common divisor can never be larger than x (or y, for that matter), and the brute- force search therefore begins with that value. If the program ever gets out of the while loop, it must have already tried each value between x and the current value of guess. Thus, if there were a larger value that divided evenly into both x and y, the program would already have found it in an earlier iteration of the while loop.

To recognize that the method terminates, the key insight is that the value of guess must eventually reach 1, even if no larger common divisor is found. At this point, the while loop will surely terminate, because 1 will divide evenly into both x and y, no matter what values those variables have.

Euclid’s algorithm

Brute force is not, however, the only effective strategy. In fact, the brute-force algorithm used in the gcd implementation presented in the preceding section is a poor choice if you are at all concerned with efficiency. Consider what happens, for example, if you call the method with the integers 1,000,005 and 1,000,000. The brute-force algorithm will run through the body of the while loop almost a million times before it comes up with 5—an answer that you can easily determine just by thinking about the two numbers.

What you need to find is an algorithm that is guaranteed to terminate with the correct answer but that requires fewer steps than the brute-force approach. This is where cleverness and a clear understanding of the problem pay off. Fortunately, the necessary creative insight has already been supplied by the Greek mathematician Euclid, whose

Elements (book 7, proposition II) contains an elegant solution to this problem. In modern English, Euclid’s algorithm can be described as follows:

1. Divide x by y and compute the remainder; call that remainder r. 2. If r is zero, the procedure is complete, and the answer is y.

3. If r is not zero, set x equal to the old value of y, set y equal to r, and repeat the entire process.

You can easily translate this algorithmic description into the following Java code: int gcd(int x, int y) {

int r = x % y; while (r != 0) { x = y; y = r; r = x % y; } return y; }

This implementation of the gcd method also correctly finds the greatest common divisor of two integers. It differs from the brute-force implementation in two respects. On the one hand, it computes the result much more quickly. On the other, it is more difficult to prove correct.

Defending the correctness of Euclid’s algorithm

Although a formal proof of correctness for Euclid’s algorithm is beyond the scope of this book, you can easily get a feel for how the algorithm works by adopting the mental model of mathematics the Greeks used. In Greek mathematics, geometry held center stage, and numbers were thought of as distances. For example, when Euclid set out to find the greatest common divisor of two whole numbers, such as 55 and 15, he framed the problem as one of finding the longest measuring stick that could be used to mark off each of the two distances involved. Thus, you can visualize the specific problem by starting out with two sticks, one 55 units long and one 15 units long, as follows:

x y

The problem is to find a new measuring stick that you can lay end to end on top of each of these sticks so that it precisely covers each of the distances, x and y.

Euclid’s algorithm begins by marking off the large stick in units of the shorter one: x

y

Unless the smaller number is an exact divisor of the larger one, there is some remainder, as indicated by the shaded section of the upper stick. In this case, 15 goes into 55 three times with 10 left over, which means that the shaded region is 10 units long. The fundamental insight that Euclid had is that the greatest common divisor for the original two distances must also be the greatest common divisor of the length of the shorter stick and the distance represented by the shaded region in the diagram.

Given this observation, you can solve the original problem by reducing it to a simpler problem involving smaller numbers. Here, the new numbers are 15 and 10, and you can find their greatest common divisor by reapplying Euclid’s algorithm. You start by representing the new values, x′ and y′, as measuring sticks of the appropriate length. You then mark off the larger stick in units of the smaller one.

xy

Once again, this process results in a leftover region, which this time has length 5. If you then repeat the process one more time, you discover that the shaded region of length 5 is itself the common divisor of x′ and y′ and, therefore, by Euclid’s proposition, of the original numbers x and y. That this new value is indeed a common divisor of the original numbers is demonstrated by the following diagram:

x y

Euclid supplies a complete proof of his proposition in the Elements. If you are intrigued by how mathematicians thought about such problems almost 2500 years ago, you may find it interesting to look up the original source.

Comparing the efficiency of the two algorithms

To illustrate the difference in efficiency of the two algorithmic strategies for computing the greatest common divisor, consider once again the integers 1,000,005 and 1,000,000. To find the greatest common divisor of these two integers, the brute-force algorithm requires a million steps; Euclid’s algorithm requires only two. At the beginning of Euclid’s algorithm, x is 1000005, y is 1000000, and r is set to 5 during the first cycle of the loop. Since the value of r is not 0, the program sets x to 1000000, sets y to 5, and starts again. On the second cycle, the new value of r is 0, so the program exits from the while loop and reports that the answer is 5.

The two strategies for computing greatest common divisors presented in this chapter offer a clear demonstration that the choice of algorithm can have a profound effect on the efficiency of the solution. If you continue your study of computer science, you will learn how to quantify such differences in performance along with several general approaches for improving algorithmic efficiency.

Summary

In this chapter, you learned about methods, which enable you to refer to an entire set of operations by using a simple name. By allowing the programmer to ignore the internal details and concentrate only on the effect of a method as a whole, methods provide a critical tool for reducing the conceptual complexity of programs.

The important points introduced in this chapter include:

• A method consists of a set of program statements that have been collected together and given a name. Other parts of the program can then call that method, possibly passing it information in the form of arguments and receiving a result returned by that method. • A method that returns a value must have a return statement that specifies the result.

• Methods in an object-oriented language like Java are often applied to other objects. When that occurs, you need to specify a receiver as part of the method call, like this:

receiver.name(arguments)

• Methods that return Boolean values are called predicate methods and play an important role in programming.

• Within the body of a method, the variables that act as placeholders for the argument values are called formal parameters.

• Variables declared with a method are local to that method and cannot be used outside of it. Internally, all the variables declared within a method, including the parameters, are stored together in a stack frame.

• When a method returns, it continues from precisely the point at which the call was made. The computer refers to this point as the return address and keeps track of it in the stack frame.

• Because methods tie together a collection of statements so as to have a specific effect, methods provide the standard framework for expressing algorithms in Java.

• There are often many different algorithms to solve a particular problem. Choosing the algorithm that best fits the application is an important part of your task as a programmer.

Review questions

1. Explain in your own words the difference between a method and a program. 2. Define the following terms as they apply to methods: call, argument, return.

3. What is the difference between passing information to a method by using arguments and reading input data using methods like readInt? When would each action be appropriate?

4. How do you specify the result of a method in Java?

5. Can there be more than one return statement in the body of a method? 6. How do you indicate that you want to apply a method to another object?

7. Why has it been unnecessary to specify receivers in the programs presented in the earlier chapters?

8. Why was it unnecessary to include a break statement at the end of each case clause in the monthName method presented in the chapter?

9. What is a predicate method?

10. How do you tell whether two strings contain the same characters? 11. What is the relationship between arguments and formal parameters?

12. Variables declared within a method are said to be local variables. What is the significance of the word local in this context?

14. What is a brute-force algorithm?

15. Use Euclid’s algorithm to compute the greatest common divisor of 7735 and 4185. What values does the local variable r take on during the calculation?

16. In the examples of Euclid’s algorithm to calculate gcd(x, y) that appear in this chapter, x is always larger than y. Does this condition matter? What happens if x is smaller than y?

Programming exercises

1. Write a program that displays the value of the mathematical constant φ = 1 + √⎯5

2

This constant φ is called the golden ratio. Classical mathematicians believed that this number represented the most aesthetically pleasing ratio for the dimensions of a rectangle, but it also turns up in computational mathematics.

2. In high-school algebra, you learned that the standard quadratic equation ax2 + bx + c = 0

has two solutions given by the formula

x = –b ±

√⎯⎯⎯⎯⎯⎯

b2 – 4ac 2a

The first solution is obtained by using + in place of ±; the second is obtained by using – in place of ±.

Write a Java program that accepts values for a, b, and c, and then calculates the two solutions. If the quantity under the square root sign is negative, the equation has no real solutions, and your program should display a message to that effect. You may assume that the value for a is nonzero. Your program should be able to duplicate the following sample run:

Quadratic

Enter coefficients for the quadratic equation: a: 1

b: -5

c: 6

The first solution is 3 The second solution is 2

3. The Fibonacci sequence, in which each new term is the sum of the preceding two, was introduced in Chapter 4, exercise 9. Rewrite the program requested in that exercise, changing the implementation so that your program calls a method fibonacci(n) to calculate the nth Fibonacci number. In terms of the number of mathematical calculations required, is your new implementation more or less efficient that the one you used in Chapter 4?

4. Write a method raiseIntToPower that takes two integers, n and k, and returns nk. Use your method to display a table of values of 2k for all values of

k from 0 to 10. 5. Write a method raiseRealToPower that takes a floating-point value x and an integer

k and returns xk. Implement your method so that it can correctly calculate the result when k is negative, using the relationship

x-k = 1 xk

Use your method to display a table of values of πk for all values of

k from –4 to 4. 6. Write a method nDigits(n) that returns the number of digits in the integer n, which

you may assume is positive. Design a main program to test your method. For hints about how to write this program, you might want to look back at the DigitSum program that was given in Figure 4-6.

7. Write a predicate method isPerfectSquare(n) that returns true if the integer n is a perfect square. Remember that the method Math.sqrt returns a double, which is therefore only an approximation of the actual square root.

8. Write a predicate method askYesNoQuestion(str) that prints out the string str as a question for the user and then waits for a response. If the user enters the string "yes", the askYesNoQuestion method should return true; if the user enters "no", the method should return false. If the user enters anything else, askYesNoQuestion should remind the user that it is seeking a yes-or-no answer and then repeat the question. For example, if the program includes the statement

if (askYesNoQuestion("Would you like instructions")) . . . the interaction with the user might look like this:

YesNoExample

Would you like instructions? maybe Please answer yes or no.

Would you like instructions? no

9. The values of the combinations method used in the text are often displayed in the form of a triangle using the following arrangement:

C(0,0) C(1,0) C(1,1) C(2,0) C(2,1) C(2,2) C(3,0) C(3,1) C(3,2) C(3,3) C(4,0) C(4,1) C(4,2) C(4,3) C(4,4)

and so on. This figure is called Pascal’s Triangle after the seventeenth-century French mathematician Blaise Pascal, who described it, even though it was known by Chinese mathematicians over 2000 years ago. Pascal’s Triangle has the interesting property that every interior entry is the sum of the two entries above it.

Write a Java GraphicsProgram that uses GLabel objects to display the first eight rows of Pascal’s Triangle.

10. An integer greater than 1 is said to be prime if it has no divisors other than itself and one. The number 17, for example, is prime, because it is divisible only by 1 and 17. The number 91, however, is not prime because it is divisible by 7 and 13. Write a predicate method isPrime(n) that returns true if the integer n is prime, and false otherwise. As an initial strategy, implement isPrime using a brute-force algorithm that simply tests every possible divisor. Once you have that version working, try to