Linked
Lists – Advanced Operations
In the previous lessons we learned some basic operations
that can be performed on linked lists. In particular, we discussed how to
append, prepend, insert and delete nodes to/from a
linked list. In this lesson we learn some advanced operations that can be
performed on linked list. We will also discuss how to generalize the singly
linked list methods to a more complex data structure like a doubly linked list
or a multilinked list. We will start with some advanced operations on singly
linked lists.
Reversing a List
One of the useful operations that can be performed on linked lists is to
reverse a linked list. A naive algorithm for doing this is to remove the first
node of the list, insert to the beginning of a new list, then remove the second
node (which now has become first node) and insert to the beginning of the new list
etc. If the list has n nodes, then after performing n of these operations, we
would have reversed the list. The following diagrams illustrate this process.
Original List
1 2 3
Delete
first node and insert to the beginning of a new list.
1
Delete the second node and insert
to the beginning of the new list etc..
3
This algorithm requires one to carefully
manage removing nodes from the old list and adding nodes to the new list. Even
if the new nodes are not created, complex pointer manipulations is necessary.
Exercise: Complete the method
public myList reverese(myList L); that reverses the list L and returns a reference to the new list.
An Alternate Algorithm
So can we find a better algorithm
for reversing the list? One possibility is to reverse the links without
deleting or inserting nodes. The following figure illustrates the process.
Last
node
head
In this algorithm, the next
reference can be reversed as we traverse the list.
Exercise: Complete the method
public myList reverese(myList L);
that reverses the list L and returns
a reference to the new list using new algorithm that reverses the links.
Cloning
Linked Lists
Let us begin by evaluating the
following code
LinkedList list = new LinkedList();
list.prepend(new
Node("first"));
list.append(new
Node("last"));
LinkedList list2 = list;
The reference list2
is an alias, it points to the same object as list1. This means that once we
change list, these changes occur in list2 as well. In this case, we
consider list2 as a shallow copy of
list1. In other words, list1 and list2 shares the same nodes in one linked
list. In many cases it will be quite useful to have a deep copy of an object. To create a deep copy, we need to duplicate
all nodes in the list. For this purpose, the Object class provides the method clone(), which we will override in the LinkedList
class:
public Object clone() throws java.lang.CloneNotSupportedException
{
LinkedList twin =
new LinkedList();
if(head == null)
return twin;
Node tmp = head;
for(int i = 0; i
< size; i++)
{
twin.append(tmp.data);
tmp
= tmp.getNext();
}
return twin;
}
As you know append()
is quite expensive operation, because each time you need to traverse the whole
list to append a node.
Exercise: rewrite the clone method assuming there is a prepend
method in your LinkedList class.
Sorting
a Linked Lists
Sorting a List is one of the
common operations that are performed. Sorting an array of object can easily be
done using sorting algorithms like bubble, insertion or quick sort. However,
sorting a linked list is not that trivial since linked list nodes cannot be
randomly accessed. Only useful way to sort a linked list is to remove a node
from the old list and insert into a new list in order. However for large lists,
this is not very practical as insertion sort requires O(n2)
operations. An alternative method is to define a toArray
method for the linked list class that allows the linked list to be converted to
an array first and then apply a more efficient algorithm like quick sort to
sort. Once sorted the array can be converted into a linked list again. This is
still more efficient in theory as toArray, sorting
using qsort, constructing a linked list from an array
takes O(n), O(n log n) and O(n) operations respectively. Asymptotically this is
still better than O(n2) performance of a
slow sort.
Doubly
Linked Lists
We briefly discussed the concept
of doubly linked list in lesson 1. Each node in a doubly linked list has a
reference to the previous node. A good exercise is to write append, insert and
remove methods for a doubly linked list. In a singly linked list, insert
requires changing two references. Generalizing this to a doubly linked list
requires changing four references (two for next and two for prev).
We need to take great care in the order we perform these four operations, as
incorrect order may create lists with nodes that have self references, creating
an infinite loop during the traversal of the list.
An
Array of Linked Lists
Another useful data structure to
consider is an array of linked lists. This type of a structure allows us to use
two indices, the array index and linked list position to locate a particular
node. For example, you can maintain a priority queue as an array of linked
lists, where next element to process is selected from the array index based on
the priority. Here is a picture of a priority queue implementation using
In this implementation we assume that array
index indicates the priority of the list and each list is a queue where next
element to process is the first node of the list. In this case removing a node
from a list is easy (as nodes are removed from the front) but adding a node to
a list is expensive, requiring us to traverse the list to find the end of the
list.