Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 35

Singly Linked List Fundamentals

Recommended resources

Hello! In our previous lesson, we dove into recursion, a powerful problem-solving technique where a function calls itself. You learned how the call stack manages these calls and practiced with classic examples like factorial and string reversal. This foundation is essential as we move into more complex data structures.

Today, we'll build our first data structure from scratch: the singly linked list. Your goal for this lesson is to implement a basic singly linked list in Java, complete with methods for insertion, deletion, and traversal. While many linked list operations can be solved elegantly with recursion, we will focus on an iterative approach first. This will help you master the fundamental mechanics of pointer manipulation, a critical skill for any developer working with low-level data structures.

This lesson is a practical, hands-on exercise in object-oriented programming, directly applying your knowledge of classes and objects to a core computer science concept. Let's get started.

1. The Anatomy of a Linked List

Unlike an array, which stores elements in a single, contiguous block of memory, a linked list is a dynamic data structure where elements are scattered across memory. Each element, called a node, contains two pieces of information:

  1. The data it holds.
  2. A reference (or "pointer") to the next node in the sequence.

This chain of nodes forms the list. The entry point to the list is a special pointer called the head, which points to the very first node. The last node in the chain points to null, signaling the end of the list.

This diagram illustrates a singly linked list. Each node consists of data and an address (pointer) to the next node. The `Head` pointer marks the beginning, and the final node's pointer is `NULL`, indicating the end of the list.

Linked lists have distinct trade-offs compared to arrays.

Java Custom Linked List Implementation - Java Code Geeks

This article provides a concise introduction to linked lists and their advantages and disadvantages compared to arrays.

Read the introduction and the subsections on advantages and disadvantages. Focus on understanding why linked lists excel at insertions and deletions but are slower for search operations.


2. The Building Blocks: Node and LinkedList Classes

To implement a linked list in Java, we need two classes:

  1. A Node class to represent each element.
  2. A LinkedList class to manage the overall structure and operations.

Since you are aiming for Java mastery in a backend context, we'll use generics (<T>) for our implementation. This allows our linked list to hold any type of data (Strings, Integers, custom objects) in a type-safe way, which is a standard practice in modern Java.

First, let's define the Node. It's a simple container. We'll make it a private inner class within our LinkedList because its existence is solely to support the list itself.

// This will be inside the LinkedList class
private class Node {
    private T value;
    private Node next;

    public Node(T value) {
        this.value = value;
        // this.next is null by default
    }

    public Node(T value, Node next) {
        this.value = value;
        this.next = next;
    }
}

Next, we define the LinkedList class itself. It will manage the nodes using head and tail pointers. We also include a size field to keep track of the number of elements, which is an O(1) way to know the list's length.

The following video from Kunal Kushwaha provides an excellent walkthrough of setting up these classes.

Linked List Tutorial - Singly + Doubly + Circular (Theory + Code + Implementation)

This segment shows how to structure the Node and LinkedList classes in Java, including the head, tail, and size fields.

Watch the section from creating the classes. Pay attention to why the Node class is private and how the main LinkedList class holds the essential head, tail, and size properties.

Here's the skeleton for our CustomLinkedList class based on this structure:

public class CustomLinkedList<T> {

    private Node head;
    private Node tail;
    private int size;

    public CustomLinkedList() {
        this.size = 0;
    }

    // --- Inner Node Class ---
    private class Node {
        private T value;
        private Node next;

        public Node(T value) {
            this.value = value;
        }

        public Node(T value, Node next) {
            this.value = value;
            this.next = next;
        }
    }

    // --- We will add our methods here ---
}

3. Core Operations: Traversal, Insertion, and Deletion

Now, let's implement the methods that bring our linked list to life.

Traversal: Displaying the List

To see the contents of our list, we need to traverse it from head to tail. A common mistake is to move the head pointer itself. This would permanently modify the list, effectively losing the beginning of your list after one traversal!

Instead, we use a temporary temp node that starts at head and iterates through the list.

Linked List Tutorial - Singly + Doubly + Circular (Theory + Code + Implementation)

This video segment explains why using a temporary variable for traversal is crucial and demonstrates how to implement a display method.

Watch from how to display the list. The explanation of why you should not modify the head pointer is a critical concept to grasp.

Here is the code for the display method:

public void display() {
    Node temp = head;
    while (temp != null) {
        System.out.print(temp.value + " -> ");
        temp = temp.next;
    }
    System.out.println("END");
}

Insertion

There are three main places to insert a node: at the beginning, at the end, or at a specific index.

1. Inserting at the Beginning (insertFirst)

This is the most efficient insertion. We create a new node, make it point to the current head, and then update head to be this new node.

To insert a new node (E) at the beginning, its `Next` pointer is set to the current head (A), and the `Head` pointer is updated to point to the new node (E).

The logic is explained clearly in this part of the video.

Linked List Tutorial - Singly + Doubly + Circular (Theory + Code + Implementation)

This segment provides a whiteboard explanation and codes the insertFirst method.

Watch the detailed breakdown from inserting the first item. Note how it handles the special case where the list is initially empty.

Here's the implementation:

public void insertFirst(T val) {
    Node node = new Node(val);
    node.next = head;
    head = node;

    if (tail == null) {
        tail = head;
    }
    size++;
}

2. Inserting at the End (insertLast)

If we only had a head pointer, we'd have to traverse the entire list (an O(n) operation) to find the last node. By maintaining a tail pointer, we can do this in constant time, O(1).

Linked List Tutorial - Singly + Doubly + Circular (Theory + Code + Implementation)

This segment shows how to efficiently insert a node at the end using the tail pointer.

Watch the explanation from inserting at the last. The video emphasizes the performance benefit of using a tail pointer.

The implementation is straightforward:

public void insertLast(T val) {
    if (tail == null) {
        // If the list is empty, inserting last is the same as inserting first
        insertFirst(val);
        return;
    }
    Node node = new Node(val);
    tail.next = node;
    tail = node;
    size++;
}

3. Inserting at a Specific Index (insert)
This is more involved. To insert a node at index i, we must traverse to the node at index i-1, and then carefully rewire the pointers.

Linked List Tutorial - Singly + Doubly + Circular (Theory + Code + Implementation)

This video covers the logic for inserting a node at an arbitrary index.

Watch from inserting at an index. Focus on the loop that finds the node before the target insertion point.

Here's the code:

public void insert(T val, int index) {
    if (index == 0) {
        insertFirst(val);
        return;
    }
    if (index == size) {
        insertLast(val);
        return;
    }
    
    Node temp = head;
    // Traverse to the node at index - 1
    for (int i = 1; i < index; i++) {
        temp = temp.next;
    }

    Node node = new Node(val, temp.next);
    temp.next = node;
    size++;
}

Deletion

Deletion follows a similar logic to insertion, requiring careful pointer manipulation.

1. Deleting the First Node (deleteFirst)
This is simple: move the head pointer to the next node. The garbage collector will eventually clean up the old head node.

Linked List Tutorial - Singly + Doubly + Circular (Theory + Code + Implementation)

This segment covers deleting the first element and handling the case where the list becomes empty.

Watch from deleting the first element.

2. Deleting the Last Node (deleteLast)
This operation highlights a key limitation of a singly linked list. To delete the last node, you must set the next pointer of the second-to-last node to null. This requires an O(n) traversal to find that node.

Linked List Tutorial - Singly + Doubly + Circular (Theory + Code + Implementation)

This explains the process of deleting the last node, which involves finding the second-to-last node.

Watch from deleting the last item.

3. Deleting at a Specific Index (delete)
Similar to insertion, you must find the node before the one you want to delete and update its next pointer to "skip over" the deleted node.

Linked List Tutorial - Singly + Doubly + Circular (Theory + Code + Implementation)

This video walks through deleting a node at a given index.

Watch the logic from deleting from an index.


4. Your Turn: A Complete Implementation

Below is the complete CustomLinkedList class with all the methods we've discussed. Your task is to study this code, compile it, and run it. Before you run the main method, try to trace the operations on paper and predict the output at each display() call. This exercise will solidify your understanding of how the pointers change with each operation.

Create a file CustomLinkedList.java:

public class CustomLinkedList<T> {

    private Node head;
    private Node tail;
    private int size;

    public CustomLinkedList() {
        this.size = 0;
    }

    public void insertFirst(T val) {
        Node node = new Node(val);
        node.next = head;
        head = node;

        if (tail == null) {
            tail = head;
        }
        size++;
    }

    public void insertLast(T val) {
        if (tail == null) {
            insertFirst(val);
            return;
        }
        Node node = new Node(val);
        tail.next = node;
        tail = node;
        size++;
    }

    public void insert(T val, int index) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        if (index == 0) {
            insertFirst(val);
            return;
        }
        if (index == size) {
            insertLast(val);
            return;
        }

        Node temp = head;
        for (int i = 1; i < index; i++) {
            temp = temp.next;
        }

        Node node = new Node(val, temp.next);
        temp.next = node;
        size++;
    }
    
    public T deleteFirst() {
        if (size == 0) {
            throw new RuntimeException("List is empty");
        }
        T value = head.value;
        head = head.next;
        if (head == null) {
            tail = null;
        }
        size--;
        return value;
    }

    public T deleteLast() {
        if (size <= 1) {
            return deleteFirst();
        }

        Node secondLast = get(size - 2);
        T value = tail.value;
        tail = secondLast;
        tail.next = null;
        size--;
        return value;
    }

    public T delete(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        if (index == 0) {
            return deleteFirst();
        }
        if (index == size - 1) {
            return deleteLast();
        }

        Node prev = get(index - 1);
        T value = prev.next.value;
        prev.next = prev.next.next;
        size--;
        return value;
    }

    // Helper method to get the node at a specific index
    public Node get(int index) {
        Node node = head;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        return node;
    }

    public void display() {
        Node temp = head;
        while (temp != null) {
            System.out.print(temp.value + " -> ");
            temp = temp.next;
        }
        System.out.println("END");
    }

    private class Node {
        private T value;
        private Node next;

        public Node(T value) {
            this.value = value;
        }

        public Node(T value, Node next) {
            this.value = value;
            this.next = next;
        }
    }
}

Create a Main.java file to test it:

public class Main {
    public static void main(String[] args) {
        CustomLinkedList<Integer> list = new CustomLinkedList<>();

        System.out.println("--- Inserting elements ---");
        list.insertFirst(3);
        list.insertFirst(8);
        list.insertFirst(2);
        list.insertFirst(17);
        list.display(); // Predict: 17 -> 2 -> 8 -> 3 -> END

        list.insertLast(99);
        list.display(); // Predict: 17 -> 2 -> 8 -> 3 -> 99 -> END

        list.insert(100, 3);
        list.display(); // Predict: 17 -> 2 -> 8 -> 100 -> 3 -> 99 -> END
        
        System.out.println("\n--- Deleting elements ---");
        System.out.println("Deleted first: " + list.deleteFirst());
        list.display(); // Predict: 2 -> 8 -> 100 -> 3 -> 99 -> END

        System.out.println("Deleted last: " + list.deleteLast());
        list.display(); // Predict: 2 -> 8 -> 100 -> 3 -> END

        System.out.println("Deleted from index 2: " + list.delete(2));
        list.display(); // Predict: 2 -> 8 -> 3 -> END
    }
}

Conclusion

Congratulations on building a singly linked list from scratch! This is a significant milestone in your journey to mastering core Java and data structures. You've gone beyond just knowing what a linked list is to understanding how it works under the hood.

Key Takeaways:

  • A linked list is a chain of Node objects, each containing data and a reference to the next node.
  • The head pointer is the entry point, and the last node points to null. Maintaining a tail pointer optimizes insertion at the end.
  • Traversal must be done with a temporary pointer to avoid modifying the list's structure.
  • Insertion and deletion involve carefully re-assigning the next pointers of the surrounding nodes.
  • Operations at the beginning are O(1), while operations at the end or in the middle are O(n) for a singly linked list (unless you have a tail pointer for insertion).

In our next lesson, we'll explore two other fundamental data structures: stacks and queues. You'll see how to use Java's built-in Deque interface, which can be implemented using a linked list, to model Last-In, First-Out (LIFO) and First-In, First-Out (FIFO) behaviors.