CS140 Final Solutions

Spring 2015


  1. (8 points) Show the binary search tree that results if 300 is deleted from the tree below:
                               ---100---
                              /         \
                             50        -300-
                            /         /     \
                           25       125     400
                     	       /   \       \
                           	     110   200     500
                                 	      \
    			     	      250
    				     /   
    			           225   
    
    To delete 300 from this tree we must find the largest child in the left subtree, which is 250, and delete it. We will then replace 300 with 250. 250 has a single child, so to delete 250, we replace 250 with this child, producing the tree:
                               ---100---
                              /         \
                             50        -300-
                            /         /     \
                           25       125     400
                     	       /   \       \
                           	     110   200     500
                                 	      \
    			     	      225
    
    Then we replace 300 with 250, producing the final result:
                               ---100---
                              /         \
                             50        -250-
                            /         /     \
                           25       125     400
                     	       /   \       \
                           	     110   200     500
                                 	      \
    			     	      225
    
    Incorrect but worth 5 points: The course notes told you to delete the largest child from the left subtree and replace the deleted key with this child. If you instead deleted the smallest child from the right subtree, and replaced the deleted key with this child, you could get 5 points of partial credit. 400 is the smallest child in the right subtree, so you first delete 400. Since 400 has a single child, you replace 400 with 500, and you replace 300 with 400. The final tree in this case is:
                               ---100---
                              /         \
                             50        -400-
                            /         /     \
                           25       125     500
                     	       /   \     
                           	     110   200   
                                 	      \
    			     	      250
    				     /   
    			           225   
    
    

  2. (6 points) Show the result of doing a single right rotation about the node 175. Do not worry if the rotation increases the height of the tree. All I care about is whether you know how to perform a rotation.
                                -300-
                               /     \
                             175	 400
                               \
    			   250
    		          /   \
    			200   275
    
    The right rotation will cause 300 to become a right child of 175. In so doing the right subtree rooted at 250 will become an orphan because 300 is taking its place as 175's right child. Therefore 300 adopts 250 as its left child while keeping 400 as its right child. Note that it is ok for 300 to adopt 250 as its left subtree because all of the values in 250's tree are less than 300. The final tree becomes:
                          175
                             \   
                             300
                            /   \
    		      250   400
    		     /   \
    		   200   275
    

  3. (12 points) 12 has just been inserted into the following AVL tree, causing it to violate the AVL condition:
                              20
                            /    \
                          10     40
                        /    \
                       8     16
                            /
                           12
    

  4. (6 points) What is the Big-O running time for the following functions?

    1. O(n2): T(n) = 5n2 + 10n * log n + 1000n
    2. O(1): T(n) = 1000000
    3. O(2n): T(n) = 100n3 + 20n2 + 100 + 2n

  5. (2 points) If you had three programs with the above running times, which one would you prefer if you were unsure about your input size but wanted to assume the worst (i.e., that the input sizes could be fairly large). Circle one of the following three letters to denote which program you would choose:
         a        b        c
    

    b: as n gets large, the constant time program will be the fastest. Since we are unsure about the size of the input and are assuming the worst, we must assume that the input can be fairly large. Hence we prefer the constant time program, even though it has the largest constant.

  6. (8 points) Behold the following 4 fragments of code:
    (a)
    int a, b; int sum; cin >> a; cin >> b; sum = a + b; cout << sum << endl;
    (b)
    for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { sum = 0; for (k = 0; k < n; k++) { sum += matrix1[i][k] * matrix2[k][j]; } result[i][j] = sum; } }
    (c)
    sum = 0 for (i = n; i >= 1; i = i/2) { sum += a[i]; }
    (d)
     for (i = 0; i < n; i++) {
         min[i] = a[i][j];
         for (j = 0; j < n; j++) {
             if (a[i][j] < min[i]) {
                  min[i] = a[i][j]
             }  
         }
     }
     for (i = 0; i < n; i++) {
         cout << min[i] << endl;
     }
         

    For each fragment of code, please circle its Big-O running time:

    1. O(1): There are no loops or functions in this code fragment, so it is constant time.

    2. O(n3): This code fragment has 3 nested loops, each of which iterates n times. The innermost loop's body has roughly 3 statements--the multiplication, the comparison k < n, and the increment, k++. It iterates n times, so the rough number of instructions for the inner loop is 3n. The number of instructions per iteration for the middle loop is roughly 3n + 4, which we obtain as follows:

      • The comparison, j < n is one instruction.
      • The increment, j++ is one instruction sum = 0 is one instruction
      • The inner loop is 3n instructions
      • The instruction result[i][j] = sum is one instruction
      The middle loop has n iterations, and hence the total instructions for the middle loop is n * (3n + 4) or 3n2 + 4n.

      Finally each iteration of the outermost loop has the increment and compare instructions, totaling 2 instructions, plus an execution of the middle loop, totaling 3n2 + 4n. The total instructions for each iterations is thus 3n2 + 4n + 2. There are n iterations of the outer loop, and hence the total number of instructions for the outer loop is n * (3n2 + 4n + 2), which is 3n3 + 4n2 + 2n. With Big-O notation we throw away all but the biggest term, which is 3n3, and then we throw away the leading constant, thus obtaining O(n3).

    3. O(log n): The loop halves the loop variable at each step, which means the loop executes log n times. Thus the running time of the code fragment is O(log n).

    4. O(n2): This code fragment has two loops that execute sequentially. The first loop is a doubly nested loop. Its inner loop has a single if statement. We must pessimistically assume that the condition is always true and that the if branch executes. The condition counts as 1 instruction and the assignment as a second instruction. The loop check and loop increment each count as an additional instruction, so the inner loop body executes 4 instructions on each loop iteration. The inner loop executes n times, and hence requires 4n instructions. The outer loop body has 4n + 3 instructions--4n for the inner loop, 2 for the loop condition and increment, and 1 for the initialization of min[i]. The outer loop body executes n times, for a total of n * (4n + 3) or 4n2 + 3n instructions.

      The second loop in the sequence prints the min array. Its loop body has a single cout statement, which we count as 1 instruction, and the loop increment and loop condition add 2 additional instructions. Hence the loop body requires 3 instructions. The loop executes a total of n times and hence takes 3n instructions.

      When loops run sequentially as in this code fragment, we add the running times of the loops to get the running time of the code fragment. Adding the running times of the two loops gives us 4n2 + 6n instructions. Since Big O notation only cares about the biggest term and strips its leading constant, we end up with a Big O running time of O(n2).

  7. (10 points) For each of the following questions give the average case and worst case running time for each operation on the specified data structure. Please use Big-O notation. For example, the average case and worst case running time for appending an element to the end of a list is O(1) and the average case and worst case running time for finding an element in an ordered list is O(n).

    Operation Average Case Worst Case
    Inserting an element into a hash
    table that uses separate chaining
    O(1)O(n)
    Finding an element in a binary search tree O(log n)O(n)
    Inserting an element into an AVL tree O(log n)O(log n)
    Adding an element to the front of a vector O(n)O(n)
    Removing an element from the top
    of a stack (i.e., pop)
    O(1)O(1)

    A vector requires O(n) time to add an element to the front of the vector because all existing elements must be moved 1 element to the right.

  8. (10 points)
     
         a. array
         b. vector
         c. stack
         d. deque
         e. hash table
         f. list
         g. binary search tree
         h. AVL tree
         i. BTree
    

    For each of the following questions choose the best answer from the above list. Assume that the size of an array is fixed once it is created, and that its size cannot be changed thereafter. Sometimes it may seem as though two or more choices would be equally good. In those cases think about the operations that the data structures support and choose the data structure whose operations are best suited for the problem. You may have to use the same answer for more than one question:

    1. array The most time efficient data structure you could use to implement a stack in which there is an upper limit, max, on the number of elements that can be stored on the stack.

      If you know the number of elements in advance, then you can pre-allocate an array and it will be more efficient then either a vector or a linked list.

    2. vector The most tim efficient data structure you could use to implement a stack in which the the number of elements that can be stored on the stack is unlimited.

      If you don't know the number of elements in advance, then you have to use either a vector or a linked list. The vector is more efficient because you can store the elements contiguously in memory rather than linking them using pointers.

    3. binary search tree The data structure you should use if you want an in memory tree to store keys and the keys to be inserted are inserted in a random order.

      If elements are inserted in random order, then you can use a binary search tree since on average the tree will be balanced and the operations will require O(log n) time. Although binary search trees and AVL trees have the same Big O running time, binary search trees are faster when the trees are naturally balanced since they do not waste time on either checking for balance or rebalancing the tree.

    4. AVL tree The data structure you should use if you want an in memory tree to store keys and the keys to be inserted are inserted in a nearly sorted order.

      If the keys are inserted in a nearly sorted order, then a binary search tree is likely to be lopsided and have its worst case running time of O(n) for insert, delete, and find. In this case we need to use an AVL tree to rebalance the tree after each insertion in order to guarantee an O(log n) running time.

    5. stack The data structure used to manage the frames associated with each function (i.e., the frame that gets created when a function is called to store its parameters and local variables, and then gets destroyed when the function returns).

  9. (8 points) You are given the following B-tree of order 5. Show the new btree that results when 375 is inserted into it.

    When 375 gets inserted into the tree, it gets inserted into the leaf node containing the keys 325, 350, 400, and 425. Since the B-tree is of order 5, nodes can only hold 4 children. Hence we must split the leaf node and promote the middle element, which is 375, to the parent node. When we promote 375 to the parent node, it also overflows and must be split in two. We promote the middle element, which is 450, and it becomes the new root of the tree. The new tree is shown below:


Final Coding Questions

  1. (10 points--CS140Sp15-Final-Recursion): Write a recursive function named palindrome that determines if a string is a palindrome, that is, it is equal to its reverse. For example, "racecar", "g", and "tattarrattat" are palindromes. The function should return a bool with true indicating that the designated portion of the string is a palindrome and false otherwise.

    // On the exam s was passed by-value but it should have been passed by
    // reference since it is an object
    bool palindrome(string &s, int begin, int end) {
       // base case occurs when begin and end "cross over"
       if (begin >= end)
          return true;
    
       // if the outer characters are equal, then check the rest of the string
       else if (s[begin] == s[end])
          return palindrome(s, begin+1, end-1);
       else
          return false;
    }
    
  2. (20 points--CS140Sp15-Final-Tree): Write a recursive method named recursive_size that takes a binary tree node as a parameter and returns the number of nodes in the binary search tree rooted at that node. recursive_size should return 0 if the node is a sentinel node and otherwise should return the sum of the number of nodes in the left and right subtrees, plus 1 for the node itself.
    int BSTree::recursive_size(BSTNode *n) {
      if (n == sentinel)
        return 0;
      else {
        int leftSize = recursive_size(n->left);
        int rightSize = recursive_size(n->right);
        return leftSize + rightSize + 1;
      }
    }
    
  3. (20 points--CS140Sp15-Final-Queue) Write a method named Delete for the singly linked Queue class presented in class lecture notes. Delete takes a string and deletes the node from the queue that contains this string. Delete should return true if the string is in the Queue and false if string is not in the queue. If the key is in the Queue, then Delete should delete its node, make the node preceding the deleted node point to the node succeeding the deleted node, and update the first and last pointers in the queue.
    bool Queue::Delete(string key) {
      Qnode *currentNode, *prev;
      
      // As we move through the queue searching for the node to delete, prev
      // always points to the previous node that we visited. If we find a node
      // that contains the string, then we make this previous node point to
      // whatever the deleted node used to point to.
      prev = NULL;
      for (currentNode = first;  currentNode != NULL; currentNode = currentNode->next) {
        // if we have found the node that contains the string, first check to
        // see if we are deleting the first node in the list. 
        if (currentNode->s == key) {
          if (currentNode == first) {
    	first = currentNode->next;
            // if first is now NULL, then the list had only one element and
    	// therefore is now empty. Hence we must set last to be NULL as well
    	if (first == NULL)   
    	  last = NULL;
          }
          // else we are deleting a node from somewhere in the middle of the list
          else {
    	prev->next = currentNode->next;
            // if we are deleting the last node in the list, then we need to
            // update the last pointer to point to prev
    	if (last == currentNode)
    	  last = prev;
          }
          delete currentNode;
          return true;
        }
        else {
          prev = currentNode;
        }
      }
      // if we don't find the string in the loop, then it's not in the queue
      return false;   
    }