← Back to Home

Module 4: Linked Lists

Module Overview

Understanding and implementing linked list data structures in Java. Learn about the mechanics, benefits, and trade-offs of using linked lists versus other collection types.

Learning Objectives

Video Content: Linked Lists Introduction

This video introduces the concept of linked lists, explaining their structure, basic operations, and comparative advantages over array-based lists in certain scenarios.

// Basic Node class for a singly linked list
public class Node<T> {
    private T data;
    private Node<T> next;
    
    public Node(T data) {
        this.data = data;
        this.next = null;
    }
    
    public T getData() {
        return data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
    
    public Node<T> getNext() {
        return next;
    }
    
    public void setNext(Node<T> next) {
        this.next = next;
    }
}

// Simple LinkedList implementation
public class LinkedList<T> {
    private Node<T> head;
    private int size;
    
    public LinkedList() {
        head = null;
        size = 0;
    }
    
    // Add to the beginning - O(1) operation
    public void addFirst(T data) {
        Node<T> newNode = new Node<>(data);
        newNode.setNext(head);
        head = newNode;
        size++;
    }
    
    // Get element at index - O(n) operation
    public T get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }
        
        Node<T> current = head;
        for (int i = 0; i < index; i++) {
            current = current.getNext();
        }
        
        return current.getData();
    }
}

This example shows a basic implementation of a singly linked list with a Node class and essential operations. Note how adding to the beginning is an O(1) operation (constant time) because it only requires updating the head reference, while accessing an element by index is an O(n) operation (linear time) as we must traverse from the head to the desired index.

Video Content: LinkedList in Java Collections

This video discusses Java's built-in LinkedList implementation in the Collections framework, comparing it with ArrayList and examining when to use each.

import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;

public class ListPerformanceComparison {
    
    public static void main(String[] args) {
        // Creating both types of lists
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();
        
        // Measuring insertion time at the beginning
        long startTime = System.nanoTime();
        
        // ArrayList insertion at beginning (slow - O(n))
        for (int i = 0; i < 100000; i++) {
            arrayList.add(0, i);  // Have to shift all elements
        }
        
        long endTime = System.nanoTime();
        System.out.println("ArrayList insertion at beginning: " + 
            (endTime - startTime) / 1000000 + " ms");
        
        startTime = System.nanoTime();
        
        // LinkedList insertion at beginning (fast - O(1))
        for (int i = 0; i < 100000; i++) {
            linkedList.add(0, i);  // Just update references
        }
        
        endTime = System.nanoTime();
        System.out.println("LinkedList insertion at beginning: " + 
            (endTime - startTime) / 1000000 + " ms");
        
        // Random access performance (get by index)
        startTime = System.nanoTime();
        
        // ArrayList random access (fast - O(1))
        for (int i = 0; i < 10000; i++) {
            int index = (int) (Math.random() * arrayList.size());
            arrayList.get(index);  // Direct index calculation
        }
        
        endTime = System.nanoTime();
        System.out.println("ArrayList random access: " + 
            (endTime - startTime) / 1000000 + " ms");
        
        startTime = System.nanoTime();
        
        // LinkedList random access (slow - O(n))
        for (int i = 0; i < 10000; i++) {
            int index = (int) (Math.random() * linkedList.size());
            linkedList.get(index);  // Must traverse from head/tail
        }
        
        endTime = System.nanoTime();
        System.out.println("LinkedList random access: " + 
            (endTime - startTime) / 1000000 + " ms");
    }
}

This code demonstrates performance differences between ArrayList and LinkedList. LinkedList excels at insertions/deletions at the beginning or end (O(1) operations) but is inefficient for random access (O(n) operations). ArrayList has the opposite characteristics: fast random access (O(1)) but slow insertions/deletions that require shifting elements (O(n)).

Video Content: Implementing a Linked List

This video walks through building a custom linked list implementation, demonstrating key operations and design considerations.

// More complete linked list implementation
public class CustomLinkedList<E> {
    
    private static class Node<E> {
        E data;
        Node<E> next;
        
        Node(E data) {
            this.data = data;
            this.next = null;
        }
    }
    
    private Node<E> head;
    private Node<E> tail;
    private int size;
    
    public CustomLinkedList() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }
    
    // Add to the end - with tail reference this is O(1)
    public void add(E element) {
        Node<E> newNode = new Node<>(element);
        
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode;
            tail = newNode;
        }
        
        size++;
    }
    
    // Add at specific index - O(n) in worst case
    public void add(int index, E element) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }
        
        if (index == 0) {
            addFirst(element);
            return;
        }
        
        if (index == size) {
            add(element);
            return;
        }
        
        Node<E> current = head;
        for (int i = 0; i < index - 1; i++) {
            current = current.next;
        }
        
        Node<E> newNode = new Node<>(element);
        newNode.next = current.next;
        current.next = newNode;
        size++;
    }
    
    // Add to the beginning - O(1)
    public void addFirst(E element) {
        Node<E> newNode = new Node<>(element);
        
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            newNode.next = head;
            head = newNode;
        }
        
        size++;
    }
    
    // Get element at index - O(n)
    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index);
        }
        
        Node<E> current = head;
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        
        return current.data;
    }
    
    // Remove first element - O(1)
    public E removeFirst() {
        if (head == null) {
            throw new NoSuchElementException();
        }
        
        E data = head.data;
        head = head.next;
        
        if (head == null) {
            tail = null;
        }
        
        size--;
        return data;
    }
}

This more comprehensive implementation demonstrates key linked list operations with time complexity analysis. Note the use of tail reference to make adding to the end an O(1) operation. The code includes methods for inserting elements at different positions, retrieving elements by index, and removing elements, each with appropriate time complexity considerations.

Video Content: Sprint 14 Linked Lists Overview

This video provides a comprehensive overview of linked lists, summarizing key concepts, implementations, and use cases covered in this module.

// Example: Choosing the right list implementation for the job
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class ListSelectionExample {
    
    // Queue/stack operations (add/remove at ends) - use LinkedList
    public static void queueOperationExample() {
        Deque<String> queue = new LinkedList<>();
        
        // Add to end (enqueue)
        queue.addLast("First");
        queue.addLast("Second");
        queue.addLast("Third");
        
        // Remove from front (dequeue)
        String first = queue.removeFirst();  // "First"
        String second = queue.removeFirst(); // "Second"
    }
    
    // Random access and modification - use ArrayList
    public static void randomAccessExample() {
        List<Integer> numbers = new ArrayList<>();
        
        // Fill with data
        for (int i = 0; i < 1000; i++) {
            numbers.add(i);
        }
        
        // Fast random access
        int value = numbers.get(500);  // O(1) operation
        
        // Update value at specific index
        numbers.set(500, 9999);        // O(1) operation
    }
    
    // Frequent insertions/deletions in the middle - consider choices carefully
    public static void frequentModificationExample() {
        // If most operations are at the beginning/end
        LinkedList<String> linkedNames = new LinkedList<>();
        
        // If most operations are random access with occasional insertions
        ArrayList<String> arrayNames = new ArrayList<>();
        
        // LinkedList is better when you have a reference to the node
        // and can directly insert/remove without traversal
        // But often ArrayList is still faster overall due to cache locality
        // and lower overhead, unless insertions/deletions dominate
    }
}

This code demonstrates practical scenarios for choosing between LinkedList and ArrayList. LinkedList is optimal for queue/stack operations (adding/removing at ends) and for frequent insertions in the middle when you already have a reference to the position. ArrayList excels with random access and performs better overall in many scenarios due to memory locality and lower overhead per element.

Resources