The test was marked out of 100, 20 marks for each of the five questions. The standard Queen Mary grading system was used, so 40% is the pass mark, with 40-44 being a grade E, 45-49 a grade D, 50-59 a grade C, 60-69 a grade B, and 70 or more a grade A.
The results were very disappointing. In fact over half of those who sat the test did not pass it. This ought to serve as a warning to all students registered for the course (except the few who passed the test with good marks), that you will need to put a great deal more effort into this course if you are to pass the exam in May. You can find a spreadsheet with the results here.
I will go through the questions below, giving answers, and comments on how the question was answered by those who sat the test.
Anyhow, here's part a) of the question:
"Write a Java static method which takes as its argument an array of integers and two integers and changes the array destructively so that all occurrences of the first integers are changed to the second."
An example was given that if the integers are 7 and 8 and the array is:
Please note that this array, with the two numbers 7 and 8 were only given as an example to show just one way in which the method you write should operate. You were not meant to write code that could only change 7 to 8, or only deal with an array of length 11 just because the array given to illustrate the method was of length 11. Your method should deal with any array and any two integers passed as argument. Writing code that mentions the number 7 and 8, as quite a few students did, shows a major failure to understand the principle of abstraction (i.e. moving from specific examples to general cases), which underlies a lot of what Computer Science is about.
When asked to write a method, the first thing to consider is its
signature. All methods have to be given a return type. However, if
a method does not return a value, its return type is given as
void
. Destructive methods don't return a value, they have
their effect by changing the value of an object they are given as an
argument. All methods must have a name; no name was mentioned in the
question, so we can just make one up, say substitute
.
The method signature has the arguments the method takes in rounded
brackets following its name. Each argument is given with its type
and the name you have decided to use for it in the method. The question makes
clear the array takes three arguments: an array of integers and two
integers. The type for an array of integers is int[]
.
A convention often observed is that array arguments are called
a
, and integers arguments are called m
and
n
. So this gives us the following as the signature for
the method that answers this question:
static void substitute(int[] a,int m,int n)All we need to do in the body of the method is to go through the cells of the array one by one, and if any cell has value equal to
m
, change it to become n
. That's it.
Any change that takes place in a method to an array passed as an argument
is permanent, so that is how this method has its effect. Here's the
full answer to part a):
static void substitute(int[] a,int m,int n) { for(int i=0; i<a.length; i++) if(a[i]==m) a[i]=n; }Note that
a[i]==m
is the way of testing that the
array cell a[i]
stores a value equal to m
,
while a[i]=n
is the way of making the array
cell a[i]
become equal to n
. It was
surprising to see how many students still seem to be unclear on
the difference between ==
and =
.
A rather strange mistake that was made by quite a few students was to write code that not only changed all occurrences of the first integer to the second, but also changed all occurrences of the second integer to the first. If that is what the question had intended, it would have said that, but it didn't.
Some students seem to feel it is necessary to have a {
and a }
around the body of every for loop,
and every if statement. In fact it isn't necessary to have {
and }
around a body that consists just of one statement.
My preference is to leave them out in this case, since I feel they
make the code look cluttered. But they don't actually affect how the Java
code runs, and I know of at least one text book that suggests it's
"good practice" always to use {
and }
. If
you insist on doing so, you would get the answer below to part a) of
question 1):
static void substitute(int[] a,int m,int n) { for(int i=0; i<a.length; i++) { if(a[i]==m) { a[i]=n; } } }Please note the importance of good layout. In the code I've given, the line beginning
if
is indented to show it's inside the
for loop, and the line a[i]=n
is indented more to show
it's inside the if-statement which is inside the for-loop. Always
use indentation when you write your code. Not using it makes your code
harder to understand at a glance, and suggests you are not using it
because you aren't clear about program structure.
My preference with {
and }
is to put
the opening {
on a new line, and the matching closing
}
below it. The ONLY OTHER ACCEPTABLE LAYOUT OF BRACKETS
is to put the opening {
at the end of the line which
heads the block, and the closing }
indented by the
same number of spaces as that line. This would lay out the above code as:
static void substitute(int[] a,int m,int n) { for(int i=0; i<a.length; i++) { if(a[i]==m) { a[i]=n; } } }What I never, ever want to see is code where the closing
}
is put at the end of a line containing code. I do not know why so many
students gave answers which had that sort of layout, because no-one
has ever shown you code that looks like that, and no-one anywhere
suggests it is a good style.
So, now lets us consider part b) of question 1), where you are asked to
write a method that performs the same operation as part a) but
constructively. This means you create a new array that represents the
changes, but you don't actually change the old array. In this case,
the method returns an array of integers, so it has return type
int[]
. It takes the same arguments as in part a).
This is so basic there is really no excuse for not getting this part
right and not even being able to write the correct signature, which is:
static int[] substitute(int[] a,int m,int n)The first thing we need to do in the method is to create a new array of the same size as the array we call
a
in the method.
This is how we do it:
int[] b = new int[a.length];This declares a new variable called
b
, which is of type
int[]
i.e. "array of integers", and immediately assigns
it to refer to a new array whose length is a.length
.
Here are some things to note, all of which are based on mistakes encountered many times in answers to this question:
str
is given
by str.length()
, there is no ()
when we are
getting the length of an array.
array
, and this word plays no
part in the declaration of array variables or creation of new array
values. If we ever use this word, it will only be because we decided to name
an array variable array
rather than, say, just a
.
a
, assuming the base type of a
is int
you write new int[a.length]
and not
new int[a.length-1]
. The last cell of the new array
is indexed by a.length-1
, yes (e.g. b[a.length-1]
),
but the number of cells in the new array is a.length
with
indexing starting at 0.
If
, there is only if
. The primitive
type of integers is int
, it cannot be written Int
.
return
means "stop executing this method
immediately and put the value given next as the result of the method in
the code where it is called". Do not ever use return
unless
you intend to terminate the method at that point.
static int[] substitute(int[] a,int m,int n) { int[] b = new int[a.length]; for(int i=0; i<a.length; i++) if(a[i]==m) b[i]=n; else b[i]=a[i]; return b; }
Now here is part c) of question 1):
"Write a Java static method which takes an array and an integer and returns an array consisting of all those elements of the original array less than the integer in the order in which they occurred in the original array".
The example given is that if the method has the same array for its argument as above, and its integer argument is 6, the array returned is:
This is a little more tricky, as the array that is
returned from the method is not of the same length as the argument
array. Also, the numbers are not in the same positions. For example,
the number 4 is in cell a[5]
if a
is the name
of the original array, but is put in cell b[2]
of the
array that is returned if b
is that array's name.
The big problem is that we don't know at the start what will be the size of the final array. The solution I prefer is to create a temporary array of the maximum possible size, which is the size of the original array. Then those numbers to go in the new array are copied into it. It is necessary to keep two indexes, one for positions in the argument array as we go through its cells one by one, one for positions in this new array as we copy only some of the integers from the argument array into it. The final value of the second integer is the actual size of the array that needs to be returned. We then create a new array of that size and copy the numbers from the temporary array into it. This is returned as the result of the method. If we just returned the temporary array as the result, which is what a lot of people wrote code that did, the array that would result would be:
rather than what was required.
Here is my code which answers part c) of question 1):
static int[] filter(int[] a,int n) { int[] temp = new int[a.length]; int count=0; for(int i=0; i<a.length; i++) if(a[i]<n) { temp[count]=a[i]; count++; } int[] b = new int[count]; for(int i=0; i<count; i++) b[i]=temp[i]; return b; }
and draw the state after the execution of some code.
The diagram above, which is what was given on the test paper, shows how a cells and pointers diagram for linked lists should look. Remember that pointers come from individual fields of cells, in our linked list example shown as the right hand part of the cell. But they point to whole cells.
A lot of people gave answers to this question which showed cells linked
with pointers, but did not show where list1
and
list2
pointed to. It is necessary to show this,
even if there has been no change - you need to show that you know
there is no change. It is also very important to show where
cells are shared. If the setup of some cells and pointers system is that
two list variables point to structures which share some cells, then that
is a different set up from one where the two list variables represent
the same lists but don't share cells. You need to demonstrate that
you understand this. It is also important when giving an answer to this
sort of question to show clearly which is your final answer, and which
is your working out. For example, if a pointer is changed to point
to another location, a diagram which shows it pointing to the old location
and the new one is useless, it is not clear what you mean, and might
suggest that you believe that pointers can point to two different
places at the same time (which, of course, they can't). One more
thing, if nothing points to a cell because what used to point to it has
been altered to point to something else then don't draw that
cell in your diagram, as it is no longer part of the cells and pointers
system in question.
Note that in all cases (assuming we are dealing with linked lists of integers)
something.first = ...is an integer assignment. It won't cause any pointers to change, but it will copy whatever integer value the
...
refers
to into the left-hand portion of the cell which something
points to. On the other hand,
something.next = ...is an object assignment. It won't cause any of the integer values in any of the cells to change. It will cause the pointer in the right-hand portion of the cell pointed to by
something
to be changed
to point to whatever ...
points to.
Part a) of question 2) asks what happens to the initial situation when
list2.next.next = list1.next;is executed. The answer is:
It means that list2.next.next
is set to point to the same
cell that list1.next
points to. It is essential to show
the cell sharing this leads to. Note that since nothing points to the
cell list2.next.next
used to point to (which contained
the number 6), that cell is not drawn as part of the solution.
Part b) of question 2) asks what happens to the initial situation when
for(ptr=list1; ptr.next!=null; ptr=ptr.next) {} ptr.next=list2.next; list1.next.first = 10;is executed. Note that the first line of this is a for-loop without a body, indicated by the
{}
at the end. You can also
spot this because the following line is not indented. A for loop like
the one here has the effect of setting ptr
to point to the
last cell of the list. In this case, the next
field of
that cell is then made to point to the same cell pointed to by the
next
field of the cell pointed to by list2
.
The final line is an integer assignment, so it does not change any pointers,
but does change the value of the integer stored in the cell pointed
to by list1.next
. The final result is:
Part b) of question 2) asks what happens to the initial situation when
list2 = list2.next; for(ptr=list1; ptr!=null; ptr=ptr.next) ptr.first = ptr.first+list2.first;is executed. The first line simply causes
list2
to stop
pointing to what it used to point to (the cell containing 2) and start
pointing to what list2.next
points to (the cell containing 7).
Obviously, since list2.next
is part of the cell
list2
points to, it has to work out what this means
before it changes what list2
points to. The rest of the
code is a for-loop which this time does have a body. The for loop moves
ptr
all the way down the list referred to by list1
executing the code body for ptr
pointing to each of its cells.
That is indicated by the condition being ptr!=null
. At the end
of the execution of this for loop, ptr
does have the
value null
. The body of the for-loop is an assignment. Note
that since the cell ptr
refers to changes each time the
loop is executed, then ptr.first
refers to a different
integer variable each time. But nothing in the loop changes what
list2.first
refers to, so each time round the loop it
refers to a variable holding the number 7. Note the difference at this
point between list2
which points to a cell whose
first
field hold 7 and whose next
field
points to a cell storing 6, and list2.first
which is the
portion of this cell that actually holds the number 7.
The answer to part c) of question 2) is:
Cell
class from question 2). In part a) of question 3)
you were asked:
Write a static method which takes a linked list of integers and an integer
n
as arguments and multiplies all the integers in the linked
list by n
(performing the operation destructively).
As before, the first thing to do when confronted with a question that
asks you to write a method is to think what the method signature is.
As with part a) of question 1), the fact that the work is to be done
destructively indicates the method return type should be void
.
The work of the method is done by changing the data structure it
takes as an argument. The question states clearly that the method has
two arguments, a linked list of integers and an integer.
Although we discussed the abstract data type List
later in
the course, this is not a question about that type. In general, when
asked to answer a question involving writing Java code, you should
not assume the existence of any class or method which is not
built into Java, unless it is mentioned in the question. Therefore,
in this question you can make use of the type Cell
but
not of the type List
. A linked list is made up of objects
of type Cell
linked together, therefore its type is
Cell
. It is manipulated by directly accessing the fields
of Cell
objects, as we did in question 2). This is different
from writing code dealing with objects defined by the List
class we gave later, where they can only be manipulated by the methods
which are defined as part of the List
class.
So the signature for the method asked for in part a) of question 3) is:
static void mult(Cell list,int n)We have decided to name the linked list argument
list
and
the integer argument n
. So list
is used
here just because it an appropriate variable name here, not because it
has because it has any special meaning.
The code for destructively altering the list uses the standard technique of a pointer going through the cells of the list one at a time. If we change one of the fields of the cells, we will cause the list of which that cell is part of to be altered permanently, i.e. we have made a destructive change. Here is code to answer part a) of question 2:
static void mult(Cell list,int n) { for(Cell ptr=list; ptr!=null; ptr=ptr.next) ptr.first = ptr.first*n; }We can declare the variable
ptr
inside the for loop header
because we do not need to access it after the loop is executed. A lot
of people wrote code like this but had the test in the header of the
for loop as ptr.next!=null
. This is what we would have
if we wanted the pointer to be pointing to the last cell once the for
loop has finished. However, it means the body of the for loop is not
executed with ptr
pointing to the last cell, since the
loop would be exited as soon as it points to it, therefore using
this test instead of ptr!=null
would mean all but the
last cell of the list has its integer multiplied by n
.
Note, we could have used ptr.first *= n;
as
a shorthand for ptr.first = ptr.first*n;
.
Some people (myself included) like these shorthands because they
make it visually clear that we are changing the value of a
variable, others however don't like them and think they are something
Java only has because the language C had them, and Java was devised to
resemble C.
Quite a few people attempted to answer this question with code which made no sense in the context of this question, but looked like they were trying to recall some code that used linked lists that they remembered from the lectures or notes. This is an indication of that pitfall I warned you about: confusing memorisation with learning. It won't work with most computer science, certainly not with programming. It is like learning to drive (you will note I like to make analogies which compare learning to program with learning to drive) not by learning all the various manoeuvres and understanding when to use them, but instead trying to memorise the exact set of manoeuvres that take you from one particular place to another particular place. If you don't understand a piece of code, but are writing it down because some trigger word in the question brought it to your memory, you have the wrong approach, and are very likely to get it wrong. Whenever you write code to answer a question in a test, you should understand the code you are writing and be clear in your mind what it does and why it solves the problem that was set.
Part b) of question 3 asked you to write a method that performs the same operation as part a), but constructively, that is returning a new list which represents the change without changing the argument list.
So any answer to this question would need to create a new cell to
match each of the cells of the original list, but with its integer
multiplied by n
. No change at all should be made to
the original list. The obvious way to do this is to use recursion,
since there is a clear recursive way of describing what we want that
translates directly to code:
"The multiplication of all the elements of a linked list by n
when the list is null
is null
. The multiplication
of all the elements of a linked list by n
otherwise is a list
whose first cell contains the integer of the first cell of the original
list multiplied by n
and whose rest is the rest of the
original list multiplied by n
".
The underlined part above is, of course, a restatement of the entire problem, hence can be achieved by a recursive call. This gives the following as the answer to the question:
static Cell mult(Cell list,int n) { if(list==null) return null; else { Cell newnext = mult(list.next,n); return new Cell(list.first*n,newnext); } }The part following
else
here could be written in a more
concise style
return new Cell(list.first*n,mult(list.next,n));
if you prefer.
The return type for this method is Cell
because it returns
a new linked list value, and as the question stated, linked lists should
be represented in the form given in question 2.
Solving this problem this way ought to be the obvious way to do it, once you are used to using recursion it should be very simple. However, most people who attempted this question tried to do it using iteration. The fact that nearly everyone who attempted an answer using iteration made mistakes that meant their code would never work as intended indicates that this is a much more complicated way of looking at it. However, there is an iterative way of solving it similar to the iterative way given in class for copying a list. Here is the iterative solution to the constructive list multiplication problem:
static Cell mult(Cell list,int n) { if(list==null) return null; else { Cell list1 = new Cell(list.first*n,null); Cell ptr,ptr1; for(ptr1=list1,ptr=list; ptr.next!=null; ptr=ptr.next, ptr1=ptr1.next) ptr1.next = new Cell(ptr.next.first*n,null); return list1; } }Note that the only way we can create a new cell that links to the list we are building (
list1
) is to have a pointer
(ptr1
) to the last cell of that list and then set
its next
field to point to the new cell. If (as many
people who answered this question did) we wrote something like
ptr1 = new Cell(ptr.first,null);
, that
would create a new cell but it wouldn't be linked up to anything.
Many people gave answers to part b) of question 3) which did not
involve new cell creation, so could not have constructed an entire
new list as required. In some cases, their code would still have had
the effect of changing the original list destructively. Quite a few
people assumed a new cell could be constructed by
new Cell()
. This is not the case, because the code
for Cell
in question 2) gives only a two-argument
constructor. A zero-argument constructor for a class is provided
automatically only if no other constructor for the class is given.
Otherwise, if you want a zero-argument constructor along with other
constructors, it will only exist if you write it as part of the class.
An alternative way of tackling this problem iteratively is to go through the original list and put the new cells you are creating at the front of a new list. This will have the result of creating the list you want in reverse order. So you then need to go through this new list again putting the cells on the front of another list, and returning that list as the result. Here is some code which does it this way:
static Cell mult(int n,Cell list) { Cell ptr,temp=null; for(ptr=list; ptr!=null; ptr=ptr.next) temp = new Cell(ptr.first*n,temp); Cell list2=null; while(temp!=null) { ptr=temp.next; temp.next=list2; list2=temp; temp=ptr; } return list2; }
A few people answered question 3) by writing code that dealt with arrays of integers. Given the amount of time given in this course to linked lists, it was incredible to see students who did not realise they are an entirely separate thing from arrays. It was particularly strange given that question 3) referred directly to question 2), which clearly was not about arrays.
One point to note about questions involving definitions and explanations is that you have to be very precise with them. Computer Science is a precise subject, and you should be careful to use language in such a way as to make clear what you intend and to avoid waffle. Rememer, you can lose marks by adding extra irrelevant waffle, since that detracts from the brief explanation you were asked to give. If you write something that is correct, but then spoil it by adding extra material that suggests you didn't really understand what you wrote, you will lose marks.
In part a), "aliasing" refers to the way two different variable names
can refer to the same object. A number of people in answering this
question confused "variable" with "object" - in Java it is easy to
slip into saying "object" when you mean "variable which refers to an
object", but that's a particularly wrong thing to do here when the
difference between the two is crucial. Another mistake made more than
once was to explain aliasing correctly, but then to spoil it by giving
an example involving two variables of type int
, which
cannot be aliases because they hold primitive values rather than
object references. Some people also said that because b=a
caused b
to be an alias of a
then
b=c
would cause a
to change. No - it causes
b
to stop being an alias for a
and start being
an alias for c
. Aliasing has the effect of appearing to
change the object one variable refers to when changing another object
(actually the same object under an alias) only when a destructive method
is called on one of the variables that refer to that object, or when
one of it fields is changed through an assignment. So, after
b=a
, the code b.field=x
will also change
what a
refers to, as will b.meth(x)
if
meth
is destructive, but making b
itself
refer to another object won't change a
.
Many people answered part b), which asked for an explanation of
"tail recursion" just by saying what recursion is. A recursive call
is tail recursive if its result is returned directly as the result of
the call that made it, or in the case of a void
-returning
method if it is the last statement that is executed in the method.
To answer this question correctly, you had to make quite clear that
you understood under what condition a recursive call is tail recursive.
Some people wrote answers that looked as if they might understand but
used language in such a vague way that it wasn't clear. Quite a few
people answered this question in a way that suggested they thought
"tail" here referred to the tail of a list, which it doesn't.
Part c) asked for an explanation of "O(N2)". What was required here was something that showed you understood the big-O notation. Many people just said it was "a description of selection sort" or similar words. Although selection sort is O(N2), it is not the only algorithm that is. It was not necessary, or even particularly desirable, to mention selection sort in order to answer this question. Any algorithm is O(N2) if the time it takes to execute is roughly proportional to the square of the amount of data, N, for large values of N. It is not the case, as many people wrote that, O(N2) means an "inefficient algorithm". It all depends on the problem. An O(N2) sort algorithm is inefficient because we know there are better sort algorithms, but there are other problems where an O(N2) algorithm would be very efficient. Some people in answering this question wrote of "selection search" or something similar which suggested they were unaware of the difference between sorting and searching. Sorting puts a whole collection of data in order, while searching simply finds one particular data item in a collection.
Part d) asked for an explanation of "axioms". These are rules which define how a particular data type works. It was important in answering this question that you showed you understood axioms are actually part of the definition of an abstract data type. If you wrote something like just "they are rules a programmer must keep to", that is far too vague.
In part a) of this question, you were asked to describe informally how selection sort works. You could have written "You find the lowest item in a collection and put it in the first position of the collection and put the value that was there in the original position of the second item. Then you find the lowest item of all the collection except for the first item and put it in the second position and put the item that was there in the second lowest items' original position. Then you find the lowest item of all the collection except for the first two items and swap it with the itemin the third position, and so on". It was not really necessary to give details of actual variables used in writing the code to do this. However, it was necessary to write the explanation in such a way that anyone who wasn't familiar with selection sort would after reading it understand how it worked. Many people gave answers where this wouldn't be the case.
Part b) of this question asked for an explanation of how you would
delete a number from a linked list of numbers. The important thing
here was that you had to show clearly that you understood you had to
get a pointer to point to the cell before the one being
deleted from the list and then alter that cell's next
field to refer to the cell the next
field from the
cell being deleted referred to. Quite a lot of answers didn't show clearly
that this was understood.