A linked list is a data structure where one object refers to the next one in a sequence by storing its address. Each object is referred to as a node. In addition to a reference to the next object in the sequence, each object carries some data (e.g., integer value, reference to string, etc.). The following diagram illustrates a typical linked lists:
The following class defines a node of a linked list where the data is an integer.
public class Node { int data; Node next; // reference to next object in the sequence }
The following class defines a node of a linked list where data is a string reference.
public class Node { String data; Node next; // reference to next object in the sequence }
A reference variable of node type called head has the address of the first node of the list. A empty list is represented by setting the head to null.
The following is a typical LinkedList class definition where the node class has been defined as an inner class. Although we are defining a linked list of strings, the example is representative of any linked list.
public class LinkedList { private class Node { private String data; private Node next; private Node(String data) { this.data = data; this.next = null; // unnecessary, but to illustrate initial value } } private Node head; }
public LinkedList() { head = null; }An empty list is defined by setting head to null.
public void insert(String data) { Node newElement = new Node(data); if (head == null) { head = newElement; return; } newElement.next = head; // S1 head = newElement; // S2 }See the following animation where top represents head: Stack (Linked List Implementation)
Node curr = head; while (curr != null) { /* Process Node's data */ curr = curr.next; // Advance to next node in the list }For example, the following code prints the contents of a list:
Node curr = head; while (curr != null) { System.out.println(curr.data); curr = curr.next; // Advance to next node in the list }The following code inserts an element after a particular element of the list. It will add the new node and return true if the target exists or it will not add the node and return false if target is not found.
public boolean insertAfter(String target, String toInsert) { Node curr = head; while (curr != null) { /* Conditional to check if current node is the target */ if (curr.data.equals(target)) { /* Target found, create new node to be added */ Node toAdd = new Node(toInsert); /* Set new node's next to point to current node's next */ toAdd.next = curr.next; /* Set current node's next to new node, now the new node is between the current and current's next */ curr.next = toAdd; return true; } curr = curr.next; } /* Target node was not found, return false */ return false; }The second traversal moves two references (prev and curr) in parallel. The prev reference points to the element that precedes the current (curr) one. Any operation that requires identifying the node that precedes the current one (e.g., insert before a target node) can be implemented with this traversal.
Node prev = null; // first node has no predecessor Node curr = head; while (curr != null) { /* Process data or any other operation */ prev = curr; // current node will become previous curr = curr.next; // updating to next node }For example, the following method inserts an element into the list before the target element.
public boolean insertBefore(String target, String toInsert) { Node prev = null; Node curr = head; while (curr != null) { /* Conditional to check if current node is the target */ if (curr.data.equals(target)) { /* Target found, create new node to be added */ Node toAdd = new Node(toInsert); /* Conditional for when the new node has to be head */ if (prev == null) { head = toAdd; toAdd.next = curr; } /* Otherwise */ else { prev.next = toAdd; toAdd.next = curr; } return true; } prev = curr; curr = curr.next; } return false; }
Implementing recursive solutions to linked lists usually requires an auxiliary that will take a head reference as a parameter. For example, printing a list can define as follows:
public void printList() { printList(head); } public void printList(Node curr) { if (curr != null) { System.out.println(curr.data); // S1 printList(curr.next); // S2 // processes the tail } }
Reversing statements S1 and S2 will print the list in reverse order. Another example where a value is returned is represented by the size() method.
public int size() { return size(head); } public int size(Node curr) { if (curr == null) { return 0; } else { return 1 + size(curr.next); // 1 + size of the tail } }
An example where a data structure is created and passed as part of the recursive calls is the following:
public ArrayList<String> getData() { ArrayList<String> answer = new ArrayList<String>(); getData(head, answer); return answer; } public void getData(Node curr, ArrayList<String> answer) { if (curr != null) { answer.add(curr.data); getData(curr.next, answer); } }
A modification of the list using recursion is represented by the following
example:
/* Sets the head to the result of remove recursive call. */ public void remove(String target) { head = remove(target, head); } public Node remove(String target, Node curr) { /* If current reaches the end (null), target was not found. */ if (curr == null) { return null; } /* Conditional to check if target is found. */ if (curr.data.equals(target)) { /* remove target node by returning next. */ return curr.next; } /* Target is not found, so set next to recursive call. */ else { curr.next = remove(target, curr.next); } return curr; }
Class LinkedList { private class Node { // node class is same ... } /* value of first Node isn’t part of list. final is used so that it is not modified. */ final Node head = new Node(null); }And the insert function.
public void insert(String data) { Node newElement = new Node(data); newElement.next = head.next; head.next = newElement; }Notice that we do not have to check if head is null (list is empty).
/* this function returns a deep copy of provided linked list */ public LinkedList createDeepCopy() { LinkedList newList = new LinkedList(); Node currOriginal = this.head; /* set head. this step would not be necessary if we used dummy node implementation. */ newList.head = this.head == null ? null : new Node(currOriginal.data); if (newList.head == null) { return newList; }; Node currNew = newList.head; while (currOriginal.next != null) { currOriginal = currOriginal.next; currNew.next = new Node(currOriginal.data); currNew = currNew.next; } return newList; }
public class MyLinkedList<T extends Comparable<T>> { /* Notice the parameter */ private class Node { private T data; private Node next; private Node(T data) { this.data = data; next = null; /* do we really need to do this? */ } } /* List head pointer */ private Node head; /* We don't actually need it */ public MyLinkedList() { head = null; } /* Adding at the front of the list */ public MyLinkedList<T> add(T data) { Node newNode = new Node(data); newNode.next = head; head = newNode; return this; } public String toString() { String result = "\" "; Node curr = head; while (curr != null) { result += curr.data + " "; curr = curr.next; } return result + "\""; } public boolean find(T target) { Node curr = head; while (curr != null) { if (curr.data.compareTo(target) == 0) { return true; } curr = curr.next; } return false; } public T getLastElement() { if (head == null) { return null; } Node curr = head; while (curr.next != null) { curr = curr.next; } return curr.data; } /* No insertion will take place if list empty or targetElement not found */ public void insertElementAfter(T targetElement, T toInsert) { Node curr = head; while (curr != null) { if (curr.data.compareTo(targetElement) == 0) { Node newNode = new Node(toInsert); newNode.next = curr.next; curr.next = newNode; return; /* what would happen if we don't return? */ } curr = curr.next; } } /* No insertion will take place if list empty or targetElement not found */ public void insertElementBefore(T targetElement, T toInsert) { Node prev = null, curr = head; while (curr != null) { if (curr.data.compareTo(targetElement) == 0) { Node newNode = new Node(toInsert); if (curr == head) { newNode.next = head; head = newNode; } else { prev.next = newNode; newNode.next = curr; } return; } else { prev = curr; curr = curr.next; } } } public void delete(T targetElement) { Node prev = null, curr = head; while (curr != null) { if (curr.data.compareTo(targetElement) == 0) { if (curr == head) { head = head.next; } else { prev.next = curr.next; } return; } else { prev = curr; curr = curr.next; } } } public MyLinkedList<T> getListWithDataInBetween(T start, T end) { MyLinkedList<T> newList = new MyLinkedList<T>(); if (head != null) { Node curr = head, last = null; while (curr != null) { if (curr.data.compareTo(start) >= 0 && curr.data.compareTo(end) <= 0) { Node newNode = new Node(curr.data); if (newList.head == null) { newList.head = newNode; } else { last.next = newNode; } last = newNode; } curr = curr.next; } } return newList; } }