Tutorial Exercises Week 02

Exercise 1 - Function Pointers

Given the following function prototypes and assignments:
int  add (int n1, int n2);
link reverse (link ls);
void square (double *x);

 x1 = add;
 x2 = reverse;
 x3 = square;
Give the correct variable declarations for x1, x2, and x3.
Solution:
int  (*x1) (int, int);
link (*x2) (link);
void (*x3) (double *);

Exercise 2 - Stacks

Write a program which, given a string containing round, curly, and square brackets, will print 'success' if the parentheses are correctly balanced, and 'fail' otherwise.
 
 
> ./check_parens "()"
success
> ./check_parens "([])"
success
> ./check_parens "([]){}"
success
> ./check_parens "([]){"
fail
> ./check_parens "([sdfdf]ss)fdfsd"
success
Hint: the problem is similar to the postfix expression evaluation problem.
Solution
Item safePop (Stack s) {
  if (stackSize(s) == 0) {
    printf ("syntax error in postfix expression\n");
    abort ();
  } else {
    return (pop(s));
  }
}


char closing (char c) {
  if (c == '(') {
    return ')';
  } else if (c == '[') {
    return ']';
  } else if (c == '{') {
    return '}';
  } else return '\0';
}


int main (int argc, char *argv[]) { 
  Stack s;
  char *a;
  int i;
   
  if (argc < 2) {
    printf ("No argument string provided\n");
    return 0;
  }
  a = argv[1];
  
  s = createStack();
  for (i = 0; a[i] != '\0'; i++) {
    if ((a[i] == '(') || 
        (a[i] == '{') || 
        (a[i] == '[')) { 
      push(s,a[i]);
    } else if ((a[i] == ')') || 
               (a[i] == '}') || 
	       (a[i] == ']')) {
      if (closing (safePop (s)) != a[i]) {
	printf ("fail\n");
	return (0);
      }
    }
  }
    
  if (stackSize (s) == 0) {
    printf ("success\n");
  } else {
    printf ("fail\n");
  }
  return (0);
}       

Exercise 3 - Testing

What is the difference between black box and white box testing ADTs?

Solution 
Black box testing tests the functionality of the code. It focuses on the input and output of the functions
defined in the interface. It does not rely on the underlying  implementation.  It can't access the underlying
implementation. Our black box tests should still work even if we change the underlying implementation.

White bo testing te
sts the structure of the code. It relies on knowing, accessing and testing the internal
state of the code. If we changed our implementation we would need to write new white box tests. 

Exercise 4 - Blackbox testing

Imagine you have the following test file defined in a file called testStack.c and it includes the Stack.h interface from Exercise 1:
#include "Stack.h"

int main(int argc, char * argv[]){
    printf("Testing new stack\n");
    Stack s = createStack();
    assert(s->size == 0);
    printf("Test passed\n");
    return 0;
}
When compiling my test file using
gcc -Wall -Werror -c testStack.c
I get the following compile error
testStack.c:12:5: error: dereferencing pointer to incomplete type
What does this mean?

Solution
It this case we are getting the error as we are trying to use the internal structure of the ADT by saying s->size. We have no access to the underlying implementation of the stack, we we can only access the ADT by the functions and typedefs provided in the interface (.h file).

Exercise 4 - Timing

  1. You can obtain overall timing data on the execution of programs on Linux via the bash shell's built-in time command. You use it simply by preceding the command you want to run with the word time, e.g.

    $ time ./program < aBigInputFile > aBigOutputFile
    

    The output from the time command looks like:

    0.22user 0.03system 0:01.90elapsed 13%CPU (0avgtext+0avgdata 17488maxresident)k
    0inputs+13488outputs (0major+1760minor)pagefaults 0swaps
    

    Describe what the first four components on the first line (in green) are and how they are related.

  2. Solution
    For the purpose of measuring the "execution cost" of a program, we would typically use user+system.
  3. Consider the following shell script to run some timing tests:

    #!/bin/sh
    
    cat /dev/null > log
    for size in 1000 10000 100000 1000000
    do
    	for order in random sorted reverse
    	do
    		echo === Testing for $order input, size $size === >> log
    		for i in 1 2 3 4 5
    		do
    			case $order in
    			random) flag="R" ;;
    			sorted) flag="A" ;;
    			reverse) flag="D" ;;
    			esac
    			{ ./seqq $size $flag | time sort > /dev/null ; } 2>> log
    		done
    		echo "" >> log
    	done
    done
    

    The seqq prints sequences of numbers in the range 1..Max. It takes two command line parameters:

    Answer the following based on your understanding of the script:

    1. How many times will the sort command be invoked?
    2. What sequences of numbers are going to be generated by seqq?
    3. Where does the output from the script go?
    4. What does the line cat /dev/null > log do?
    5. What does the line echo "" >> log do?
    Solution
    1. The sort command is invoked once during each iteration of the innermost loop body. The outermost loop executes 4 times (for 1000..1000000). For each of its executions, the for order loop will be executed 3 times (for random, sorted, reverse). For each of the for order loop's iterations, the for i loop is executed 5 times. Total number of executions of innermost loop body = 4*3*5 = 60. So sort is executed 60 times.
    2. The first 1000 positive integers in ascending, descending and random order.
      The first 10000 positive integers in ascending, descending and random order.
      The first 100000 positive integers in ascending, descending and random order.
      The first 1000000 positive integers in ascending, descending and random order.
    3. All of the output from the script goes to a file called log.
    4. The line cat /dev/null > log creates an initially empty file. Reading from /dev/null always gives empty input.
    5. The line echo "" >> log appends an empty line to the log file, after each set of 5 runs for a given size and order combination.

Exercise 5 - Algorithmic Complexity

  1. Consider two functions f(n) and g(n) for the same task. The time cost for f(n) is time(f(n)) = 100n, while the time cost for g(n) is time(g(n)) = 2n2.

    1. Which function is faster for n=10?
    2. Which function is faster for n=20?
    3. Which function is faster for n=100?
    4. Which function is faster for n=1000?
    5. What is the crossover point where f() becomes more efficient than g()?
    1. For n=10,   time(f(n)) = 1000,   time(g(n)) = 200
    2. For n=20,   time(f(n)) = 2000,   time(g(n)) = 800
    3. For n=100,   time(f(n)) = 10,000,   time(g(n)) = 20,000
    4. For n=1000,   time(f(n)) = 100,000,   time(g(n)) = 2,000,000
    5. Crossover occurs when time(f(n)) = time(g(n)), i.e. when 100n = 2n2 With a little bit of simple algebraic manipulation n = 50 is the crossover point.

  2. Analyze the behaviour of each of the following functions, and determine their algorithmic complexity.


    a. Nested Loop
    void f3(int n) {
        int i,j;
        for (i=1; i<=n; i++) {
          for (j=1; j<=n; j++) {
           printf("hello");}
          }
        }
    }
    b. Linear Search

    // Pre: n > 0 && valid(int[n],a) && valid(int,val)
    // Post: return value = (∃ i ∈ [0..n-1], a[i] == val)
    bool found(int a[], int n, int val) {
         int i;
         for (i = 0; i < n; i++) {
             if (a[i] == val) return 1;
        }
        return 0;
    1. Nested Loop The outer loop executes n times. The inner loop executes n times. The total number of steps is n^2. Therefore, time-complexity of the algorithm is O(n^2) or quadratic. If there is a third nested loop that executes n times, then time-complexity is O(n^3).
    2. Linear search The function scans the array from start to finish and examines all n elements. Each element is compared against the value being searched for val. If val is found, the function immediately returns a true result and the rest of the array is ignored. The analysis is more complicated, because the performance can be different for different arrays and search values.
      • Best case: val occurs as the first element in the array. In this case only one comparison occurs.
      • Worst case: val is not in the array, or only occurs as the last element. In this case there are n comparisons.
      • Average case: val occurs somewhere in the middle of the array. If each value is equally likely to be searched for, then, on average, there will be n/2 comparisons.
      The most interesting case is the worst case, since it gives us an upper bound on the performance. We would normally quote the algorithmic complexity as the complexity of the worst case; in this case O(n).