April 15, 2023
Have you ever wondered what makes a data structure a linked list? Is it the links? The list? Or perhaps the feeling of being lost in a sea of pointers? Well, fear not! In this and subsequent posts, we will explore the world of Linked Lists - from the Singly Linked List to the Doubly Linked List and even the Circular Linked List. So sit back, grab your favorite beverage, and let’s dive in!
A linked list is a linear data structure consisting of a collection of nodes, where each node contains some data and a reference (or pointer) to the next node in the list. This concept remains same whether it is a Singly Linked List, Doubly Linked List or Circular Linked List. Only thing which changes is how the pointers are connected between the nodes.
Below interface represents the common functionalities offered by the List ADT -
import java.util.Iterator;
public interface List<T> {
/**
* @return the number of elements in the list
*/
int size();
/**
* @return true, if the list is empty, false otherwise
*/
boolean isEmpty();
/**
* @param element - element to be checked in the list
* @return true, if the element is present in the list, false otherwise
*/
boolean contains(T element);
/**
* @param element to be added in the list
*/
void add(T element);
/**
* @param element element to be removed from the list
* @return removed element
*/
T remove(T element);
/**
* @return Returns an iterator over the elements in this list in proper sequence.
*/
Iterator<T> iterator();
/**
* Empties the list by removing all the elements from it
*/
void clear();
}
Each type of List represents this ADT differently, but the behavior remains the same. In this post, we will see how Singly Linked List implements the List ADT.
Singly Linked List is the simplest of the Linked Lists. As the name suggests, it is a list where each element is linked to the next one in a single direction. Think of it like a train where each carriage is connected to the next one by a single link. This makes it easy to traverse the list from start to end but not so much the other way around.
It will look like below -
class SinglyListNode {
private int data;
private SinglyListNode next;
SinglyListNode(int data) {
this.data = data;
this.next = null;
}
}
The above Java code defines a class named SinglyListNode
that represents a node in a singly linked list. The class has two instance variables: an integer variable named data
that stores the value of the node and a reference variable named next
that points to the next node in the linked list.
The class also has a constructor with a single integer parameter data
. When a new Node object is created using this constructor, it initializes the data variable with the value of data and sets the next variable to null, which means that the new node does not point to any other node yet.
To create a Singly Linked List, we start with a head node that points to the first element in the list. Each subsequent element is linked to the previous one by a pointer. The last element in the list points to null, indicating the end of the list.
Below is the Java implementation of a Singly Linked List.
import java.util.Iterator;
import java.util.NoSuchElementException;
public class SinglyLinkedList<T> implements List<T>, Iterable<T> {
// Head of the linked list
public SinglyListNode<T> head;
// Size of the list
private int size;
public SinglyLinkedList() {
this.size = 0;
this.head = null;
}
/**
* @return the number of elements in the list
*/
@Override
public int size() {
return this.size;
}
/**
* @return true, if the list is empty, false otherwise
*/
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* @param element - element to be checked in the list
* @return true, if the element is present in the list, false otherwise
*/
@Override
public boolean contains(T element) {
// Reference to the head
SinglyListNode<T> temp = head;
// Loop through the linked list to check each element
while (temp != null) {
if (temp.data.equals(element)) {
return true;
}
temp = temp.next;
}
return false;
}
/**
* @param element to be added in the list at the last
*/
@Override
public void add(T element) {
// Create a new node with the given data
SinglyListNode<T> node = new SinglyListNode<>(element);
// Base case when head is null
if (head == null) {
head = node;
size++;
return;
}
// Reference to the head
SinglyListNode<T> temp = head;
// Loop through the linked list to get the last element
while (temp.next != null) {
temp = temp.next;
}
// Make the next of last node as the new node
temp.next = node;
size++;
}
/**
* @param element to be added at the front of list
*/
public void addFirst(T element) {
// Create a new node with the given data
SinglyListNode<T> node = new SinglyListNode<>(element);
// Make the current head as the next of this node
node.next = head;
// Make this node as the new head
head = node;
size++;
}
/**
* @param element to be added
* @param elementInTheList after which the element is to be added
*/
public void addAfterNode(T element, T elementInTheList) {
// First check if the given node exists in the list or not
if (!contains(elementInTheList)) {
throw new IllegalArgumentException("Given node doesn't exist in the list");
}
// Get the reference of the given node
SinglyListNode<T> temp = head;
while (temp.next != null) {
if (temp.data.equals(elementInTheList)) {
break;
}
temp = temp.next;
}
// Create a new node with the given data
SinglyListNode<T> newNode = new SinglyListNode<>(element);
// Get the next element of this node
SinglyListNode<T> nextNode = temp.next;
// Insert the given node in the list
temp.next = newNode;
// Link this node with the previous next node
newNode.next = nextNode;
size++;
}
/**
* @param element element to be removed from the list
* @return removed element
*/
@Override
public T remove(T element) {
// Check if the node to be removed exists in the list
if (!contains(element)) {
throw new IllegalArgumentException("Given node doesn't exist in the list");
}
// For head
if (head.data.equals(element)) {
head = head.next;
return element;
}
// Find the reference of the given node in the list
SinglyListNode<T> temp = head;
while (temp.next != null) {
if (temp.next.data.equals(element)) {
break;
}
temp = temp.next;
}
// Remove the node by skipping it
if (temp.next != null) {
temp.next = temp.next.next;
}
size--;
return element;
}
/**
* @return last element which will be removed
*/
public T removeLast() {
// Base case
if (head == null) {
throw new IllegalArgumentException("Cannot remove from empty list");
}
if (head.next == null) {
T data = head.data;
head = null;
size--;
return data;
}
// Traverse the list to go to the second last node
// so that the last node can be removed by pointing
// second last node's next to null
SinglyListNode<T> temp = head;
while (temp.next.next != null) {
temp = temp.next;
}
// Store the data of the last node to return
T data = temp.next.data;
// End the list after current second last node
temp.next = null;
// Update the size
size--;
return data;
}
/**
* @return removed element from the list from the front
*/
public T removeFirst() {
// Base case
if (head == null) {
throw new IllegalArgumentException("Cannot remove from the empty list");
}
// Get the data at head
T data = head.data;
// Move head to the next pointer
head = head.next;
size--;
return data;
}
/**
* Empties the list by removing all the elements from it
*/
@Override
public void clear() {
head = null;
size = 0;
}
/**
* @return Returns an iterator over the elements in this list in proper sequence.
*/
@Override
public Iterator<T> iterator() {
return new Iterator<>() {
// Reference of the head
SinglyListNode<T> current = head;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public T next() {
if (current == null) {
throw new NoSuchElementException();
}
// Data from the current node
T data = current.data;
current = current.next;
return data;
}
};
}
/**
* Returns a string representation of the object.
*/
@Override
public String toString() {
StringBuilder output = new StringBuilder();
output.append("[");
// Reference of the head
SinglyListNode<T> temp = head;
while (temp.next != null) {
output.append(temp.data).append(", ");
temp = temp.next;
}
output.append(temp.data).append("]");
return output.toString();
}
static class SinglyListNode<T> {
T data;
SinglyListNode<T> next;
SinglyListNode(T data) {
this.data = data;
}
}
}
The class provides several methods to manipulate the linked list, including:
size()
Returns the number of elements in the list.
isEmpty()
Returns true if the list is empty, false otherwise.
contains(T element)
Returns true if the list contains the specified element, false otherwise.
add(T element)
Adds the specified element to the end of the list.
addFirst(T element)
Adds the specified element to the beginning of the list.
addAfterNode(T element, T elementInTheList)
Adds the specified element after the specified node in the list.
remove(T element)
Removes the first occurrence of the specified element from the list.
removeLast()
Removes and returns the last element in the list.
removeFirst()
Removes and returns the first element in the list.
The implementation of these methods uses a combination of iteration and manipulation of the head
and next
fields of the nodes in the linked list.
The class also implements the Iterable
interface, which allows instances of SinglyLinkedList
to be iterated over using a for-each loop. The Iterator object returned by the iterator()
method provides a way to iterate over the elements in the list in a forward direction.
Now, let’s take it up a notch with the Doubly Linked List. As the name suggests, this list has two links - one to the next element and one to the previous element. Think of it like a train with carriages that are connected to the one in front and the one behind it. This makes it easy to traverse the list in both directions, but it also means that we need to maintain two pointers per node instead of one.
It will look like below -
To create a Doubly Linked List, we start with a head node that points to the first element in the list, just like in the Singly Linked List. Each subsequent element has two pointers - one to the previous node and one to the next node. The last element points to null
, just like before. Here’s a code snippet to give you an idea:
class DoublyListNode<T> {
private T data;
private DoublyListNode<T> previous;
private DoublyListNode<T> next;
DoublyListNode(T data) {
this.data = data;
}
|
As you can see, each node now has an additional pointer to the previous node. This makes it easier to traverse the list in both directions, but it also means that we need to be careful when inserting or deleting nodes to maintain the links properly.
The above code defines a generic class named DoublyListNode
which represents a node in a doubly linked list. The class has three private instance variables - data
, previous
, and next
.
The first instance variable data
is of generic type T
and it stores the data associated with the node.
The second instance variable previous
is of type DoublyListNode<T>
and it refers to the previous node in the doubly linked list.
The third instance variable next
is also of type DoublyListNode<T>
and it refers to the next node in the doubly linked list.
The class has one constructor that takes the data
as an argument and initializes it to the data instance variable. The previous
and next
variables are initialized to null by default.
Overall, this class represents a node in a doubly linked list and stores the associated data along with references to the previous and next nodes.
A typical node in the circular linked list would look like below -
class CircularListNode<T> {
T data;
CircularListNode<T> next;
CircularListNode(T data) {
this.data = data;
}
}
Below is the Java implementation of a Doubly Linked List.
import java.util.Iterator;
import java.util.NoSuchElementException;
public class DoublyLinkedList<T> implements List<T>, Iterable<T> {
// Head of the linked list
public DoublyListNode<T> head;
// Size of list
private int size;
public DoublyLinkedList() {
this.size = 0;
this.head = null;
}
/**
* @return the number of elements in the list
*/
@Override
public int size() {
return this.size;
}
/**
* @return true, if the list is empty, false otherwise
*/
@Override
public boolean isEmpty() {
return this.size == 0;
}
/**
* @param element - element to be checked in the list
* @return true, if the element is present in the list, false otherwise
*/
@Override
public boolean contains(T element) {
// Check each node one by one
DoublyListNode<T> temp = head;
while (temp != null) {
if (temp.data.equals(element)) {
return true;
}
temp = temp.next;
}
return false;
}
/**
* @param element to be added in the list at the end
*/
@Override
public void add(T element) {
// Create a new node with the given element
DoublyListNode<T> node = new DoublyListNode<>(element);
// Special case
if (head == null) {
head = node;
size++;
return;
}
// Reference of the head node
DoublyListNode<T> temp = head;
// Loop until we reach to the end of the list
while (temp.next != null) {
temp = temp.next;
}
temp.next = node;
node.previous = temp;
size++;
}
/**
* @param element to be added at the front of the list
*/
public void addFirst(T element) {
// Create a new node with the given data
DoublyListNode<T> node = new DoublyListNode<>(element);
// Special case
if (head == null) {
head = node;
size++;
return;
}
node.next = head;
head.previous = node;
head = node;
size++;
}
/**
* @param element to be added
* @param elementInTheList after which the element is to be added
*/
public void addAfterNode(T element, T elementInTheList) {
// First check if the given node exists in the list or not
if (!contains(elementInTheList)) {
throw new IllegalArgumentException("Given node doesn't exist in the list");
}
// Get the reference of the given node
DoublyListNode<T> temp = head;
while (temp.next != null) {
if (temp.data.equals(elementInTheList)) {
break;
}
temp = temp.next;
}
// Create a new node with the given data
DoublyListNode<T> newNode = new DoublyListNode<>(element);
// Get the next element of this node
DoublyListNode<T> nextNode = temp.next;
//Link the new node with previous of next node
nextNode.previous = newNode;
// Insert the given node in the list
temp.next = newNode;
newNode.previous = temp;
// Link this node with the previous next node
newNode.next = nextNode;
size++;
}
/**
* @param element element to be removed from the list
* @return removed element
*/
@Override
public T remove(T element) {
// Check if the node to be removed exists in the list
if (!contains(element)) {
throw new IllegalArgumentException("Given node doesn't exist in the list");
}
// For head
if (head.data.equals(element)) {
head = head.next;
head.previous = null;
return element;
}
// Find the reference of the given node in the list
DoublyListNode<T> temp = head;
while (temp.next != null) {
if (temp.next.data.equals(element)) {
break;
}
temp = temp.next;
}
// Remove the node by skipping it
if (temp.next != null) {
// Next node of given node
temp.next = temp.next.next;
}
size--;
return element;
}
/**
* @return removed element from the last
*/
public T removeLast() {
// Base case
if (head == null) {
throw new IllegalArgumentException("Cannot remove from empty list");
}
if (head.next == null) {
T data = head.data;
head = null;
size--;
return data;
}
// Traverse the list to go to the lost
DoublyListNode<T> temp = head;
while (temp.next.next != null) {
temp = temp.next;
}
T data = temp.next.data;
temp.next = null;
size--;
return data;
}
/**
* @return removed element from the list from the front
*/
public T removeFirst() {
// Base case
if (head == null) {
throw new IllegalArgumentException("Cannot remove from the empty list");
}
// Get the data at head
T data = head.data;
// Move head to the next pointer
head = head.next;
if (head != null) {
head.previous = null;
}
size--;
return data;
}
/**
* Empties the list by removing all the elements from it
*/
@Override
public void clear() {
size = 0;
head = null;
}
/**
* @return Returns an iterator over the elements in this list in proper sequence.
*/
@Override
public Iterator<T> iterator() {
return new Iterator<>() {
// Reference of the head
DoublyListNode<T> current = head;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public T next() {
if (current == null) {
throw new NoSuchElementException();
}
// Data from the current node
T data = current.data;
current = current.next;
return data;
}
};
}
/**
* Returns a string representation of the object.
*/
@Override
public String toString() {
StringBuilder output = new StringBuilder();
output.append("[");
// Reference of the head
DoublyListNode<T> temp = head;
while (temp.next != null) {
output.append(temp.data).append(", ");
temp = temp.next;
}
output.append(temp.data).append("]");
return output.toString();
}
static class DoublyListNode<T> {
T data;
DoublyListNode<T> previous;
DoublyListNode<T> next;
DoublyListNode(T data) {
this.data = data;
}
}
}
This implementation provides the basic methods for the doubly linked list:
int size()
returns the size of the doubly linked list, which is the number of nodes in the list.
boolean isEmpty()
returns true if the doubly linked list is empty (size = 0), and false otherwise.
boolean contains(T element)
returns true if the doubly linked list contains the specified element, and false otherwise. It traverses the list and checks each node one by one.
void add(T element)
adds the specified element to the end of the doubly linked list. It creates a new node with the given element and inserts it at the end of the list.
void addFirst(T element)
adds the specified element to the front of the doubly linked list. It creates a new node with the given element and inserts it at the front of the list.
void addAfterNode(T element, T elementInTheList)
adds the specified element after the specified node in the doubly linked list. It first checks if the specified node exists in the list or not, then finds its reference, and inserts the new node after it.
T remove(T element)
removes the first occurrence of the specified element from the doubly linked list. It first checks if the specified element exists in the list or not, then removes it by skipping its reference.
T removeLast()
removes the last element from the doubly linked list. It traverses the list to the last element, removes it by setting its previous node’s next reference to null.
T removeFirst()
removes the first element from the doubly linked list. It removes the head node and moves the head to the next node.
Last but not least, we have the Circular Linked List. As the name suggests, this list is Circular - meaning that it forms a loop. Think of it like a circular train track where the last carriage is connected to the first one. This makes it easy to traverse the list endlessly, but it also means that we need to be careful not to get stuck in an infinite loop.
Circular linked lists have various applications, such as in scheduling algorithms, where a circular queue is required. They are also useful in data structures where a particular sequence needs to be repeated over and over again, such as in animation and game development.
Circular linked lists are similar to regular linked lists in terms of implementation, but there are a few key differences. For example, when traversing a Circular linked list, we need to be careful not to fall into an infinite loop. To avoid this, we can use a slow pointer and a fast pointer. The slow pointer moves one node at a time, while the fast pointer moves two nodes at a time. If the two pointers ever point to the same node, we know that we have found a cycle.
import java.util.Iterator;
public class CircularLinkedList<T> implements List<T>, Iterable<T> {
// Head of the list
public CircularListNode<T> head;
// Tail of the linked list
public CircularListNode<T> tail;
// Size of the linked list
private int size;
public CircularLinkedList() {
this.size = 0;
this.head = null;
this.tail = null;
}
/**
* @return the number of elements in the list
*/
@Override
public int size() {
return this.size;
}
/**
* @return true, if the list is empty, false otherwise
*/
@Override
public boolean isEmpty() {
return size == 0;
}
/**
* @param element - element to be checked in the list
* @return true, if the element is present in the list, false otherwise
*/
@Override
public boolean contains(T element) {
// Base case
if (head == null) {
return false;
} else {
// Reference to the head
CircularListNode<T> temp = head;
do {
if (temp.data.equals(element)) {
return true;
}
temp = temp.next;
} while (temp != head);
}
return false;
}
/**
* @param element to be added in the list at the last position
*/
@Override
public void add(T element) {
// Create a new node with the given data
CircularListNode<T> node = new CircularListNode<>(element);
// For empty list
if (head == null) {
head = node;
tail = node;
tail.next = head;
size++;
return;
}
// Current tail
CircularListNode<T> currentTail = tail;
// Point next of current tail to the new node that we created.
currentTail.next = node;
// Update the tail
tail = node;
// Join the tail with head
tail.next = head;
size++;
}
/**
* @param element to be added
* @param elementInTheList element after which we need to add
*/
public void addAfterNode(T element, T elementInTheList) {
// First check if the given node exists in the list or not
if (!contains(elementInTheList)) {
throw new IllegalArgumentException("Given node doesn't exist in the list");
}
// Reference of the first node
CircularListNode<T> temp = head;
// Traverse through the list to find out the given
// elementInTheList node and stop the iteration.
do {
if (temp.data.equals(elementInTheList)) {
break;
}
temp = temp.next;
} while (temp != head);
// Create a new node with the given data
CircularListNode<T> newNode = new CircularListNode<>(element);
if (temp == tail) {
tail = newNode;
tail.next = head;
}
// Get the next element of this node
CircularListNode<T> nextNode = temp.next;
// Insert the given node in the list
temp.next = newNode;
// Link this node with the previous next node
newNode.next = nextNode;
size++;
}
/**
* @param element element to be removed from the list
* @return removed element
*/
@Override
public T remove(T element) {
// Check if the node to be removed exists in the list
if (!contains(element)) {
throw new IllegalArgumentException("Given node doesn't exist in the list");
}
// For head
if (head.data.equals(element)) {
head = head.next;
size--;
return element;
}
// Find the reference of the given node in the list
CircularListNode<T> temp = head;
do {
if (temp.next.data.equals(element)) {
break;
}
temp = temp.next;
} while (temp.next != head);
// Check if the removed element is the tail
if (temp.next == tail) {
tail = temp;
}
// Remove the node by skipping it
temp.next = temp.next.next;
size--;
return element;
}
/**
* Empties the list by removing all the elements from it
*/
@Override
public void clear() {
head = null;
tail = null;
size = 0;
}
/**
* @return Returns an iterator over the elements in this list in proper sequence.
*/
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
// Reference of the first node
CircularListNode<T> current = tail.next;
@Override
public boolean hasNext() {
return current != tail;
}
@Override
public T next() {
T data = current.data;
current = current.next;
return data;
}
};
}
/**
* Returns a string representation of the object.
*/
@Override
public String toString() {
StringBuilder output = new StringBuilder();
output.append("[");
// Reference of the head
CircularListNode<T> temp = head;
do {
output.append(temp.data).append(", ");
temp = temp.next;
} while (temp.next != head);
output.append(temp.data).append("]");
return output.toString();
}
static class CircularListNode<T> {
T data;
CircularListNode<T> next;
CircularListNode(T data) {
this.data = data;
}
}
}
The above class has the following methods:
int size()
This method returns the number of elements in the list.
boolean isEmpty()
This method returns true if the list is empty, and false otherwise.
boolean contains(T element)
This method checks if the list contains a given element. It returns true if the element is present in the list, and false otherwise.
void add(T element)
This method adds an element to the end of the list.
void addAfterNode(T element, T elementInTheList)
This method adds an element after a specified node in the list.
T remove(T element)
This method removes a specified element from the list.
void clear()
This method empties the list by removing all elements.
Iterator<T> iterator()
This method returns an iterator over the elements in the list in proper sequence.
String toString()
This method returns a string representation of the list.
The CircularListNode
class is a static inner class of CircularLinkedList
, which represents a node in the linked list. Each node has a data field and a next field, which points to the next node in the list.
Overall, this implementation of a circular linked list provides basic functionality for creating, manipulating, and accessing elements in the list.
In conclusion, Singly, Doubly, and Circular Linked Lists are three fundamental data structures used in computer science and programming. Singly linked lists are useful for applications where forward traversal is a common operation, and memory usage needs to be minimized. Doubly linked lists allow for both forward and backward traversal and are useful when bidirectional movement is required. Circular linked lists provide a closed loop where the last node points to the first node, and they can be used to implement various algorithms such as round-robin scheduling.
In terms of implementation, all three types of linked lists are relatively straightforward, and their code can be written in various programming languages. Understanding the workings and code of these data structures is essential for any programmer, as linked lists provide a foundation for more complex data structures and algorithms.
Overall, Singly, Doubly, and Circular Linked Lists are powerful data structures that are widely used in computer science and programming. By mastering these data structures, programmers can gain a deeper understanding of how data is organized and accessed in computer systems, which is crucial for developing efficient and effective algorithms.
You can find the complete code in the GitHub repository. I hope you enjoyed the post. Feel free to share your thoughts. Till next time… Happy learning 😄 and Namaste 🙏!