![]() |
Algorithms and Data Structures | ||||
|
Linked
Lists
![]() We will first introduce a list in its purest form, implementing
the basic methods over a list of integers. This will then be generalised
to a list of objects, and applied to a small problem (managing a "To Do"
list). We then re-implement this abstract data type using vectors and then
with a two dimensional array with internal memory management.
What is a list? A list is a sequence of elements. You can think of many applications of lists, in the real world and abstractly. Real world, we have shopping lists, lists of things to do, list of students, etc. Abstractly, in the computer, we can manage the memory of the machine as a list, a file of characters as a list, a list of activities to process, etc. Generally, lists are flexible, grow and shrink on demand, elements can be accessed, inserted, deleted. Lists can be joined together (concatenated), split into sub-lists, etc Mathematically: a list is empty or is has a head with data in it, followed by a tail that is a list. Note:That is a recursive definition of a list; we have defined a list in terms of a list! A list is often shown as follows: a1, a2, ..., an or as [a1,a2,...,an] or as (a1, a2,...,an)The basic methods we would expect to have over a list are as follows
A first stab at the List Class, and its methods (java code) Our first definition of the List Class, and its methods is defined textually below. A List is defined as a triple: boolean eol; int data; List next;
Below we have a diagram of a list containing the elements [21, 14, 7]. There are 4 elements in the list. Note that the first 3 all have their eol attribute set to False signifying that the element is not an end of list element. The data attribute has an integer value (21, 14, or 7). The next attribute points to another list element. Note that the last element of the list has an eol set to True and a next attribute pointing to the ground (this is generally accepted as a pointer to null). This fourth and last element, is our representation of the end of list. ![]() Why can't I have a null List in Java? A good question! The methods that we use on a List are defined for the class List (obviously). It would have been nice to be able to apply the relevant methods to an empty list null. Unfortunately null is classless! So, Java would not know what method to apply. This is a nuisance. Consequently I have to tag each List element to say if it is actual data or the end of list marker. And the end of list marker looks like every other List element with the exception that its eol attribute is True. The constructors We have three constructors:
The accessors We have 3 methods to access elements of the list. Assume we have the List L.
To print a list This is our first recursive function. A recursive function/method is defined in terms of itself. We have already seen a recursive definition of a class, the List. So it would appear natural that methods that operate over this class will be recursive as well. A recursive function is generally of this form: define f(x) if x=some.terminating.value then do-something-and-then-stop else {compute y using the value of x; f(y);}This is an example of tail recursion. The print method is also tail recursive. We must address the following situations when printing a List L:
public void print() { if (isEmpty()) System.out.println(""); else { System.out.print(head() + " "); tail().print(); } }The method terminates when it encounters the empty list element. You might like to think of the method as operating with some kind of pointer that chases along the list, and each recursive call uses this pointer and then moves it along one place/element on the recursive call. Length of a list The length of a list is a count of the number of List elements in the List. Given a List L, the length of L is 0 if L is empty, otherwise it is one plus the length of the rest of the list. public int length() { if (isEmpty()) return 0; else return 1 + tail().length(); }The above italicised text is a recursive definition of the length of a list, defined in terms of the method length. Again, it has a stopping condition (when the List is empty), and an action to perform immediatley before it makes is recursive call. Given a List L, L.length() might be thought of as expanding to a sequence of additions 1 + 1 + 1 + 1 + ... + 1 + 0 Add an element e to the list public List add(int e) { return new List(e,this); }Add an element to the list, for example with List L, L=L.add(3), will add a new List element to the front of L. In a lisp-like language (lisp or scheme) this method is implemented in the function cons. Note the use of this. this refers to the current object. The current object is generally implict, for example when we use the method head() in append below. But when we want to do something explicitly with the current object we refer to it as this. Append two lists together Given two Lists L1 and L2, L1.append(L2) delivers a new List with the elements of L1 followed by the List L2. Again, the method is tail recursive. public List append(List l2) { if (isEmpty()) return l2; // (1) else return new List(head(),tail().append(l2)); // (2) }The line commented (1) we are appending L2 to an empty List, so we just deliver L2. Line commented (2) we make our recursive call: we deliver a new List with L1.head() as the data and a next of the list composed of L2.tail() with L2 appended. So, you might think of this expanding into a sequence of calls to the constructor List(e,l). For example assume we have List L1 [1 3 5] and List L2 [2 4 6], a call L1.append(L2) expands as follows List(1,[3,5].append(L2)) List(1,List(3,[5].append[L2])) List(1,List(3,List(5,null.append[L2]))) List(1,List(3,List(5,L2))) Insert an element e into a List Given a List L1, insert an element in order into the list. Deliver a new list. A call might be L1.insert(3), inserting the integer 3 into the List L1. public List insert(int e) { if (isEmpty()) return new List(e); else if (e < head()) return new List(e,this); else return new List(head(),tail().insert(e)); }Again, tail recursive. We see a pattern similar to append, with recursive calls building up a sequence of calls to the constructor List(e,l). Delete an element e from a List Delete an element from the List, delivering a new List. Given L1 and element e=3, we might have L1.delete(3), deleting the list element with L1.head()==3. public List delete(int e) { if (isEmpty()) return this; else if (e == head()) return tail(); else return new List(head(),tail().delete(e)); }Similar to insert and append, being tail recursive, building up a sequence of calls to the constructor List(e,l). Reverse a List Given a List L1 = [1 2 3], L1.reverse() delivers a new List [3 2 1]. The method is a bit more complicated than previous methods. Ideally, we'd like to define a method reverse that acts on a list. To do that we need a helper function, which I've called rev. Consequently reverse calls rev, and all the work is done in rev. Given a two Lists L1 and L2, L1.rev(L2) takes the elements in L1 and adds them one by one onto the front of L2, terminating when L1 is empty, delivering L2 as a result. So, to get the reverse of L1 we start with L2 = List(), the empty List. And that is what reverse does, it makes a call to L1.rev(new List()) (but of course L1 is there implicitly). private List rev(List temp) { if (isEmpty()) return temp; else return tail().rev(new List(head(),temp)); } public List reverse() { return rev(new List()); } }rev again is tail recursive, resulting in a sequence of calls to the constructor List(e,l), building the resultant list as it goes. The Test class (java code) The class definition of the List doesn't actually do anything, but it is there to be used. The little program below is a driver program, allowing us to demonstrate most of the features of the List class. The Test class uses BasicIo. You used this in 1st year, and we need to import this from java.lancs. The Test program runs on a DOS or UNIX window, putting out a prompt. The program starts by prompting us for a command: end (quits the program), sho (prints the current list), len (prints the length of the list), add (adds a new element to the list, and you are prompted for data), del (delete an element from the current list, and you are prompted for data), rev (reverse the current list). On successful completion of each command the current list is printed out. import java.lancs.*; public class Test { public static void main(String[] args) throws Exception { List l1 = new List(); int i; BasicIo.prompt("Type in command: (end, sho, len, add, ins, del, rev) "); String command = BasicIo.readString(); while (!command.equals("end")) { if (command.equals("sho")) l1.print(); if (command.equals("len")) System.out.println(l1.length()); if (command.equals("add")) { BasicIo.prompt("data: "); i = BasicIo.readInteger(); l1 = l1.add(i); l1.print(); } if (command.equals("ins")) { BasicIo.prompt("data: "); i = BasicIo.readInteger(); l1 = l1.insert(i); l1.print(); } if (command.equals("del")) { BasicIo.prompt("data: "); i = BasicIo.readInteger(); l1 = l1.delete(i); l1.print(); } if (command.equals("rev")) { l1 = l1.reverse(); l1.print(); } BasicIo.prompt("Type in command: "); command = BasicIo.readString(); } } }Note
Benefits and Costs of the linked List
Missing Methods? I guess the following methods are missing, and might be implemented.
Confessions I write this on July the 2nd 1998. I wrote my first "Hello World" program in java less than 7 days ago. So, although I've been educated until I am daft, I am still on a steep learning curve (java on the y-axis, time on the x-axis). I make claim that I cannot use null as the empty list. This might be wrong. If you can correct this, then please do, and let me know how to do it. |