5.10 Formulating Algorithms: Sentinel-Controlled Iteration

Let us generalize Section 5.9’s class-average problem. Consider the following problem:

  • Develop a class-averaging app that processes grades for an arbitrary number of students each time it’s run.

In the previous class-average example, the problem statement specified the number of students, so the number of grades (10) was known in advance. In this example, no indication is given of how many grades the user will enter during the program’s execution. The program must process an arbitrary number of grades. How can it determine when to stop the input of grades? How will it know when to calculate and print the class average?

One way to solve this problem is to use a special value called a sentinel value (also called a signal value, a dummy value or a flag value) to indicate “end of data entry.” The user enters grades until all legitimate grades have been entered. The user then types the sentinel value to indicate that no more grades will be entered. Sentinel-controlled iteration is often called indefinite iteration because the number of iterations is not known before the loop begins executing. Clearly, a sentinel value must be chosen that cannot be confused with an acceptable input value. Grades on a quiz are nonnegative integers, so –1 is an acceptable sentinel value for this problem. Thus, a run of the class-averaging program might process a stream of inputs such as 95, 96, 75, 74, 89 and –1. The program would then compute and print the class average for the grades 95, 96, 75, 74 and 89; since –1 is the sentinel value, it should not enter into the averaging calculation.

5.10.1 Top-Down, Stepwise Refinement: The Top and First Refinement

We approach this class-averaging program with a technique called top-down, stepwise refinement, which is essential to the development of well-structured programs. We begin with a pseudocode representation of the top—a single statement that conveys the overall purpose of the program:


Determine the class average for the quiz

The top is, in effect, a complete representation of a program. Unfortunately, the top rarely conveys sufficient detail from which to write a C# app. So we now begin the refinement process. We divide the top into a series of smaller tasks and list these in the order in which they’ll be performed. This results in the following first refinement:


Initialize variables
Input, sum and count the quiz grades
Calculate and print the class average

This refinement uses only the sequence structure—the steps listed should execute in order, one after the other.

Software Engineering Observation 5.2

Each refinement, as well as the top itself, is a complete specification of the algorithm— only the level of detail varies.

Software Engineering Observation 5.3

Many programs can be divided logically into three phases: an initialization phase that initializes the variables; a processing phase that inputs data values and adjusts program variables accordingly; and a termination phase that calculates and outputs the final results.

5.10.2 Second Refinement

The preceding Software Engineering Observation is often all you need for the first refinement in the top-down process. To proceed to the next level of refinement—that is, the second refinement—we commit to specific variables. In this example, we need a running total of the numbers, a count of how many numbers have been processed, a variable to receive the value of each grade as it’s entered by the user and a variable to hold the calculated average. The pseudocode statement


Initialize variables

can be refined as follows:


Initialize total to zero
Initialize counter to zero

Only the variables total and counter need to be initialized before they’re used. The variables average and grade (for the calculated average and the user input, respectively) need not be initialized, because their values will be replaced as they’re calculated or input.

The pseudocode statement


Input, sum and count the quiz grades

requires iteration to successively input each grade. We do not know in advance how many grades will be entered, so we’ll use sentinel-controlled iteration. The user enters grades one at a time. After entering the last grade, the user enters the sentinel value. The program tests for the sentinel value after each grade is input and terminates the loop when the user enters the sentinel value. The second refinement of the preceding pseudocode statement is then


Prompt the user to enter the first grade
Input the first grade (possibly the sentinel)
While the user has not yet entered the sentinel
    Add this grade into the running total
    Add one to the grade counter
    Prompt the user to enter the next grade
    Input the next grade (possibly the sentinel)

We simply indent the statements under the While to show that they belong to the While. Again, pseudocode is only an informal program-development aid.

The pseudocode statement


Calculate and print the class average

can be refined as follows:


If the counter is not equal to zero
      Set the average to the total divided by the counter
      Print the average
Else
      Print “No grades were entered”

We’re careful here to test for the possibility of division by zero—a logic error that, if undetected, would cause the program to fail or produce invalid output. The complete second refinement of the pseudocode for the class-average problem is shown in Fig. 5.10.

Error-Prevention Tip 5.4

When performing division by an expression whose value could be zero, explicitly test for this possibility and handle it appropriately in your app (e.g., by displaying an error message) rather than allowing the error to occur.

 

Fig. 5.10 Class-averaging pseudocode algorithm with sentinel-controlled iteration.

Alternate View

  1   Initialize total to zero
  2   Initialize counter to zero
  3
  4   Prompt the user to enter the first grade
  5   Input the first grade (possibly the sentinel)
  6
  7   While the user has not yet entered the sentinel
  8       Add this grade into the running total
  9       Add one to the grade counter
 10       Prompt the user to enter the next grade
 11       Input the next grade (possibly the sentinel)
 12
 13   If the counter is not equal to zero
 14       Set the average to the total divided by the counter
 15       Print the average
 16   Else
 17       Print “No grades were entered”

In Fig. 5.8 and Fig. 5.10, we included blank lines and indentation in the pseudocode to make it more readable. The blank lines separate the algorithms into their phases and set off control statements; the indentation emphasizes the bodies of the control statements.

The pseudocode algorithm in Fig. 5.10 solves the more general class-average problem. This algorithm was developed after two refinements. Sometimes more are needed.

Software Engineering Observation 5.4

Terminate the top-down, stepwise refinement process when you’ve specified the pseudocode algorithm in sufficient detail for you to convert the pseudocode to C#.

Software Engineering Observation 5.5

Some programmers do not use program-development tools like pseudocode. They feel that their ultimate goal is to solve the problem on a computer and that writing pseudocode merely delays the production of final outputs. Although this may work for simple and familiar problems, it can lead to serious errors and delays in large, complex projects.

5.10.3 Implementing Sentinel-Controlled Iteration

In Fig. 5.11, method Main implements the pseudocode algorithm of Fig. 5.10. Although each grade entered by the user is an integer, the averaging calculation is likely to produce a number with a decimal point—in other words, a real number or floating-point number (e.g., 7.33, 0.0975 or 1000.12345). The type int cannot represent such a number, so this example must use another type to do so. C# provides data types float and double to store floating-point numbers in memory. The primary difference between these types is that double variables can typically store numbers with larger magnitude and finer detail (i.e., more digits to the right of the decimal point—also known as the number’s precision). We say more about floating-point types in Chapter 6.

Fig. 5.11 Solving the class-average problem using sentinel-controlled iteration.

Alternate View

  1   // Fig. 5.11: ClassAverage.cs
  2   // Solving the class-average problem using sentinel-controlled iteration.
  3   using System;
  4
  5   class ClassAverage
  6   {
  7      static void Main()
  8      {
  9         // initialization phase
 10         int total = 0; // initialize sum of grades
 11         int gradeCounter = 0; // initialize # of grades entered so far
 12
 13         // processing phase
 14         // prompt for input and read grade from user    
 15         Console.Write("Enter grade or -1 to quit: ");   
 16         int grade = int.Parse(Console.ReadLine());      
 17
 18         // loop until sentinel value is read from the user
 19         while (grade != -1)
 20         {
 21            total = total + grade; // add grade to total
 22            gradeCounter = gradeCounter + 1; // increment counter
 23
 24            // prompt for input and read grade from user 
 25            Console.Write("Enter grade or -1 to quit: ");
 26            grade = int.Parse(Console.ReadLine());       
 27         }
 28
 29         // termination phase
 30         // if the user entered at least one grade...
 31         if (gradeCounter != 0)
 32         {
 33            // use number with decimal point to calculate average of grades
 34            double average = (double) total / gradeCounter;
 35
 36            // display the total and average (with two digits of precision)
 37            Console.WriteLine(
 38               $"
Total of the {gradeCounter} grades entered is {total}");
 39            Console.WriteLine($"Class average is {average:F} ");
 40         }
 41         else // no grades were entered, so output error message
 42         {
 43            Console.WriteLine("No grades were entered");
 44         }
 45      }
 46   }

Enter grade or -1 to quit: 97
Enter grade or -1 to quit: 88
Enter grade or -1 to quit: 72
Enter grade or -1 to quit: -1

Total of the 3 grades entered is 257
Class average is 85.67

Recall that integer division produces an integer result. This program introduces a special operator called a cast operator to force the averaging calculation to produce a floating-point numeric result. This program also stacks control statements on top of one another (in sequence)—the while statement (lines 19–27) is followed in sequence by an ifelse statement (lines 31–44). Much of the code in this program is identical to that in Fig. 5.9, so we concentrate on the new concepts.

5.10.4 Program Logic for Sentinel-Controlled Iteration

Line 11 initializes gradeCounter to 0, because no grades have been entered yet. Remember that this program uses sentinel-controlled iteration to input the grades. The program increments gradeCounter only when the user enters a valid grade. Line 34 declares double variable average, which stores the class average as a floating-point number.

Compare the program logic for sentinel-controlled iteration in this program with that for counter-controlled iteration in Fig. 5.9. In counter-controlled iteration, each iteration of the while statement (lines 14–20 of Fig. 5.9) reads a value from the user, for the specified number of iterations. In sentinel-controlled iteration, the program prompts for and reads the first value (lines 15–16 of Fig. 5.11) before reaching the while. This value determines whether the program’s flow of control should enter the body of the while. If the condition of the while is false (line 19), the user entered the sentinel value, so the body of the while does not execute (i.e., no grades were entered). If, on the other hand, the condition is true, the body begins execution, and the loop adds the grade value to the total and increments the gradeCounter (lines 21–22). Then lines 25–26 in the loop body input the next value from the user. Next, program control reaches the closing right brace of the loop body at line 27, so execution continues with the test of the while’s condition (line 19). The condition uses the most recent grade entered by the user to determine whether the loop body should execute again.

The value of variable grade is always input from the user immediately before the program tests the while condition. This allows the program to determine whether the value just input is the sentinel value before the program processes that value (i.e., adds it to the total). If the sentinel value is input, the loop terminates, and the program does not add –1 to the total.

Good Programming Practice 5.3

In a sentinel-controlled loop, prompts should remind the user of the sentinel.

After the loop terminates, the ifelse statement at lines 31–44 executes. Line 31 determines whether any grades were input. If none were input, the ifelse statement’s else part executes and displays the message "No grades were entered".

5.10.5 Braces in a while Statement

Notice the while statement’s block in Fig. 5.11 (lines 20–27). Without the braces, the loop would consider its body to be only the first statement, which adds the grade to the total. The last three statements in the block would fall outside the loop’s body, causing the computer to interpret the code incorrectly as follows:


while (grade != -1)
   total = total + grade; // add grade to total
gradeCounter = gradeCounter + 1; // increment counter

// prompt for input and read grade from user
Console.Write("Enter grade or -1 to quit: ");
grade = int.Parse(Console.ReadLine());

The preceding code would cause an infinite loop if the user did not enter the sentinel -1 at line 16 (before the while statement).

Error-Prevention Tip 5.5

Omitting the braces that delimit a block can lead to logic errors, such as infinite loops. To prevent this and other problems, we always enclose the body of every control statement in braces even if the body contains only a single statement.

5.10.6 Converting Between Simple Types Explicitly and Implicitly

If at least one grade was entered, line 34 of Fig. 5.11


double average = (double) total / gradeCounter;

calculates the average. Recall from Fig. 5.9 that integer division yields an integer result. Even though variable average is declared as a double, if we had written line 34 as


double average = total / gradeCounter;

it would lose the fractional part of the quotient before the result of the division was used to initialize average.

Cast Operator

To perform a floating-point calculation with integers in this example, you first create a temporary floating-point value using the unary cast operator. Line 34 uses the (double) unary cast operator—which has higher precedence than the arithmetic operators—to create a temporary double copy of its operand total, which appears to the right of the operator. The value stored in total is still an integer. Using a cast operator in this manner is called explicit conversion.

Promotions

After the cast operation, the calculation consists of the temporary double copy of total divided by the integer gradeCounter. For arithmetic, the compiler knows how to evaluate only expressions in which the operand types are identical. To ensure this, the compiler performs an operation called promotion (also called implicit conversion) on selected operands. In an expression containing values of types int and double, the compiler promotes int operands to double values. So, in line 34, the compiler creates a temporary copy of gradeCounter’s value of type double, then performs the floating-point division. Finally, average is initialized with the floating-point result. Section 7.6.1 discusses the allowed simple-type promotions.

Cast Operators for Any Type

Cast operators are available for all simple types. We’ll discuss cast operators for other types in Chapter 12. The cast operator is formed by placing parentheses around the name of a type. This operator is a unary operator—it takes only one operand. C# also supports unary versions of the plus (+) and minus () operators, so you can write expressions like +5 or -7. Cast operators have the second highest precedence. (See the operator precedence chart in Appendix A.)

5.10.7 Formatting Floating-Point Numbers

Line 39


Console.WriteLine($"Class average is {average:F}");

outputs the class average. In this example, we decided that we’d like to display the class average rounded to the nearest hundredth and output the average with exactly two digits to the right of the decimal point. The format specifier F in the interpolation expression


{average:F}

typically formats average’s value with two digits to the right of the decimal point—again, the Windows culture settings on the user’s machine determine the actual format, including the digits to the right of the decimal point, whether commas or periods are used for separating thousands, millions, etc.

Rounding Floating-Point Numbers

When the F format specifier is used to format a floating-point value, the formatted value is rounded to a specific decimal position, although the value in memory remains unaltered. In many cultures a floating-point value output with F will be rounded to the hundredths position—for example, 123.457 will be rounded to 123.46, and 27.333 will be rounded to 27.33—though in some cultures these values are rounded to whole numbers. In this app, the three grades entered during the sample execution total 257, which yields the average 86.66666…. In the United States, the F format specifier rounds average to the hundredths position, so the average is displayed as 85.67.

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

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