The mean on this homework was 22.62, the median was 23.75, the highest score was 28 and the standard deviation was 4.05.
It appears that some of you have some difficulty (or at least some hesitation) writing programs involving recursive data structures. You can, in general, realize relevant computations over such structures using either recursion or iteration. The first is quite easy to program but the second may be more efficient at least in C; functional programming language implementors sweat a lot to make recursion efficient, so this may not be true there.
To understand the two different ways of programming, let us consider the append function. First, here is how I would define a list type:
typedef struct ListCell *ListType;
struct ListCell {
int head;
ListType tail; };
Now, a recursive version of append can be defined simply as
ListType append(ListType l1, ListType l2)
{ ListType newl;
if (l1 == NULL) return l2;
else
{ newl = (ListType)malloc(sizeof(struct ListCell));
newl->head = l1->head;
newl->tail = append(l1->tail,l2);
return newl;
}
}
You should contrast this with the code for append that we have seen
both in Scheme and in ML---the correspondence is fairly direct.
The iterative version involves a little more work. The main problem is that you walk down the first list seeing elements in the reverse order to that in which they have to be added to the beginning of the second list. Once you have noted this fact, there are several ways to realize the required computation. Here is one way:
ListType append(ListType l1, ListType l2)
{ ListType applist, l1copy;
if (l1 == NULL) return l2;
applist = (ListType)malloc(sizeof(struct ListCell));
l1copy = applist;
while (l1->tail != NULL)
{ l1copy->head = l1->head;
l1copy->tail = (ListType)malloc(sizeof(struct ListCell));
l1copy = l1copy->tail;
l1 = l1->tail;
}
l1copy->head = l1->head;
l1copy->tail = l2;
return applist;
}
A point to note with regard to all the functions is that they
should not modify their inputs unless they are explicitly
required to do this. This is just part of good programming style. If your
functions do modify their input, then you have side effects that are not
apparent to someone who doesn't understand your function. Also, you can't do
something like append(L1, L1), even though this should be a legal function
call. Another point to note is that empty lists are legal
inputs and so should be treated correctly by your
program/functions. The code turned in by some of you did not handle
these situations correctly.
One other comment concerning the structure of a union. If you wanted to, you could equalize the sizes of the different alternatives by using, for instance, a union of pointers. This can have a space advantage if you end up using the union type with wildly different data sizes. On the other hand there is an extra indirection with associated space and time costs for this.
typedef struct ListCell *ListType;
struct ListCell {
void *head;
ListType tail; };
One problem with defining a generic type like this is ensuring that
each cell has enough space for any type of data. The only way to do
this for all cases is to use a pointer for the data. The other problem
is to determine the type of the data. In C, the only solution is to
essentially forget the type. This does pose problems but ones that can
be eliminated with a richer type system.
The changes to the structure of map and printlist should be fairly obvious; essentially the function they take would have to work with void * data. For example, the header for map might be the following:
typedef void *(*pfi)(void *i); ListType map(ListType l, pfi f);
A common kind of comment is that unions (in contrast to the approach in Problem 3) could lead to a substantial space penalty. This is not really necessary; as pointed out already, you can use a union of pointers that would, in a sense, mimic the void * solution or approach. Thus, there is a tradeoff between extra space for a pointer (and the time needed to dereference it) and the potentially wasted space if you did not use a pointer but the list elements needed differing amounts of space. Some of you also pointed to the cost of the nested ifs in your code. However, this is not a necessary cost since, as pointed out already, nested ifs are probably not the only or even the best way to realize different alternatives in this case.
Another worry is that of releasing space in conjunction with the last approach. The problem arises only if you share the representation of list elements, something that is generally reasonable to do. However, this is a general problem, not one restricted to this approach. For example, what else would you do if you were manipulating lists of lists? Neither C nor C++ offer any real solutions to this kind of problem and programmers generally have to develop a method for cleaning up garbage (if this is important) in addition to solving the real problem in these languages. The saner, more elegant, way to handle this is to leave garbage collection worries to some external process as is done in languages such as ML, Java and Prolog.
Last updated on April 11, 2006 by gopalan@cs.umn.edu and xqi@cs.umn.edu