CS140 Final Exam - December 10, 2019. Answers and Grading

James S. Plank

Question 1: 27 Points

Grading:

1.5 points per part. (27 points total). In the grading file that you receive, I used the following abbreviations:

t     for True
f     for False
1     for O(1)
ln    for O(log n)
n     for O(n)
nln   for O(n log n)
ns    for O(n^2)
nsln  for O(n^2 log n)
15    for O(15n^2)
15+   for (O(15 n^2 + 400n + 5log n)
ns+   for O(n^2 + n + log n) 
400   for O(400n)
5     for O(5 log n) 
pre   for Preorder 
post  for Postorder 
in    for Inorder 
55    for O(55n)
55+   for O(55n + 4n log(n) + 20log n) 
55-   for (O(55n + n log(n) + log(n))
4     for O(4 n log n) 
20    for O(20 log n)
lm    for O(log m)
lnlm  for O((log n)(log m)) 
m     for O(m)
nm    for O(nm)
nlm   for O(n log m)
mln   for O(m log n)
mlm   for O(m log m)


Question 2: 20 Points

This question requires that you identify the running times of the various loops and data structures:

Grading: 2 points per correct answer. The abbreviations in the grading file are roughly the same as above. If you saw "npx", it meant "n to the power of x".


Question 3: 20 Points

Grading: 2.5 points per answer.

Question 4: 17 Points

Straight from the lecture notes:

void Queue::Push(const string &s)
{
  Qnode *newnode;

  newnode = new Qnode;      // Create the new node. 
  newnode->s = s;
  newnode->ptr = NULL;

  if (last == NULL) {       // If the queue is empty, set first to be this new node. 
    first = newnode;  
  } else {                  // If the queue is non-empty, set the pointer of the last node to be this new node. 
    last->ptr = newnode; 
  }


  last = newnode;           // Finally, set last to point to the new node, and increment size.

  size++;
}

string Queue::Pop()
{
  Qnode *oldfirst;
  string rv;

  if (size == 0) throw((string) "Bad pop");

  /* Move "first" to point to the next node, store the return value, and
    delete the previous first node. */

  rv = first->s;
  oldfirst = first;            
  first = oldfirst->ptr;
  delete oldfirst;

  /* Handle the empty queue. */

  if (first == NULL) last = NULL;

  /* Update size and return. */

  size--;
  return rv;
}

Grading:

Each method was worth 8.5 points. Typically, you started with 8.5 and then received deductions. Common deductions:


Question 5: 16 Points

As the writeup says, there are two parts of this -- building v, and then writing rec_children(). I'm going to start with rec_children(). It will have the following prototype:

int rec_children(const vector <int> &v, int index);

It will return maximum number of children of any subtree the tree that starts at index. As with all recursive procedures, this needs a base case -- that should be when the tree is a single node. How do we figure that out? If v[index] is equal to index+1. In that case, we return zero.

If the tree is not a single node, then it has subtrees. We need to call rec_children() on all of the subtrees, recording the maximum return value. At the same time, we can count the children. We return the maximum value. How do we identify the subtrees? Well:

Here's the code for rec_children():

int rec_children(const vector <int> &v, int index)
{
  int recursive_answer, nc, a;
  int i;

  if (v[index] == index+1) return 0;    // Base case -- if it's a single node, return 0.
 
  nc = 0;                                           // This will hold the number of children.
  recursive_answer = 0;                             // This will hold the maximum answer for all children.

  for (i = index+1; i != v[index]; i = v[i]+1) {    // Traverse the subtrees
    nc++;                                           // Use nc to count the number of children.
    a = rec_children(v, i);                         // Compute the max children of each subtree.
    if (a > recursive_answer) recursive_answer = a;
  }
  if (nc > recursive_answer) recursive_answer = nc;
  return recursive_answer;                          // Return the max of children and answers for the children.
}

Now, the code for max_children() has to create the vector v and then call rec_children(). To create the vector v, we use a stack (I use a deque for that) data structure. Whenever we see a left paren, we push its index onto the stack. When we see a right parent, we pop its corresponding left paren off the stack, and use that pairing to create v. Here's the code:

int max_children(const string &s)
{
  vector <int> v;
  deque <int> st;
  size_t i, j;

  v.resize(s.size(), -1);                          // Create v by using st as a stack
  for (i = 0; i < s.size(); i++) {
    if (s[i] == '(') {                             // Push the index onto the stack on '('
      st.push_front(i);
    } else {                                       // Otherwise pop the index of the corresponding '('
      j = st[0];
      st.pop_front();
      v[j] = i;
    }
  }
  return rec_children(v, 0);                       // The answer is rec_children, starting at index 0
}

I've put a main() onto this code in q5.cpp, so that you can test it. It takes the parameter string on the command line.

The majority of you did not use a stack, but instead did something like:

This is an O(n2) algorithm, whereas using a stack is O(n). For that reason this answer started at 5 points rather than 8.

Grading:

I typically started at 8 points for rec_children, and then took off deductions. Common deductions:

For max_children(), you either started at 8 points or 5 points, depending on whether you used the stack solution or the O(n2) solution above. Common deductions: