CS Behind the Curtain (CS195 2003S)

Notes on Exam 1

Contents

General Issues

Grading Technique

You start with 100 points. For each problem, I add or subtract points. (I add points for outstanding solutions; I subtract points for any errors I find.) I add two points of extra credit if you indicated how long each problem took. I add three points of extra credit for the three errors that others found in my code. That's about it.

I do not scale exams. I use the standard numeric grading scale (94 and up is an A, 90-93 is an A-, 87-89 is a B+, and so on and so forth).

Style

A few of you seemed to use the commenting style that Mr. Stone and Mr. Gum use in CSC151. That is, in the documentation, you put the names of procedures and variables in all-caps. Note that it's okay to change names to all caps in Scheme, since Scheme is generally case-insensitive. It is not okay in C. if your procedure is named readLine do not refer to it as READLINE.

I really really don't like to see lines that wrap when you print them. It looks ugly. I did not penalize anyone this time, but I will next time.

A few of you like to use something like

if (test)
  return 1;
else
  return 0;

However, that's considered inelegant, just as the similar

(if test #t #f)

is considered inelegant in Scheme. What should you do?

return test;

Error Messages

Never, never, never print error messages within a utility procedure (like readLine, rdLn, or strcpy. Instead, throw an exception (in Java) or return a special error value (such as the null from malloc). Think about it: If someone's program uses your utility, the user of that program will have no idea why your error messages are appearing.

Citations

When you cite a work, you should give the author (or "Anonymous"), title, date published, and any other appropriate information (such as publishing organization). These categories hold for cited Web pages as well as cited articles. For cited Web pages, you should also include the date the page was last modified and the date you visited it.

Six P's

I'm fairly anal about how you write the six P's. Here's what you should do:

Miscellaneous

Some of you neglected to send me an electronic copy. Some of you neglected to turn in a paper copy. I really do expect both.

Problems

Problem 1: Reading Lines

Write a procedure, readLine(file, str, max), that reads one line of text from file into str. It should stop reading when one or more of the following holds: max-1 characters have been read, the end-of-line character has been read, or the end-of-file character has been read. It should not store either end-of-line or end-of-file in the string. It should put a 0 at the end of the string. You may only use getc to read characters.

If a complete line was read, readLine should return 1. Otherwise, it should return 0.

When evaluating your answer, I will look at conciseness in addition to correctness (although it is clearly better to be correct than concise).

A Solution

readline.h
/*
 * File:
 *   readline.h
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003
 * Summary:
 *   Declarations of procedures provided by readline.c
 */

#ifndef _READLINE_H_
#define _READLINE_H_

#include <stdio.h>

/*
 * Procedure:
 *   readLine
 * Parameters:
 *   file, a pointer to a FILE
 *   str, a string
 *   max, an integer
 * Purpose:
 *   Read one line or max-1 characters from file, whichever comes
 *   first.
 * Produces:
 *   read-all, an integer
 * Preconditions:
 *   file is open for reading.
 *   max >= 1.
 *   str points to at least max consecutive characters.
 * Postconditions:
 *   At most max characters have been read from file.
 *   Nothing beyond the first end-of-line at or after the cursor 
 *     has been read.
 *   str contains the characters read from the file in order.
 *   str does not contain end-of-line.
 *   str is terminated by 0.
 *   read-all is 1 if an end-of-line or end-of-file character was read.  
 *   read-all is 0 otherwise.
 *   file is open.
 */
extern int readLine(FILE *file, char *str, int max);

#endif /* _READLINE_H_ */
readline.c
/*
 * File:
 *   readline.c
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003.
 * Summary:
 *   An implementation of the readLine procedure from CSC195 Exam 2.
 */

/********************************************************************* 
 * Headers *
 ***********/

#include <stdio.h>
#include <stdlib.h>
#include "readline.h"


/********************************************************************* 
 * Exported Procedures *
 ***********************/

/* See readline.h for documentation. */
int readLine(FILE *file, char *str, int max)
{
  int i;	/* Counter variable. */
  int ch;	/* Character read. */
  for (i=0; (i < max-1) && ((ch=getc(file)) != '\n') && (ch != EOF); i++)
    str[i] = (char) ch;
  str[i] = '\0';
  return ((ch == EOF) || (ch == '\n'));
} /* readLine(FILE *, char *, int) */

Testing

How should one test a procedure like readLine? I'm a fan of quick, simple, tests. So, repeatedly read no more than N characters (for some N) and print them out again. Here's my test program. It's interactive in that I get to type the inputs.

/*
 * File:
 *   testreadline.c
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003
 * Summary:
 *   A quick test of the readLine procedure.
 */

/*********************************************************************
 * Headers *
 ***********/

#include <stdio.h>
#include <stdlib.h>
#include "readline.h"


/*********************************************************************
 * Constants *
 *************/

#define STRLEN 10


/*********************************************************************
 * Main *
 ********/

int main(int argc, char *argv[])
{
  char ch;
  char str[STRLEN];
  FILE *infile;
  int result;

  if (argc == 1) {
    infile = stdin;
  } 
  else {
    infile = fopen(argv[1], "r");
    if (infile == NULL) {
      fprintf(stderr, "Invalid file: %s\n", argv[1]);
      exit(EXIT_FAILURE);
    } /* if (infile == NULL) */
  } /* if (argc > 1) */

  while ((ch = fgetc(infile)) != EOF) {
    ungetc(ch, infile);
    if (readLine(infile, str, STRLEN)) 
      printf("[%s]\n", str);
    else 
      printf("[%s]", str);
  } /* while */

  if (argc > 1)
    fclose(infile);

  exit(EXIT_SUCCESS);
} /* main */

Notes on the Problem

As always, I wanted you to think about special cases. Some basic special cases are when max is set to 0, when max is set to 1, and when the line contains exactly max-1 characters. The first case is somewhat meaningless, since you're supposed to stop when max-1 characters have been read (and reading negative one characters is simply odd). The second case should read nothing, since it's supposed to stop when 0 characters have been read.

The last case (input line contains max-1 characters followed by the newline) is perhaps the most interesting. If you've already read the max-1 characters, should you look ahead to see if there's a newline? My decision was not to read any more, particularly since that's easier to implement.

Many of you put readLine and main in the same file. I would have much preferred that you used separate files. (I did not, however, penalize you for this design strategy.)

Grading

I took off one or two points for each relatively minor error I encountered (stylistic or logical). Here are some of the errors I encountered.

I took off more if your code would actually fail to work correctly on some examples.

I was feeling somewhat mellow, so I did not penalize you for bad preconditions and postconditions. Note that you should have made these speak to the status of file (open for reading), the relationship of max to str (str can hold at least max characters), and such.

Problem 2: Reading Lines, Revisited

Rather than reading into an existing string, it is often useful to build a new string for each line read. Assume that the values are input in such a way that each line begins with an integer that gives the number of characters in the rest of the line. For example,

5Hello
16Once upon a time
0
4SamR

Write a rdln(file) procedure that allocates, reads, and returns a string that corresponds to the text on the current input line.

For example

  char *alpha;
  char *beta;
  alpha = rdln(file);
  beta = rdln(file);
  printf("Alpha: '%s'; Beta: '%s'\n", alpha, beta);

A Solution

rdln.h
/*
 * File:
 *   rdln.h
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003
 * Summary:
 *   Declarations of procedures provided by rdln.c
 */

#ifndef _RDLN_H_
#define _RDLN_H_

#include <stdio.h>

/*
 * Procedure:
 *   rdln
 * Parameters:
 *   file, a pointer to a FILE
 * Purpose:
 *   Read one line from file.
 * Produces:
 *   str, a string.
 * Preconditions:
 *   file is open for reading.
 *   The cursor in file is positioned before a digit.
 *   The current line is of the form 0\n or [0-9]*[^0-9].*\n
 *     where the digits at the start of the line give the number
 *     of remaining characters on the line.
 *   file represents an ASCII file.
 * Postconditions:
 *   The cursor is now positioned at the start of a new line.
 *   str is a newly-allocated string of the specified length.
 *   str contains the characters after the initial digits.
 *   str is terminated by 0.
 */
extern char *rdln(FILE *file);

#endif /* _RDLN_H_ */
rdln.c
/*
 * File:
 *   rdln.c
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003.
 * Summary:
 *   An implementation of the rdln procedure from CSC195 Exam 2.
 */

/********************************************************************* 
 * Headers *
 ***********/

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include "rdln.h"


/********************************************************************* 
 * Exported Procedures *
 ***********************/

/* See rdln.h for documentation. */
char *rdln(FILE *file)
{
  int i;	/* Counter variable. */
  int len=0;	/* Length of the string. */
  char *str;	  char *str;	/* The string to be read. */
  int ch;	/* Temporary character for reading. */
  /* Determine the length of the string. */
  for (ch=getc(file); isdigit(ch); ch=getc(file))
    len = len*10 + (ch - '0');      len = len*10 + (ch - '0');  /* Assume we're using ASCII. */   
  /* Undo the extra character read. */
  ungetc(ch, file);
  /* Allocate the string. */
  str = malloc(len+1);
  /* Sanity check. */
  if (str == NULL) return NULL;
  /* Read the characters. */
  for (i = 0; i < len; i++)
    str[i] = (char) getc(file);
  /* Drop the newline. */
  getc(file);
  /* Add the end-of-string mark. */
  str[len] = '\0';
  /* That's it, we're done. */
  return str;
} /* rdln(FILE *) */

Notes

A number of you checked for end-of-line and end-of-file at every step in the reading. The intent of this procedure was that you didn't need to check (that's what the preconditions should specify).

Grading

In general, if you repeated mistakes from problem 1 here (e.g., deciding to assign the result of getc to a character rather than an integer), I tried not to take off again.

Here are some of the errors I noticed:

Problem 3: Specifying Insertion

A typical insertion sort procedure for arrays of integers steps through the indices of the array, inserting each value into the sorted subarray of previous values. That is, insert(arr, pos), inserts the value at position pos in the subarray at positions 0 through pos-1, expanding that subarray into position pos.

For example,

void insertionSort(int values[], int len)
{
  int i;
  for (i = 0; i < len; i++)
    insert(values, i);
} /* insertionSort(int[], int) */

Document as carefully as you can the preconditions and postconditions for insert.

A Solution

Procedure
insert
Parameters
arr, an array of integers.
pos, an integer.
Purpose
Insert the value at position pos into the subarray at positions 0 .. pos-1.
Produces:
[Nothing]
Preconditions
1. 0 <= pos < length(arr)
2. The first pos values in arr are sorted in increasing order. That is, arr[i] <= arr[i+1] for 0 <= i < pos-1.
Postconditions:
1. The first pos+1 values in arr are sorted in increasing order.
2. The first pos+1 values in arr are a permutation of the first pos+1 values in arr immediately before insert was called.

If we were more formal, we'd write something like the following:

Preconditions:
(0 <= pos < length(arr))
and (for all i in [0..pos-2], arr[i] < arr[i+1])
and (arr[0..pos] = A)
and (pos = P)
Postconditions:
(pos = P)
and permutation(arr[0..pos], A)
and (for all i in [0..pos-1], arr[i] < arr[i+1])

Grading

Problem 4: Partitioning Vectors

Here is an incorrect implementation of the partition procedure that Quicksort typically uses. It assumes that an appropriate pivot has been put in values[lb]. It returns the position in which it places the pivot.

int swap(int values[], int i, int j)
{
  int tmp = values[i];
  values[i] = values[j];
  values[j] = tmp;
} /* swap(int[], int, int) */

int partition(int values[], int lb, int ub)
{
  int start = lb;
  int pivot = values[lb++];
  while (lb <= ub) {
    while (values[lb] <= pivot) 
      ++lb;
    while (values[ub] > pivot)
      --ub;
    swap(values, lb, ub);
    ++lb;
    --ub;
  } /* while */
  swap(values, start, lb);
  return lb;
} /* partition(int[], int, int) */

a. What's wrong with this implementation of partition? Note that you cannot just present a correct implementation; you must identify flaws in the implementation.

A: Some Problems

b. How might (or did) careful use of preconditions and postconditions have helped identify those errors? Be as specific as you can.

B: Using Assertions

Here's an attempt to add assertions at various stages. These assertions let us identify errors. The key assertion has to do with the relationship between the different parts of the array.

int partition(int values[], int lb, int ub)
{
  int start = lb;
  int end = ub;
  int pivot = values[lb++];
  /* 
     values[start..lb-1] <= pivot < values[ub+1 .. end] 
     values[start] = pivot
     start <= lb,ub, <= end
   */
  while (lb <= ub) {
    while (values[lb] <= pivot) 
      ++lb;
    /* 
       values[start..lb-1] <= pivot < values[ub+1 .. end] 
       values[start] = pivot
       values[lb] > pivot
       start <= lb,ub, <= end -- NOPE; lb could be after end.
       Assume we've fixed that problem.
     */
    while (values[ub] > pivot)
      --ub;
    /* 
       values[start..lb-1] <= pivot < values[ub+1 .. end] 
       values[start] = pivot
       values[lb] > pivot
       values[ub] <= pivot
       start <= ub < end
     */
    swap(values, lb, ub);
    /* 
       values[start..lb] <= pivot < values[ub .. end] 
       values[start] = pivot - Whoops, what if ub was pivot?  Error
       start <= ub < end -- We know this b/c values[start] = pivot
     */
    ++lb;
    --ub;
    /* 
       values[start..lb-1] <= pivot < values[ub+1 .. end] 
       values[start] = pivot
       start <= ub < end - Nope.  If ub was start, we have trouble.
     */
  } /* while */
  swap(values, start, lb);
  return lb;
} /* partition(int[], int, int) */

Problem 5: Java vs. C

Suppose you were asked to teach C to a Java programmer. What two key differences between C and Java would you most emphasize? Argue that those are the appropriate ones to emphasize.

You should write about one paragraph (at least three sentences, no more than ten) for each difference.

Problem 6: Exponentiation

Here's a recursive way to think efficiently about exponentiation.

x0 = 1
x2k = (xk)2
xn+1 = x*xn for even n

Implement this efficient exponentiation procedure iteratively in C. The running time of your algorithm should be in O(log2n).

A Solution

There are a few key ideas in building the iterative solution. The first is the observation that x2k = (x2)k, which means that we can square the value and halve the power when the power is even. The second is that we need to use an extra variable to keep track of the extra parts to multiply.

In C code we get the following,

/*
 * File:
 *   expt.c
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003
 * Summary:
 *   An implementation of the logarithmic time exponentiation
 *   procedure.
 */

#include "expt.h"

/*********************************************************************
 * Exported Procedures *
 ***********************/

double expt(double val, int power) {
  double result = 1;
  double x = val;
  int n = power;
  /* result*x^n == val^power */
  while (n > 0) {
    if (n % 2 == 0) {
      x = x*x;
      n = n/2;
      /* result*x^n == val^power */
    }
    else {
      result = result * x;
      n = n-1;
      /* result*x^n == val^power */
    }
  } /* while */
  /* result*x^n == val^power */
  /* n = 1. */
  /* Therefore, result = val^power. */
  return result;
} /* expt(double, int) */

Testing

How should we test this procedure? Once again, I've built a simple and straightforward front end. To test, type the base and the power on the command line.

How do we know the answer is right? For simple examples, like 23, we can check the answer by hand. For more complex examples, I've added the exp/log computation we discussed as a check.

/*
 * File:
 *   testexpt.c
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003.
 */

/*********************************************************************
 * Headers *
 ***********/

#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include "expt.h"


/*********************************************************************
 * Main *
 ********/

main(int argc, char *argv[]) {
  double value;
  int power;

  /* Sanity check. */
  if (argc != 3) {
    fprintf(stderr, "Usage: %s val power.\n", argv[0]);
    exit(EXIT_FAILURE);
  }
  
  /* Get the important values. */
  value = strtod(argv[1], NULL);
  power = (int) strtol(argv[2], NULL, 10);
  /* Do the computation and print the result. */
  printf("%f^%d = %f\n", value, power, expt(value,power));
  printf("e^(log(%f)*%d) = %f\n", value, power, 
         exp(log(value)*power));
  exit(EXIT_SUCCESS);
} /* main(int, char **) */

I use the following lines in my Makefile to compile it.

testexpt: testexpt.o expt.o
        $(CC) -lm -o testexpt testexpt.o expt.o

Note that the solution we've written is likely to be more accurate. Try computing 250 and see what you get.

Notes

Since integers provide a relatively small range, I expected you to use double (or at least long) for the type of base value (the x in xn).

The recursive algorithm given is O(log2n). I therefore expected an O(log2n) solution. Many of you wrote O(n) solutions.

Grading

I did not take off for choosing int for the type of the base value.

I took off six (6) points for writing a linear solution, since that ignored the main focus of the problem.

Problem 7: How Complex is Complex?

Part A: The Type

a. Define a complex struct that contains the two parts of a complex number (that is, a real part and an imaginary part).

complex.h
/*
 * File:
 *   complex.h
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003
 * Summary:
 *   Type and function declarations for a simple complex type.
 */

#ifndef _COMPLEX_H_
#define _COMPLEX_H_

/* Complex numbers contain a real and imaginary part. */
typedef struct complex {
  double real;
  double imaginary;
} complex;

/*
 * Procedure:
 *   cmult
 * Parameters:
 *   a, a complex number
 *   b, a complex number
 * Purpose:
 *   Multiply a and b.
 * Produces
 *   c, a complex number.
 * Preconditions:
 *   [Standard]
 * Postconditions:
 *   c = a*b
 */
extern complex cmult(complex a, complex b);

#endif /* _COMPLEX_H_ */
Notes

A number of you made both components of the value integers. It's probably a better idea to make them some form of floating-point number (probably double values).

B. Multiplying Complex Numbers

b. Document, implement and test a cmult procedure, which multiplies two complex numbers and returns their product.

complex.c
/*
 * File:
 *   complex.c
 * Author:
 *   Samuel A. Rebelsky
 * Version:
 *   1.0 of March 2003.
 * Summary:
 *   A few simple procedures to support computation with complex
 *   numbers.
 * Contents:
 *   complex cmult(complex a, complex b)
 *     Compute and return the produce of a and b.
 */

/*********************************************************************
 * Headers *
 ***********/

#include "complex.h"

/*********************************************************************
 * Exported Procedures *
 ***********************/

complex cmult(complex a, complex b) {
  complex result;
  result.real = a.real*b.real - a.imaginary*b.imaginary;
  result.imaginary = a.real*b.imaginary + b.real*a.imaginary;
  return result;
}
  

Problem 8: Copying Strings

Implement strncpy as concisely as you can. You may not call the built-in strncpy.

A Solution

Notes

I assumed that you would look at the man page for strncpy. That page reads, in part,

The strcpy() function copies the string pointed to by src (including the terminating `\0' character) to the array pointed to by dest. The strings may not overlap, and the destination string dest must be large enough to receive the copy.
The strncpy() function is similar, except that not more than n bytes of src are copied. Thus, if there is no null byte among the first n bytes of src, the result will not be null-terminated.
In the case where the length of src is less than that of n, the remainder of dest will be padded with nulls.
Free Software Foundation (2001). strcpy and strncpy man page. In Linux Man pages, designated as a GNUpage. Dated 1993-04-11.

What are the key parts of this definition?

Grading

Problem 9: Weakest Preconditions of Sequences

Programmers typically assume that it doesn't matter whether the sequence S1 ; S2 ; S3 is interpreted as (S1 ; S2) ; S3 or as S1 ; (S2 ; S3). Gries even makes a statement to this effect in his discussion of the weakest precondition of instruction sequences.

Verify this assertion by showing that wp(((S1 ; S2) ; S3), P) is the same as wp((S1 ; (S2 ; S3)), P) .

A Solution

Gries definition 8.3 on p. 115 tells us that

wp(S1;S2,P) = wp(S1, wp(S2, P))

So, let's see what we can compute for the two things we want to prove equivalent.

wp((S1;S2);S3, P)
= wp(S1;S2, wp(S3, P)) [by 8.3]
= wp(S1, wp(S2, wp(S3, P)) [by 8.3]

wp(S1;(S2);S3, P)
= wp(S1, wp(S2; S3), P)
= wp(S1, wp(S2, wp(S3, P)))

Since the two things are equal to equal things, they are equal.

Notes

Yes, the proof is amazingly simple. This problem was intended to be a simple reminder that (1) some proofs are, inf fact, easy; and (2) it's helpful to check some of Gries's assertions.

 

History

Sunday, 30 March 2003 [Samuel A. Rebelsky]

 

Disclaimer: I usually create these pages on the fly, which means that I rarely proofread them and they may contain bad grammar and incorrect details. It also means that I tend to update them regularly (see the history for more details). Feel free to contact me with any suggestions for changes.

This document was generated by Siteweaver on Fri May 2 14:19:08 2003.
The source to the document was last modified on Sun Mar 30 19:33:58 2003.
This document may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CS195/2003S/Exams/notes.01.html.

You may wish to validate this document's HTML ; Valid CSS! ; Check with Bobby

Samuel A. Rebelsky, rebelsky@grinnell.edu