Linked Lists

CSCI 1933 – Introduction to Algorithms and Data Structures
Adriana Picoral

Array-based List

Array-based List

Elements are stored in contiguous memory (issue of allocation)

Front of List Middle of List End of List
get O(1) O(1) O(1)
set O(1) O(1) O(1)
insert O(n) O(n) O(1)
remove O(n) O(n) O(1)
  • random access (access through an index) is fast
  • insert/remove something – shift everything else

Linked lists

A linked list is a chain of nodes, where each node contains:

  • data - the actual value you’re storing (like arrays)
  • a pointer/reference - the address of the next node in the chain

No need for contiguous memory allocation (nodes can be scattered anywhere in memory)

Linked List

  • dynamic structure, can grow and shrink as needed (no allocation needed)
  • elements are arranged in a linear sequence
  • implemented through nodes – each node has a reference to the next node

Nodes

To create a linked list:

  • a class of self-referential objects
  • each object of the class contains a reference to an object of the same class
  • convention is to name the class Node

Write a class in Java called Node that has two instance variables: data (any type), and a reference to another node.

Write the constructor method, and a set and get method for each instance variable.

Generics

  • Generic Data – what is it?
  • Why generics rather than Object?
    • Flexibility and re-usability and strict type checking
    • Detect errors early – compile time
  • Generic typing allows an attribute’s type to be specified and type-checked at compile-time using a single class definition
  • Effectively, the actual type of a generic typed attribute within an object is determined at at variable definition and object creation

Generic Node class

Write:

  • a constructor that takes in data (type T)
  • getters for both instance variables – getData and getNext
  • setters for both instance variables – setData and setNext
public class Node<T> {
    private T data;
    private Node<T> next;
  
}

Submit your Node.java file to gradescope

Solution

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

    // constructor
    public Node(T data) {
        this.data = data;
        next = null;
    }

    // setters
    public void setNext(Node<T> next) {
        this.next = next;
    }

    public void setData(T data) {
        this.data = data;
    }

    // getters
    public Node<T> getNext() {
        return next;
    }

    public T getData() {
        return data;
    }

}

LinkedList class

  • What instance variables do you need?

LinkedList class

A linked list has a head (reference to a Node)

LinkedList set up and constructor

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

    // constructor
    public LinkedList() {
        head = null;
    }

}

Methods

  • What methods do you need to implement?

Methods

  • Insertion
  • Search
  • Deletion

A toString() method to test our code

addNode(Node<T> node) method

Write a addNode(Node<T> node)

  • What are the options of implementation for insertion?
  • What are the advantages and disadvantages of each?

Insert at end of list

How can we improve on this?

public void addNode(Node<T> node) {
  if (head == null) head = node;
  else {
    Node<T> currNode = head;
    while (currNode.getNext() != null) {
      currNode = currNode.getNext();
    }
    currNode.setNext(node);
  }
}

toString() method

Before we move on to other types of insert, lets create a toString() method to test our list insertion.

Write a toString() method that traverses the entire list

  • Concatenate values

toString() solution

  @Override
  public String toString() {
  String result = "";
  Node<T> currNode = head;
  while (currNode != null) {
    result += currNode.getData() + " ";
    currNode = currNode.getNext();
  }
  return result;
}

Gradescope Submission

Submit your Node.java (previously implemented) and LinkedList.java files to gradescope

Methods in LinkedList.java the autograder will test:

  • addNode(Node<T> node) that adds a node at the end of the list
  • toString()
  • isEmpty() returns true if list is empty (no nodes), false otherwise
  • length() returns an integer representing number of nodes in the list (should be O(1))

Other insertion methods?

Our insert method has to traverse the entire list to insert a new node.

How can we improve on insertion time?

Insert at the beggining of list

public void push(Node<T> node) {
  if (head == null) head = node;
  else {
    node.setNext(head);
    head = node;
  }
}

search(T value) method

Write a search(T value) method that traverses the entire list trying to find a node that matches the argument value – if found, returns the node, if not, returns null

  • Similar to toString()

Submit your Node.java and LinkedList.java with search(T value) method to gradescope

search() solution

public boolean search(T data) {
  Node<T> currNode = head;
  while (currNode != null) {
    if (currNode.getData().equals(data)) {
      return true;
    }
    currNode = currNode.getNext();
  }
  return false;
}

Linear search – complexity

What we have implemented is an unsorted linked list – how many comparisons does it required to find a target element?

In the worst case, each node must be examined once, resulting in O(n) complexity – n is the number of elements in the list.

Delete node

Removing a node takes a few more steps:

  • Find the node to remove (if present, can find it through index or matching data), look ahead for the next node, adjust reference for the previous node
  • How to remove head?

Search matching data and remove

public Node<T> remove(T data) {
  Node<T> currNode = head;
  Node<T> previous = null;
  while (currNode != null) {
    if (currNode.getData().equals(data)) {
      if (previous != null) {
        previous.setNext(currNode.getNext());
      } else {
        head = head.getNext();
      }
      return currNode;
    } else {
      previous = currNode;
      currNode = currNode.getNext();
    }
  }
  return null;
}

Remove first element of the list

public Node<T> pop() {
  Node<T> node = null;
  if (head != null) {
    node = head;
    head = head.getNext();
  }
  return node;
}

Remove Nth node

public Node<T> pop(int n) {
  if (n >= 0 && n < length) {
    Node<T> current = head;
    Node<T> previous = null;
    for (int i = 0; i < n; i++) {
      previous = current;
      current = current.getNext();
    }
    if (previous == null) return pop();
    else previous.setNext(current.getNext());
    length--;
    return current;
  }
  return null;
  
}

Runtime of LinkedList

Front of List Middle of List End of List
get O(1) O(n) O(1)
set O(1) O(n) O(1)
insert O(1) O(n) O(1)
remove O(1) O(n) O(1)

Other types of linked list

We just implemented a singly linked list

Doubly linked list:

Other types of linked list

Circular linked list:

Recursion

Many linked list functions can be implemented with recursion.

public Node<T> search(Node<T> node, T value) {
  if (node == null) { // base case
    return null;
  }
  if (node.getData().equals(value)) {
    return node; // found node with matching value
  }
  // search next
  return search(node.getNext(), value);
  
}

public Node<T> search(T value) {
  // traverse all nodes, return the first
  // node that data equals value argument
  return search(head, value);
}

java.util.LinkedList Class

java.util.LinkedList

  • Doubly-linked list implementation of the List and Deque interfaces

Linked Lists: Pros and Cons

Some Pros:

  • easy add and delete (no moving/copying values over to keep index order)
  • no need to pre-specify length (use only the space that is needed)

Some Cons:

  • no random access so must “link” through nodes to get to desired location in list
  • sorted linked list pales over sorted array for lookup (binary search not an option)

Looking Back

  • The “list” data abstraction can be implemented as an array or linked list
  • Which Implementation might be better? Why?