Lecture 7 Handout

Linked List

Learning Outcomes

At the end of this lecture, you’ll be able to:

Lecture Plan

In this lecture, we'll cover the following lessons:

  1. Array: A Static Data Structure
  2. Linked List: A Dynamic Data Structure
  3. Java Interlude: Static Nested Class
  4. Array vs. Linked List
  5. Build a Linked List
  6. Linked List: Follow the References!
  7. Linked List Operation: Prepend
  8. Linked List Operation: Traverse
  9. Linked List Operation: Get
  10. Linked List Operation: Append
  11. Linked List Operation: Insert
  12. Linked List Operation: Delete
  13. Linked List Iteration: The Iterator!
  14. Exercise: LinkedIndexedList

Lessons marked with ⚡ contain exercise/activity.

Downloads

Array: A Static Data Structure

Array is the most fundamental data structure built into many programming languages. It is also used to implement many other data structures. Indeed, our first choice was to use an array to implement IndexedList ADT.

Arrays have their limitations too; they are static structures and therefore cannot be easily extended or reduced to fit the data set. It is also expensive to insert/delete from the front or middle of an array.

In this lecture, we explore another fundamental data structure called Linked List that does not have the limitations of arrays.

Linked List: A Dynamic Data Structure

A linked list is a linear data structure where each element is a separate object made of at least two items: the data and a reference to the next element. Conventionally, each element of a Linked List is called a node.

public class Node<E> {
  private E data;
  private Node<E> next;

  // we can have constructors, setters, getters, etc.
}

Here is a minimal implementation for a Linked List:

public class LinkedList<T> {
  private Node<T> head;

  // we can have constructors, methods to add/remove nodes, etc.
}
Resource

Wikipedia’s entry on Linked List is great!

Java Interlude: Static Nested Class

It is a common practice to nest the Node class inside the LinkedList class:

public class LinkedList<T> {
  private Node<T> head;

  private static class Node<E> {
      E data;
      Node<E> next;
  }

  // we can have constructors, methods to add/remove nodes, etc.
}

Note the nested class is declared as static.

A static nested class does not have access to the instance members of the outer class.

On the other hand, the outer class has access to the instance members of objects of the static nested class. This is the desired arrangement: the Node does not need access to the members of LinkedList, but LinkedList can access data and next on objects of Node thus eliminate the need for getters/setters.

Inner vs Static Nested Class
Inner ClassStatic Nested Class
Instance member of the outer class (not static!) and as such have access to all the members of the outer class (private, instance, static, etc.)Static (class) member of the outer class and do not have access to instance members. Rather, the outer class has access to the nested class members for convenience.

Here is a toy example that showcases the use of inner vs. static nested classes.

Resources

Array vs. Linked List

Array is a static data structure. The length of an array is set when the array is created. After creation, its length is fixed. A linked list is a dynamic data structure. The length (number of nodes in a list) is not fixed and can grow/shrink on demand.

Insertion/deletion to the front or at the middle of an array is expensive; it requires shifting other elements to make/fill a gap. Insertion/deletion in a Linked List can be done as cheap as updating a few reference variables (we will see this soon).

One disadvantage of a linked list is that it does not allow direct access to the individual elements. If you want to access a particular item then you have to start at the head and follow the references until you get to that item.

Another disadvantage of a linked list is that it uses more memory compared to an array (to store a reference to the next node, for each element).

Resource

Build a Linked List

Consider the following implementation of Node (with package private visibility modifier):

class Node {
  int data;
  Node next;

  Node(int data) { 
    this.data = data; 
    this.next = null; // we can eliminate this line; it happens by default 
  }
}

Exercise Draw a schematic representation of a linked list pointed to by the head variable after the following code snippet has been executed.

Node head = new Node(7);
head.next = new Node(5);
head.next.next = new Node(10);

Node node = new Node(9);
node.next = head;
head = node;
Solution

Linked List: Follow the References!

Consider the following linked list:

Exercise In each case, draw a schematic representation of the linked list after the statement is executed. For each statement, start with the linked list displayed above.

head = head.next;
Solution
head.next = head.next.next;
Solution
head.next.next.next.next = head;
Solution

Linked List Operation: Prepend

Suppose we have the following linked list and we want to add a new node to the front of it.

Exercise Complete the implementation of the addFirst method that creates a node and adds it to the front of the list.

public void addFirst (T data) {
  // TODO Implement Me!
}

Hint: Use the following visualization as a guidance:

Solution
public void addFirst(T t) {
  Node<T> node = new Node<>(t);
  // node.next = null; // no need: done by default!
  node.next = head;
  head = node;
}

Linked List Operation: Traverse

Suppose we have a linked list with $n$ elements (nodes) and we want to go over every element and print out the data stored in it.

Exercise Complete the implementation of traverse that iterates over a linked list and prints the data stored at every node.

public void traverse() {
  // TODO Implement me!
}

Hint: the front of the linked list is marked by the head pointer. If you were to follow the next references, how would you know when you reached the last node?

Solution
public void traverse() {
  Node<T> current = head;
  while (current != null) {
    System.out.println(current.data);
    current = current.next;
  }
}

If you keep count of the number of nodes in the linked list, then you can also write this with a counter-controlled loop.

public void traverse() {
  Node<T> current = head;
  for (int count = 0; count < numElements; count++) {
    System.out.println(current.data);
    current = current.next;
  }
}

Linked List Operation: Get

Suppose we have a linked list with $n$ elements (nodes) and we want to get the data stored in the $k^{th}$ element (at index $k-1$).

Exercise Complete the implementation of the get method which returns data stored at a given index.

public T get(int index) {
  return null; // TODO Implement me!
}

Hint: you cannot directly jump to $K^{th}$ node. You need to start at the head and follow the next references to get there!

Solution
public T get(int index) {
  return find(index).data;
}

// PRE: 0 <= index < numElements 
private Node<T> find(int index) {
  Node<T> target = head;
  for(int counter = 0; counter < index; ounter++) {
    target = target.next;
  }
  return target;
}

Caution: the implementation above fails to account for an edge case!

Linked List Operation: Append

Suppose we have the following linked list and we want to append (add to the end) a new node.

Exercise Complete the implementation of the addLast method that creates a node and add it to the back of the list.

public void addLast(T t) {
  // TODO Implement me!
}

Hint: Use the following visualization as guidance.

Solution
public void addLast(T t) {
  Node<T> tail = head;
  while (tail.next != null) {
      tail = tail.next;
  }

  tail.next = new Node<T>(t);
}

If you keep count of the number of nodes in the linked list, then you can also write this with a counter-controlled loop. In that case, the find helper method from when we implemented get can be used here to go to the last element.

Caution: the implementation above fails to account for an edge case!

Linked List Operation: Insert

Suppose we have a linked list with $n$ elements (nodes) and we want to insert a new node at index $k$. This means we insert a new node between elements at indices $k-1$ and $k$. After the insertion, we will have $n + 1$ elements:

Example: we have a linked list with 3 nodes at indices 0, 1 and 2.

We will insert a new node at index 2:

Exercise Complete the implementation of insert which adds a new node at the given index.

public void insert(int index, T t) {
    // TODO Implement me!
}

Hint: Use the following visualization as guidance.

Solution
public void insert(int index, T t) {
  Node<T> target = head;
  for (int counter = 0; counter < index; ounter++) {
    target = target.next;
  }

  Node<T> node = new Node(t);
  node.next = target.next;
  target.next = node;
}

Caution: the implementation above fails to account for edge cases or cases where index is invalid!

Linked List Operation: Delete

Suppose we have a linked list with $n$ elements (nodes) and we want to delete an element at a given index $k$. This means we remove the $k^{th}$ node from the list. After deletion we have $n - 1$ elements:

Example: we have a linked list with 4 nodes at indices 0, 1, 2, and 3.

We will delete the node at index 2:

Exercise Complete the implementation of delete which removes a node at the given index.

public void delete(int index) {
    // TODO Implement me!
}

Hint: Use the following visualization as guidance.

Solution
public void delete(int index) {
  Node<T> beforeTarget = head;
  for(int counter = 0; counter < index - 1; counter++) {
    beforeTarget = beforeTarget.next;
  }

  beforeTarget.next = beforeTarget.next.next;
}

Caution: the implementation above fails to account for edge cases and cases where index is invalid!

Linked List Iteration: The Iterator!

Assume we pass the head reference variable which points to the front of a linked list to the following Iterator class.

Exercise Complete the implementation of hasNext and next methods.

public class LinkedList<T> implements Iterable<T> {

  private Node<T> head;

  // other fields/methods are not shown here!

  @Override
  public Iterator<T> iterator() {
    return new LinkedListIterator();
  }

  private class LinkedListIterator implements Iterator<T> {

    @Override
    public T next() {
      return null; // TODO Implement me!
    }

    @Override
    public boolean hasNext() {
      return false; // TODO Implement me!
    }
  }
}

Hint: describe the responsibilities of hasNext and next before implementing them.

Solution
private class LinkedListIterator implements Iterator<T> {
  private Node<T> current;

  public LinkedListIterator(Node<T> head) {
    current = head;
  }

  @Override
  public T next() {
    if (!hasNext()) {
      throw new NoSuchElementException();
    }

    T t = current.data;
    current = current.next;
    return t;
  }

  @Override
  public boolean hasNext() {
    return current != null;
  }
}

Exercise: LinkedIndexedList

Open the starter code and look for the LinkedIndexedList.java file. This file is a linked list implementation of IndexedList ADT.

Exercise Complete the implementation of LinkedIndexedList (at home!)

Note here we cannot start with an empty list. Instead, we must build a complete list (with the given size and the default value) in the constructor of the LinkedIndexedList.

We have written our unit tests based on the specification of the IndexedList ADT. We can therefore use the same suite of tests to test both implementations of IndexedList (that is ArrayIndexedList and LinkedIndexedList). We need to switch between these two implementations. Open the starter code and find out how this is done by leveraging inheritance. (Hint: look for the abstract method createUnit).

Solution

Refer to the solution code posted.