CS140 Midterm 2 Solutions

Fall 2016


  1. (14 points) For each of the following questions circle the best answer from the above list.
     
         a. vector
         b. list
         c. deque
         d. map
         e. set
         f. multimap
         g. multiset
         h. hash table
    

  2. (11 points) Suppose I have the following declarations and code:
         int a, b, c;
         int *p, *q, *r;
    
    Also suppose that the above variables are assigned the following memory addresses:
         a: 0x1000
         b: 0x1004
         c: 0x1008
         p: 0x100c
         q: 0x1010
         r: 0x1014
     
    What values are printed by the following two cout statements. Note that the first cout statement prints p and q whereas the second cout statement prints *p and *q.
           p = &c;
           *p = 20;
           p = &a;
           *p = 40;
           q = p;
           b = *q + *p;
           cout << a << " " << b << " " << c << " " << p << " " << q << endl;
    
           a = 60;
           b = 20;
           c = 70;							    
           r = &b;
           b = a + c;		    
           p = new int;
           *p = 2;
           q = new int;
           *q = a / *p;
           cout << a << " " << b << " " << c << " " << *p << " " << *q << " " << *r << endl;
    
    cout 1:                      cout2:
    
         a: 40                     a: 60
    
    
         b: 80                     b: 130
    
    
         c: 20                     c: 70
    
    
         p: 0x1000                 *p: 2
    
    
         q: 0x1000                 *q: 30
    
    
                                   *r: 130
    
    

  3. (6 points) For each of the following definitions, select the term from the following list that best fits that definition.
     a. memory leak           b. dangling pointer     c. seg fault
        
    1. c Occurs when you try to de-reference a NULL pointer (e.g., p = NULL; *p = 20).

    2. a Occurs when you forget to delete a heap-allocated object when there are no longer any pointers to that object (i.e., you can no longer reference that object).

    3. b Occurs when you pre-maturely de-allocate a heap-allocated object when there are still pointers to that object (i.e., when parts of the program may still try to reference that object).

  4. (10 points) Explain what is inefficient about the following code segment and how you would fix it. The code segment is supposed to use linear probing to find a key in a hash table. It should return the index of the key if it is found in the hash table and -1 if the key is not in the hash table. The keys are guaranteed to be non-negative integers. A hash table entry stores one of three values: 1) a key, 2) a value of -1 to denote that the entry has never stored a key, and 3) a value of 0 to denote that the entry is currently empty but once stored a key. A sample hash table might be:

    012345678910
    -1122304835-1-130-132

    int find(vector<int> &hashTable, int key) {
        unsigned int bucket = key % hashTable.size();
        int i;
    
    1    for (i = bucket; i != bucket-1; i = (i+1) % hashTable.size()) {
    2        if (hashTable[i] == key) {
    3            return i;
    4        }
    5    }
    6    return -1;
    }
    
    1. In four sentences or left, describe why the above find function is inefficient (i.e., although this code returns the correct result, it does not faithfully implement the find algorithm for linear probing).

      If the key is not in the table, then find performs a linear search of the table, which is an O(n) operation. It should stop as soon as it reaches a bucket that is empty and has never been occupied.

    2. Fix the inefficiency by writing an appropriate code segment and indicate where you would insert it (i.e., after which line number).

      Add the following code segment after line 4:

      else if (hashTable[i] == -1) {
          return -1;
      }
      	

  5. (9 points) Your boss has asked you to choose between two algorithms. The two algorithms have the following running times:
    	  Algorithm 1: T(n) = 1000n*log2n + 20n + 3
    	  Algorithm 2: T(n) = 0.5n2 + 50n + 8
            

    1. What is the Big-O running time of Algorithm 1? O(n*log2n)

    2. What is the Big-O running time of Algorithm 2? O(n2)

    3. The size of the input will be typically quite large, on the order of n > 1000. Which of the two algorithms should you choose and why?

      You should choose Algorithm 1 because for large n, Algorithm 1's running time of n*log2n is less than Algorithm 2's running time of n2.

  6. (10 points) You are told that the running time of an O(n) algorithm for a certain value of n is 8 seconds.
    1. How long would you expect an O(log2 n) algorithm to take for the same value of n? log2 8 = 3 seconds
    2. How long would you expect an O(n2) algorithm to take for the same value of n? 82 = 64 seconds
    If you forget how to compute log2n, it is the value of x, where 2x = n.

  7. (10 points) In the Coke lab, one of your functions had to delete a user. One of the tasks this function had to perform was to remove all of the user's phones from the Phone map. To refresh your memory, here are the relevant data structures:
    class User {
      public:
         string username;
         string realname;
         int points;
         set <string> phone_numbers;
    };
    
    map <string, User *> Phones;
    
    Here are two code segments that correctly remove all of the user's phones from the Phones map:
    (a)(b)
    void deletePhones(User *u) {
       set::iterator phone_it;
       for (phone_it = u->phone_numbers.begin();
             phone_it != u->phone_numbers.end();
             phone_it++) {
          Phones.erase(Phones.find(*phone_it));
       }
    }	
    
    void deletePhones(User *u) {
       map<string, User*>::iterator phone_it;
    
       // phone_it is correctly advanced in the loop body, which is
       // why there is no phone_it++ as the third expression in the for header
       for (phone_it = Phones.begin();
             phone_it != Phones.end(); ) {
    
          if (phone_it->second == u) { 
             phone_it = Phones.erase(phone_it);
          }                                   
          else {                        
             phone_it++;
          }
      }   
    } 
    

    Make the following assumptions:

    Answer the following questions:

    1. Which code segment is more efficient (a or b)? a

    2. Explain why the code segment you selected is more efficient using Big-O notation. You will need to compute the Big-O running time of each of the two functions in order to justify your answer.

      Code segment a requires at most 5 iterations of its loop body and a remove from a map requires O(log n) time. Hence code segment a requires 5*O(log n) or O(log n) time.

      Code segment b requires n iterations of its loop body. Most of the iterations do nothing other than check the pointer to user, which is a constant time operation. At most 5 of the iterations will remove a phone, which requires O(log n) time. Hence the running time of the algorithm is at most T(n) = n + 5*log n, which is O(n).

      Since log n is less than n for large n, we prefer code segment a.

  8. (10 points) Draw a list diagram that shows what the following list looks like pictorially after the following code executes. Make sure that you include sentinel nodes and that you show where each list iterator points. Your diagram should look like the ones in the class notes.
    list<int> names;
    list<int>::iterator nit, nit1;
    names.push_back(45);
    names.push_back(100);
    names.push_front(80);
    names.push_front(50);
    names.push_front(200);
    nit = names.begin();
    nit++;
    nit++;
    nit1 = nit;
    nit1++;
    names.erase(nit);
    nit = nit1;
    nit++;
    names.insert(nit, 15);
    nit1 = names.end();
    
    
        X --- 200 --- 50 --- 45 --- 15 --- 100 --- X
                                            ^      ^
                                           nit    nit1
      
  9. (10 points) Assume you have the following declaration:
    class ListNode {
       public:
          string name;
          ListNode *next;
          ListNode *prev;
    
          ListNode(string n) : name(n) {}
    };
    
    Further suppose that a series of inserts have created the following list:

    1. Draw the diagram that results when the following code is executed:
      ListNode *newnode;
      
      1) newnode = new ListNode("Brad");
      2) newnode->next = currentNode;
      3) newnode->prev = currentNode->prev;
      
      4) currentNode->prev = newnode;
      5) currentNode->prev->next = newnode;
      

    2. How could you modify this code so that is correctly inserts newnode into the list? Be specific by identifying a) which line(s) of code are causing the problem, and b) how you would modify these lines (write the modification as C++ code).

      The problem with this code is that line 4 destroys currentNode's prev pointer to "Ben" and makes currentNode's prev pointer point to "Brad". Now when we try to make currentNode's previous node, which was "Ben", point to "Brad", we fail, because currentNode's prev pointer has already been set to point to "Brad". Hence when we try to access currentNode's previous node, we errononeously go to "Brad", and make "Brad"'s next pointer point to itself.

      Solution 1: move line 5 before line 4. If line 5 executes before line 4, then currentNode's prev pointer still points to "Ben", and "Ben"'s next pointer is correctly made to point to "Brad".

      Solution 2: To avoid these types of code ordering problems, it's best to declare a pointer variable that points to "Ben" before we start any manipulations of the list. Hence in the declaration list we should add the declaration:

      ListNode *prevNode = currentNode->prev;
      						   
      Then we can modify line 5 to read:
      prevNode->next = newnode;
      						   

Coding Solutions

  1. List problem
    list<string> *findCaps(list<string> &words) {
       list<string> *upperCaps = new list<string>();
       list<string>::iterator iter;
    
       iter = words.begin();
       while (iter != words.end()) {
          if (((*iter)[0] >= 'A') && ((*iter)[0] <= 'Z')) {
             upperCaps->push_back(*iter);
             iter = words.erase(iter);
          }
          else {
             iter++;
          }
       }
       return upperCaps;
    }
    					    

  2. Device problem:
    void Device::addCustomer(string customerName, string customerAddress) {
       Customer *newCustomer;
        newCustomer = new Customer(customerName, customerAddress);
        customerMap[customerName] = newCustomer;
    }
    
    void Device::addDevice(string deviceName, string customerName) {
       pair <set<string>::iterator, bool> deviceReturnValue;
       deviceReturnValue = devices.insert(deviceName);
       if (deviceReturnValue.second) {
           customerMap[customerName]->devices.insert(deviceName);
       }
    }
    
    void Device::printCustomers() {
       map<string, Customer *>::iterator mapIter;
       set<string>::iterator setIter;
       Customer *customer;
       for (mapIter = customerMap.begin(); mapIter != customerMap.end(); mapIter++) {
          customer = mapIter->second;
          printf("%-15s %-30s\n", customer->name.c_str(), customer->address.c_str());
          for (setIter = customer->devices.begin();
                setIter != customer->devices.end();
                setIter++) {
             printf("    %s\n", (*setIter).c_str());
          }
       }
    }