We now consider the idea of linked data structures. A linked data structure usually involves an object type which has a field of the same type within it, in other words a recursive type. Less usually it could involve the equivalent of indirect recursion, for example, an object which has a field of another type and that other type has a field of the first type.
The simplest linked data structure is the linked list. A
linked
list consists of an object with two fields, the first a piece of data,
the second a linked list. As for simplicity we concentrated on sets of
integers, so for simplicity we will concentrate on linked lists of
integers,
though the data could actually be of any type. So our linked list
consists
of an integer and a linked list, and the linked list which is part of
it
consists of an integer and a linked list, and the linked list which is
part of that consists of an integer and a linked list ... and so on.
While it seems this must go on for ever, we need to remember that an
object field can be set to refer to an object or set to null
meaning it is not set to refer to anything. This is the equivalent of
the
base case in recursion. We do not follow an endless chain of linked
lists
because eventually we will come to one set to null
and
thus
come to a halt. Actually, we could have a linked list structure which
is endless, but not infinite, since a list can point back to a cell
which is already part of the list, creating a circular
structure.
Here is the class for a linked list:
class CellWe have called the class
{
int first;
Cell next;
Cell(int f,Cell n)
{
first=f;
next=n;
}
}
Cell
rather than LinkedList
to indicate that we can think of things of this type as objects with
two parts, one an integer, the other a Cell
. Note too
that the
fields of the Cell
object are not declared as private
.
This means that they can be changed by code outside the class
Cell
. Suppose we have three variables of type Cell
:
Cell c1, c2, c3;and that after some code which sets
c2
and c3
to refer to something, we set c1
to refer to a new Cell
object, with 5 and c2
as the arguments to the
constructor:
c1 = new Cell(5,c2);Now the situation is:
The object that c1
refers to has two parts, and as
they
are not private to the object, they can be treated just like variables
called c1.first
and c1.next
. The lack of a
line around the boxes representing these fields shows they are not
protected by being private to the object. Note that the arrows
always point to the whole object even if it is shown to have
separate parts; in Java (unlike C) there is no concept of a pointer
to part of an object. The constructor
sets c1.first
to 5 and c1.next
to refer to
the
same object that c2
refers to. But we can change either
of them. If we first execute:
c1.first = 8;we will get the situation:
If we then execute
c1.next = c3;we will get the situation:
Of course, the objects that c2
and c3
refer to are themselves cells with two parts. Suppose we now
execute:
c3 = new Cell(10,null);The situation becomes:
Note that although c3
now refers to a new object,
c1.next
continues to refer to the object it was pointing
to, which is the object c3
used to refer to. The diagonal
line across the second part of the object c3
refers to
is used to indicate an object value which is set to null
.
Now if we execute
c1.next = c3;again, we will get the situation:
The object that c1.next
originally pointed to is not
drawn because now none of the variables here point to it. If nothing at
all points
to it, the Java interpreter will remove the space allocated to it
altogether,
allowing it to be reused, a process known as garbage
collection.
If after this we set c3.next
to refer to a new Cell
object, for example by executing:
c3.next = new Cell(4,null);we will get the following situation:
It can be seen that even though c1
wasn't mentioned in
the
last statement, what it refers to is changed by it, because c1
refers to an object that refers to the object that c3
refers
to, and that object was changed.
add
and
delete
methods, along with a member
and
toString
method, so the class here could be used in place
of one of the destructive set classes we used earlier just by changing
the type name in the application program.
class IntSet11What is happening here is that a set object has a field which refers to a linked list inside it. For example, if we had a variable of type
{
// Destructive Set class, sets stored as unordered linked lists
Cell list;
public void delete(int n)
{
if(list!=null)
if(list.first==n)
list=list.next;
else
{
Cell ptr;
for(ptr=list; ptr.next!=null&&ptr.next.first!=n; ptr=ptr.next) {}
if(ptr.next!=null)
ptr.next=ptr.next.next;
}
}
public void add(int n)
{
Cell ptr;
for(ptr=list; ptr!=null&&ptr.first!=n; ptr=ptr.next) {}
if(ptr==null)
list = new Cell(n,list);
}
public boolean member(int n)
{
Cell ptr;
for(ptr=list; ptr!=null&&ptr.first!=n; ptr=ptr.next) {}
return (ptr!=null);
}
private static class Cell
{
int first;
Cell next;
Cell(int f,Cell n)
{
first=f;
next=n;
}
}
public String toString()
{
String str="{";
if(list!=null)
{
str+=list.first;
for(Cell ptr=list.next; ptr!=null; ptr=ptr.next)
str+=","+ptr.first;
}
return str+"}";
}
}
IntSet11
called s
which represented
the set {5,8,3,6}, we could represent this
diagrammatically as:
One new thing here is that the class Cell
is actually
inside the class IntSet11
. To make this quite
clear, it is put between the method member
and the
method toString
, although this is just for demonstration
and it would be more normal practice to put it either before or after
all the methods. This is a simple example of a nested class
(sometimes referred to as an "inner class" though that term is often
used only for certain sorts of nested classes). The class is declared
as private
which means it cannot be used outside the
class
IntSet11
where it is declared. Nested classes can be
declared as static
or not, just like methods and fields
in classes. If an inner class is declared as static
, as
Cell
is, it cannot refer to non-static methods or
non-static fields of the class. Making a class a nested class is
a way of indicating it exists only in order to be used within its
enclosing class.
There is no constructor given for the class IntSet11
.
If there is no constructor for a class it is given a default
constructor. The default constructor for any class has no
arguments and sets all numerical fields in the new object created to
0 and all object fields to null
. So new IntSet11()
will create a new IntSet11
object, with its field called
list
, which is of type Cell
set to null
.
An IntSet11
object with its list
field set
to
null
represents the empty set (the set with no integers in
it). It can be seen that the add
method just adds the
new number to the front of the list of numbers representing the set.
So the list does not store the numbers in any particular order.
It is, however, necessary to test first whether the number is already
in the list, if it is it us not added again at the front (otherwise
the delete
method wouldn't work properly, because
deleting
one cell containing the number wouldn't delete the other).
The other methods use the technique of setting a variable to refer
to
each of the items in the list in turn, moving down the list until
either it reached the end of the list or it reaches some point where
it needs to halt. Consider the loop in the method member
:
for(ptr=list; ptr!=null&&ptr.first!=n; ptr=ptr.next) {}Note this is a loop without a body, indicated by the
{}
at the end. The variable being moved down the list is ptr
standing
for "pointer". It could be called anything, but it is commonly called
ptr
.
At first ptr
is set to be equal to the list
field of the object. Then a check is made to see if either ptr
is null
, which would only happen if the object
represented
the empty set, or the first
field of the Cell
object ptr
refers to is equal to the number n
.
If not, the update operation ptr=ptr.next
sets ptr
to refer to the second Cell
object in the linked list.
This
process is repeated until either ptr
refers to a Cell
object which stores the number being tested or membership of the set,
or
it has been moved right through the list and reached the end of it. In
the
former case, the number is a member of the list, in the latter it
isn't.
So ptr!=null
is true
if ptr
does not refer to null
and instead refers to some cell
storing n
, it is false
otherwise so the
value
of this boolean expression is returned as the value of the method call.
Now executing ptr.next=ptr.next.next
will set the
next
field of the cell ptr
is referencing,
that is the cell holding 3, to be equal to the next
field
of the cell that ptr.next
was originally referring to,
that is to refer to the cell holding 7. Since the cell holding 3 is
part of the list which starts at the field list
of the
IntSet11
object, the effect of this is to cut the cell
holding 4 out of the list:
In fact, since nothing points to the cell holding 4, we can think of it as disappearing and being recycled in garbage collection:
Note that in these "cells and pointers" diagrams, the actual placing of the cells and shape of the lines does not mean anything. All that matters is where the lines representing pointers come from (individual variables or fields within cells) and where they point to (whole cells).
When we are deleting, an additional check needs to be made
that ptr
is not pointing to the last cell of the list.
If it is, then ptr
has been moved right through the list
and the number to be deleted has been found not to be present in the
list.
Representing sets by ordered linked lists
Sets as shown above were represented by unordered linked lists.
We could represent sets by linked lists stored in numerical order
However, this isn't very helpful. The major reason it was helpful
to represent sets by ordered
arrays
was that it made searching for a member in the set quick as we could
use the binary search algorithm. Binary search works because given a
position
in an array we can get to it in one step. However, we can't do that
with
a linked list - the only way we can get to an element in the linked
lists we have discussed here is to start at the beginning of the list
and follow the links down one at a time. If we know a list representing
a set is ordered, if we are searching for a particular set member
either
to test its membership or to delete it from the set, we can stop
searching if on going through the set we find a number greater than it
(assuming the order is ascending numerical) as we know the number won't
occur after that. So on average this will cut the amount of search
done by a half. However, using the big-O
notation this is still O(n), so it is not
an equivalent gain in efficiency to that caused by introducing binary
search with its O(log n) time.
For illustration, here is come code that represents sets by ordered linked lists:
class IntSet12Note the code is more complex, in methods
{
// Destructive Set class, sets stored as ordered linked lists
Cell list;
public void delete(int n)
{
if(list!=null)
if(list.first==n)
list=list.next;
else
{
Cell ptr;
for(ptr=list; ptr.next!=null&&ptr.next.first<n; ptr=ptr.next) {}
if(ptr.next!=null&&ptr.next.first==n)
ptr.next=ptr.next.next;
}
}
public void add(int n)
{
if(list==null)
list = new Cell(n,null);
else if(n<list.first)
list = new Cell(n,list);
else
{
Cell ptr;
for(ptr=list; ptr.next!=null&&ptr.next.first<n; ptr=ptr.next) {}
if(ptr.next==null||ptr.next.first>n)
ptr.next = new Cell(n,ptr.next);
}
}
public boolean member(int n)
{
Cell ptr;
for(ptr=list; ptr!=null&&ptr.first<n; ptr=ptr.next) {}
return (ptr!=null&&ptr.first==n);
}
public String toString()
{
String str="{";
if(list!=null)
{
str+=list.first;
for(Cell ptr=list.next; ptr!=null; ptr=ptr.next)
str+=","+ptr.first;
}
return str+"}";
}
private static class Cell
{
int first;
Cell next;
Cell(int f,Cell n)
{
first=f;
next=n;
}
}
}
delete
and
member
representing the two ways in which the loop may
terminate without finding the desired number: either the pointer
sent through the list reaches the end of it, or it reaches the
position of pointing to a larger number. The code for add
shows how an item may be inserted into a linked list. First a
pointer is set to point to the cell before the position the
new cell is to go in, then ptr.next = new Cell(n,ptr.next)
is executed, where n
is the item to be inserted. In the
case where the insertion point is before all current cells, a
special section of code needs to be written to join the new cell to
the front. Suppose we want to insert 5 in numerical order in the
linked list representing [2,3,6,9].
The method add
checks the list is not null
and the number to be inserted is not less than the first number in the
list. Having checked this, it moves ptr
to the point
where it points to the cell whose next
field is greater
than the number to be inserted, by executing the loop:
for(ptr=list; ptr.next!=null&&ptr.next.first<n; ptr=ptr.next) {}resulting in the position being:
Then executing ptr.next = new Cell(n,ptr.next)
where n
holds 5 gives:
Deletion is similar to deletion from a non-ordered list, except if
the pointer is moved to a position where the cell referenced by the
next
field of the cell referenced by the pointer holds a
number greater than the number being deleted, we know that number
cannot be in the list. So in the simple form of sets we have been
considering, we leave the situation unchanged and do not report any
error or exceptional condition.
Copying linked lists
Suppose we wanted to make a copy
method for sets
represented with linked lists, as we did previously for sets
represented using arrays. Again, we could use inheritance to give
us a new class which adds a copy method:
class IntSet11a extends IntSet11Note that since this class refers to the
{
public IntSet11a()
{
super();
}
private IntSet11a(Cell l)
{
list=l;
}
public IntSet11a copy()
{
Left blank for now } }
Cell
class
and the list
field from the class IntSet11a
,
they will need to be of status protected
in that class
rather than private
.
As in our previous example,
we have
a private constructor which takes the data structure used to represent
the new set as its argument, and creates a new object around that data
structure. If the following code were used for the copy
method:
public IntSet11a copy() { Cell list1=list; return new IntSet11a(list1); }it would not work properly, because although we would create a new object, the linked list in the new object would share the linked list from the old object. So the following:
s2=s1.copy();with
s1
representing the set {2,7,4,3}
would result in the structure:
So we have two separate objects, referred to by s1
and
s2
, each with their own list
field. But
the list fields refer to the same Cell
object. Since
the delete
method alters the linked list of a set object,
calling, for example
s1.delete(4);would give the following situation, changing as a side-effect change what
s2
represents, which is clearly not the situation we
want:
What we need is to copy the list completely, so that calling
s2=s1.copy()
will result in the list
field of s2
referring to a list of cells with the
same contents as that of s1
but not actually the
same cells, that is, using our previous example:
The following code for copy
will achieve this:
public IntSet11a copy()
As can be seen, if the linked list is
{
if(list==null)
return new IntSet11a(null);
else
{
Cell list1 = new Cell(list.first,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,null);
return new IntSet11a(list1);
}
}null
, representing
the empty set, a new object whose list
field is set to
null
is created. Otherwise, a new Cell
object
is created to be the front of the new linked list, its integer is the
integer from the cell at the front of the old linked list. Two pointers
ptr1
and ptr
are then set to point to the
first cell in the new linked list and the old linked list respectively.
A new cell for the next
field of ptr1
is created with the integer from the next
field of
ptr
, this gives the situation:
Then both pointers are advanced down the list, and the process
repeated
until ptr
points to the last cell in its list. The effect
is that the copy is built up in the order the cells come. This copy
is then used to create a new object using the private constructor.
We shall use the techniques used here to copy a linked list again when we consider implementing a constructive set class using recursion.
Last modified: 4 March 2005