Algorithms and Data Structures 2004: 2nd Term-time Test Results

The second term time test was sat on March 16th. A copy of the test can be found here. This is in postscript format. The machines in the ITL are set up to display files in this format. If you want to view postcript files from elsewhere an application called Ghostscript is available for free to do it.

Each question was marked out of 20, for a total of a maximum of 80 marks for the paper. However, for the purpose of a final mark for the whole course, the paper will be treated as if it is marked out of 75. Given that the test counts 15% of the final marks, this means that every five marks on the test will count 1% towards the final assessment of the course.

A table giving the marks for each student, but with students given only by examination number, can be found here.

Below is some discussion on the test together with solutions.

Question 1

Explain how an item is deleted destructively from an ordered binary tree. You do not have to give any Java code for it.

A complete and correct answer to this question would be something like:

A pointer is set to the root item. Then repeatedly the pointer is moved to the left branch if the item to be deleted is less than the root item of the cell the pointer is pointing to, or to the right branch if it is greater. This is done until one of the following conditions holds: the item to be deleted is in the root of the left or right branch and at least one of its branches is null, or the item to be deleted is the root item of the cell being pointed to.

If the item to be deleted is in the root of the left or right branch, the pointer to that branch is replaced by null if both of that branch's branches are null, otherwise the pointer to that branch is replaced by whichever branch of the cell it is pointing to is not null.

If the item to be deleted is found to be in a cell with non-empty left and right branches, then that item is replaced by the rightmost cell of the left branch and that cell is deleted. This means that if the left branch has no right branch, the left branch is replaced by the left branch of the left branch. Otherwise, a second pointer is put at the root of the left branch and moved down the right branches until it points to a cell whose right branch points to a cell whose right branch is null. Then the root item of the cell the original pointer points to is replaced by the root item of the cell at the root of the right branch of the cell the second pointer points to. Then the right branch of the cell the second pointer points to is replaced by the left branch of the cell the right branch points to.

As the question specifically asks for an explanation of destructive deletion, for full marks it was necessary to explain it in terms of pointer operations like this. No-one managed to get this completely correct. As in the first question of the first test, many people scored badly on this question because their answers were very vague. It is necessary in this sort of question to use language which makes it quite clear what you mean and cannot be interepreted in any other way.

However, there was nothing tricky about the question: the algorithm had been covered in detail in the lectures and the web notes, and so anyone taking this test ought to have had a fair idea it would come up and they would need to know it. The question did not require any working out or special intuition. A less detailed answer which missed the full details of pointer manipulation could still have gained about 15 marks out of the full 20.

A common mistake made was to ignore the first part, which was the search through the tree moving either to the left or right to find the location of the item to be deleted. It was essential to note this, because when we have a tree initially we have only a pointer to its root, we do not know until we have searched, where any other item is in it.

Many others, when talking about deleting an item in a cell with non-empty left and right branches did not clearly specify that it should be replaced by the rightmost item of the left branch. Some just wrote "rightmost item" (which is wrong), others wrote that it would be replaced by the item immediately below it in the tree (also wrong).

I was surprised by how many people drew trees to illustrate the problem but drew trees which were not ordered - that is they had cases where somewhere in the right branch of a tree was an item less than the root value of the tree, or somewhere in the left branch of a tree was an item greater than its root value. Marks are deducted for making an elementary mistake like this.

Question 2

Assume the existence of a class of cells for building linked lists, write a static method which takes as its arguments a linked list of integers and two integers and changes all occurrences of the first integer to the second destructively. Then write a static method which performs the same operation constructively. The code given was:
class Cell
{
 int first;
 Cell next;

 Cell(int f, Cell n)
 {
  first=f;
  next=n;
 }
}

The first part of this question asking for the list to be changed destructively was reasonable well done. Here is what an answer should look like:

static void change(Cell list,int m, int n)
{
 for(Cell ptr=list; ptr!=null; ptr=ptr.next)
    if(ptr.first==m)
       ptr.first=n;
}
There were still a few people, however, who had difficulty with the very idea of a method which takes arguments and so did not even get the header correct. A common mistake, despite it being pointed out when many people made a similar mistake in the first test, was to confuse the problem with the example given to illustrate it. The example showed a case where the two integers were 5 and 8. This did not mean, however, that you were meant to write code that changed every 5 in a linked list of integers to 8. The numbers 5 and 8 should not have featured in any answer to this question. The code should take any two numbers, given as parameters to the method, m and n in the example above, and change all occurrences of the first to the second. The example given was just to illustrate one possible run of the code.

Quite a few people used the type List rather than Cell. This was wrong because the question made no mention of any class called List, linked lists are made up of cells, with the start of a linked list being the pointer to its first cell, so they are of type Cell.

A common mistake was to write the loop header as:

 for(Cell ptr=list; ptr.next!=null; ptr=ptr.next)
This would mean that if the last cell contained the number which is meant to be changed it wouldn't get changed. Remember that a for-loop has the form for(init;test;updatebody. This is equivalent to:
init;
while(test)
   {
    body;
    update;
   }
except that if init declares any new variables they would be accessible after the while loop but not after the for loop. So if the update is ptr=ptr.next and the test is ptr.next!=null, you will see that as soon as the update moves ptr to point to the last cell, the loop exits and the body is never executed with ptr equal to the last cell. There are times when you want the test to be or include ptr.next!=null, for example when you want ptr to end pointing to the last cell so you can add something to the end of the list. However, when you are simply altering the value in each cell, you want to carry on until all cells have been altered and ptr has gone right through and has value null.

A rather strange mistake which was made by several people was to write code that would change all occurrences of the first integer to the second and all occurrences of the second integer to the first. So they wrote a body to the loop something like:

    if(ptr.first==m)
       ptr.first=n;
    else if(ptr.first==n)
       ptr.first=m;
If this is what the question wanted, it would have said that. However, it said only change all occurrences of the first integer to the second, it said nothing about changing all occurrences of the second integer to the first. If you were a computer programmer and you were asked to write some code that changed all X's to Y's in some circumstance, imagine the havoc you could cause if without being told you wrote the code so that it also changed all Y's to X's. Suppose the code was running over student records and X was "grade B" and Y was "grade A", for example. The lesson here is that when you are asked to write code to do something, do not take it on yourself to make that code also do something else that might not have been wanted.

The part of this question asking for code for a constructive version of the operation was very badly answered. It has been made clear to you that constructive operations on linked structures are best handled through recursion. Here is a suitable answer for the second part of question 2 using recursion:

static Cell change(Cell list,int m, int n)
{
 if(list==null)
    return null;
 else 
    {
     Cell rest = change(list.next,m,n);
     if(list.first==m)
        return new Cell(n,rest);
     else
        return new Cell(list.first,rest);
    }
}
In other words, if the list is empty, return empty. Otherwise, first create a new list consisting of all m's changed to n's in all of the original list apart from its first element, then put n on the front if m was on the front of the original list, otherwise put what was on the front of the original list on the front of the new list.

I think this is a very clear and simple way of thinking about the problem.

Over half the class, however, tried to tackle this in an iterative way, and just one of those who did so managed to do it correctly. There are two ways it could be done iteratively. Here is a correct answer using the "pointer at back" approach:

static Cell change(Cell list,int m, int n)
{
 if(list==null)
     return null;
 else
    {
     Cell list1;
     if(list.first==m)
        list1 = new Cell(n,null);
     else
        list1 = new Cell(list.first,null);
     for(ptr=list,ptr1=list1; ptr.next!=null; ptr=ptr.next,ptr1=ptr1.next)
        if(ptr.next.first==m)
	   ptr1.next = new Cell(n,null);
	else
	   ptr1.next = new Cell(ptr.next.first,null);
     return list1;
    }
}
The important thing is that we need to make sure that new cells that are created are attached to the structure that starts at list1, and this is done by altering the field of the last cell of the structure headed by list1 each time round the loop. Many people gave answers which looked like they might have been trying to achieve what is done above, but created new cells that weren't linked to anything.

Another way of solving this problem iteratively is to use the "stack and reverse" technique. Hardly anyone chose to use this technique, but the one person who gave a correct answer that didn't use recursion did it in this way. Here is a solution using "stack and reverse":

static Cell change(Cell list,int m, int n)
{
 Cell stack=null;
 for(;list!=null;list=list.next)
    if(list.first==m)
       stack = new Cell(n,stack);
    else
       stack = new Cell(list.first,stack);
 for(;stack!=null;stack=stack.next)
    list = new Cell(stack.first,list);
 return list;
}

The important thing to note in this question is that a correct answer must create a new cell for every cell in the solution. Any answer that did not create new cells could not possibly work. For example, the following was quite often suggested as the body of the method:

 Cell list2=list;
 for(Cell ptr=list2; ptr!=null; ptr=ptr.next)
    if(ptr.first==m)
       ptr.first=n;
 return list2;
Setting a new variable list2 to list makes no difference, it still points to exactly the same cell that list points to, it doesn't create a new list. So setting ptr to that cell means ptr is set to point to the same cell as in the original list, and if that cell is changed it is a destructive change.

Question 3

Assume the definition of Cell as in question 2, and three variables of class Cell called list1, list2 and ptr. For each part of the question assume the initial situation is restarted to be as in the cells and pointers diagram below. Sketch what would be the situation after the execution of the code fragments given.

Part a)

for(ptr=list1; ptr.next!=null; ptr=ptr.next) {}
ptr.next=list2.next;

Answer:

The first line here is the standard loop for moving a pointer to the last cell of a linked list. The {} at the end of it indicate that it is a "loop without a body", so the next line is not part of the loop. The second line causes the field represented by ptr.next to stop having the value null and start having the same value as the field list2.next, so it is made to point to the same cell that list2.next points to.

Note that pointers come from fields, indicated by parts of cells, and this should be shown clarly by the pointer coming from the middle of the part of the cell representing the pointer field. But pointers point to whole cells, not to parts of cells, so the arrow at the end of the pointer should be next to the outside of the cell it points to, and it does not matter which part of that cell it points to.

Some people gave answers to this in which the linked list starting at list1 had separate cells containing 2 and 6 added at the end. This is wrong. In cells and pointers diagrams it is important to show when cells are shared as this represents a different situation form the case when the lists have separate cells but containing the same numbers.

A few people showed the cell containing 11 as having a diagonal line through its next field and a pointer coming from the cell. This is wrong, since the diagonal line indicates a field set to null and it is obviously impossible for a field to be both null and point to a cell.

Some people gave cells and pointers diagrams but did not clearly indicate what list1 referred to and what list2 referred to after the execution of the code. In the answer given above, it is also shown what ptr refers to after the execution of the code, since it does not end being set to null.

Other errors seemed to be caused by people not knowing what the assignment ptr.next=list2.next does.

Part b)

for(ptr=list1; ptr!=null; ptr=ptr.next)
    ptr.first = ptr.first*3;
list2.first=list2.first*2;

Answer:

This part was the best done of all the parts of this question. Perhaps the most common error was not to understand correctly what was in the loop and what wasn't. If a for header is not followed by a {, then only the next statement is in the loop and the statement after that is not in the loop. An assignment is a statement. So ptr.first = ptr.first*3 is in the loop while list2.first=list2.first*2 is not. This is also indicated, as is convention, by the code which is inside the loop being indented.

So it can be seen that this is the standard pattern of moving a pointer down a list changing each of the cells (in this case, multiplying their first field by 3). Following this is a single statememnt in which the first field of list2 is multiplied by 2. This last statement does not affect any other cells in the list headed by list2.

Part c)

for(ptr=list2; ptr!=null; ptr=ptr.next) {
   ptr.first = ptr.first*3;
   list1.first = list1.first*2;
}

Answer:

Again, there were problems with people who still have not understood how compound statements work. In this case, the opening { occurring after the for heading indicates that everything that follows is in the for loop up to the closing }. Contrast this with the previous question where the lack of a { means only the first statement following the for loop heading is in the loop.

So in this case we have a pointer moving down the linked list which starts with the reference in variable list2 (note carefully, as one or two people didn't that in this case it was list2 whereas in the previous part it was list1). The first statement in the loop body means that each time round the loop, the first field of the cell that ptr is pointing to gets multiplied by 3. So when the code is finished, all the cells in the list referred to by list2 have their integer value multiplied by 3. The second statement in the for loop body multiplies the first field of the cell referred to by list1 by 2. However, the variable list1 remains referring to the same cell throughout the execution of the loop, so the first field of this cell is multiplied by 2 three times, making it 24 eventually, as ptr is moved three times before it becomes null.

Part d)

list2.next = list1.next;
for(ptr=list1; ptr.next!=null; ptr=ptr.next)
   ptr.first = ptr.first+10;

Answer:

There were a couple of tricky issues in this question. The first statement causes list2.next to refer to the same cell as list1.next. This is indicated diagrammatically by list1 and list2 pointing to separate cells each of which has a pointer field with an arrow pointing to the same cell.

It is important to note that we have actual cell sharing here, because when ptr is set to move down the list starting with list1 in the loop that follows, adding 10 to the first fields, the cell which contains 7 has 10 added to its first field and this change is shared with the list starting at list2, as is the change to the next cell, as these cells are shared in the lists. Some people seemed to think for some reason that the list starting at list1 would be affected by the changes in the loop, but not the list starting at list2.

The other tricky issue in this part of the question was that the loop test was ptr.next!=null and not ptr!=null. This means the loop is exited when ptr points to the last cell in the list but before 10 is added to the first field of that cell. A lot of people assumed that the value in this cell would be changed from 11 to 21.

Question 4

Part a)

Describe what is meant by a "nested class" in Java, and say why one might be used.

A complete and correct answer to this part of the question would be something like:

A nested class is a class that is defined within the body of another class. It is used where the nested class will be referred to only within the enclosing class, for example to construct a data structure which the enclosing class uses to implement its public methods.

This part was mostly answered quite well, although some answers were a little too brief. For example, just writing that a nested class is a "class within a class" isn't quite clear enough, it was necessary to be a bit more precise over what this meant by noting that it is a class defined inside another class.

Part b)

Explain how a Java StringTokenizer works.

A complete and correct answer to this part of the question would be something like:

A string tokenizer is created with a string as its argument and breaks the string up into separate words. By default space characters are treated as separators between words, though it is possible to specify other characters to be treated as separators. A string tokenizer has a method nextToken(), and each time this method is called on it the next word from the break up of the original string is returned. There is also a method hasMoreTokens() which returns true when called before all the words have been returned, and false after they have all been returned.

Many of the answers to this were too vague. They did not say how the string was broken up, and how the separate words were returned. It was essential for full marks for this part of the question to make clear you understood that the separate words were returned through repeated calls to a method which returned a new word each time it was called. Some people in answering this question actually wrote, wrongly, that the separate parts were put in an array or some other structure by the string tokenizer.

A lot of people wrote answers to this question which used the word "read". It was not altogether clear what they meant by this because a string tokenizer does not read anything, in the strict sense of the word which means to take in text information from an external source such as a file or display. You should not use the word "read" to mean "go through some collection of information" or "take as an argument". When a method takes something as an argument it means an internal transfer of references to some structure within the execution of a program, and this should not be referred to as "reading", and a method returning a result should not be referred to as "writing" it. While a string tokenizer often is used to break up text read from a file or entered by the user of a program, it does not actually do the reading itself, it simply takes as an argument a string value which has already been read through the use of some other method, such as the method readLine() called on an object of type BufferedReader.

Part c)

Explain what the methods head, tail and cons do when called on an object of abstract data type List.

A complete and correct answer to this part of the question would be something like:

A List is a collection of data in which the order of the data is singificant. The method head returns the first item in the collection. The method tail returns a new collection which consists of all the items from the collection it was called on in the same order but missing the first item. The method cons takes an argument and returns a new collection which contains all the items from the collection it was called on in the same order, with the argument item added to the front.

This part of the question was not very well answered, despite the fact that a lab exercise using the abstract data type List had been given the day before, so everyone should have done this and learnt about the ADT List from it.

Quite a few people gave completely wrong answers to this, and many who were otherwise on the right lines supposed that tail gave the last item of a list. Clearly this suggests that people have not done the lab work. You need to recall that labwork is set because the material in this course is best learnt by practising with it. If you don't do the labwork, you will not gain familiarity with important principles.

Some people answered this question as if it were one on linked lists, making reference to "cells" and the "first" and "next" field. This is to ignore one of the most important issues in this course: the need to draw a clear distinction between an abstract data type and the data structure that might implement it. An abstract data type list is viewed only in terms of the operations possible on it, and so should be discussed only in terms of how those operations interact. An abstract data type list could be represented by a linked list as its data structure referred to within the code that implements it, but it needn't be. An alternative representation would be to use an array and count data structure, which indicates that it is clearly wrong to talk about it in terms of cells and fields, since these are not a necessary component of a list.

Another common mistake was not to make quite clear that you understood the methods on an ADT List to be constructive. It would be wrong, for example, to say that the method cons "adds an element to a list" because that suggests it changes the list. You needed to make quite clear that you understood it creates a new list. You also needed to be clear that you understood cons to create a new list which was like the original list with the new element added to the front. Not stating this is another example of being too vague in definition questions, since your answer could then equally apply to other abstract data types such as queues or sets or sequences. It was not enough, either, simply to describe the operations tail and cons as if they were destructive and add something like "done constructively" to the end. It was important to show you really understood the concept of an operation being constructive by describing it in a way that makes clear it returns a new object and does not change the one it was called on.

Part d)

Explain the difference between pre-order, post-order and in-order traversal of a binary tree.

A complete and correct answer to this part of the question would be something like:

To traverse a tree is to go through each of the items stored in the tree doing some operation on each of them. A pre-order traversal of a tree does the operation on the root item of the tree, then does the traversal recursively to its branches. A post-order traversal of a tree does the traversal recursively to its branches, and finally does the operation to its own root item. An in-order traversal of a binary tree does the traversal recursively to one of its branches, does the operation to its root item, then does the traversal recursively to the other branch.

Many people in answering this part of the question just put something like

Root, left, right
Left, right, root
Left, root, right

which does not clearly explain what is meant by the terms. It was necessary to explain that a single operation is applied to the root and the traversals of the branches are recursive.

As with question 1, another problem was to illustrate it with an example, but to use an example of a tree so small that it did not clearly indicate an understanding. For example, with a tree of just three elements, one in the left branch, one in the right, pre-order traversal is the same as breadth-first would be, so it is not enough to indicate that you understand the difference. Nor is it enough to give just an example without explaining the general rules for getting the order of your example.

A few people thought "pre-order" and "post-order" referred to the state of the tree before and after some sort of ordering operation on it. Again, these terms were clearly explained in the notes, so to make such a mistake suggests you have not taken the basic step in preparing for the test of at least reading the relevant notes.


Matthew Huntbach

Last modified: 5 April 2004