Design Linked List – 707. LeetCode



This problem asks you to design and implement your own linked list from scratch, either as a singly linked list or a doubly linked list, depending on your choice. Here’s a simplified explanation of the requirements:

  • Constructor (MyLinkedList):
  • get(int index)
  • addAtHead(int val)
  • addAtTail(int val)
  • addAtIndex(int index, int val)
  • deleteAtIndex(int index)

Tradeoffs Between Singly and Doubly Linked List

1. Singly Linked List:

• Simpler to implement.

• Requires less memory because each node only stores next.

• Cannot traverse backward.

class SinglyLinkedList {
    class Node {
        int value;
        Node next;

        Node(int value) {
            this.value = value;
        }
    }

    private Node head;

    public SinglyLinkedList() {
        this.head = null;
    }

    public void addAtHead(int val) {
        Node newNode = new Node(val);
        newNode.next = head;
        head = newNode;
    }

    public void addAtTail(int val) {
        if (head == null) {
            head = new Node(val);
            return;
        }
        Node curr = head;
        while (curr.next != null) {
            curr = curr.next;
        }
        curr.next = new Node(val);
    }

    public void deleteAtIndex(int index) {
        if (index == 0 && head != null) {
            head = head.next;
            return;
        }
        Node curr = head;
        for (int i = 0; i < index - 1 && curr != null; i++) {
            curr = curr.next;
        }
        if (curr == null || curr.next == null) return;
        curr.next = curr.next.next;
    }
}

Line-By-Line Walkthrough

Step 1: Create Node

    class Node {
        int value;
        Node next;

        Node(int value) {
            this.value = value;
        }
    }
  • A linked list is made up of nodes, each storing a value and a pointer to the next node.

Step 2: State

    private Node head;
    private int size;
  • The head allows the list to keep track of its starting point for traversals and operations.

Step 3: Constructor

    public SinglyLinkedList() {
        this.head = null;
        this.size = 0;
    }
  • Initializes an empty linked list
  • Sets head to null and size to 0;

Step 4: Get

    public int get(int index) {
        if (index < 0 || index >= size) return -1;
        Node curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr.value;
    }
  • if(index < 0 || index >= size)
    • Validates the index and prevents invalid memory access.
  • Node curr = head;
    • Start traversal at the head of the linked list.
  • for(int i = 0; i < index; i++)
    • Traverses the list until the desired index.

Step 5: Add At Head

    public void addAtHead(int val) {
        Node newNode = new Node(val);
        newNode.next = head;
        head = newNode;
        size++;
    }
  • Node newNode = new Node(val);
    • Creates a new node with the given value
  • newNode.next = head;
    • Links the new node to the current head
  • head = newNode;
    • Updates the head to point to the new node.
  • size++
    • Increment our state so that we can access it later.

Step 5: Add At Tail

    public void addAtTail(int val) {
        if (head == null) {
            head = new Node(val);
        } else {
            Node curr = head;
            while (curr.next != null) {
                curr = curr.next;
            }
            curr.next = new Node(val);
        }
        size++;
    }
  • if(head == null)
    • Handles edge case where the list is empty
  • Node curr = head
    • Assign new pointer so that we don’t manipulate the actual head.
  • while(curr.next != null)
    • Traverses the list to find the last ndoe
  • curr.next = new Node(val)
    • Adds the new node at the end of the list.

Step 6: AddAtIndex

public void addAtIndex(int index, int val) {
    if (index < 0 || index > size) return;
    if (index == 0) {
        addAtHead(val);
    } else {
        Node curr = head;
        for (int i = 0; i < index - 1; i++) {
            curr = curr.next;
        }
        Node newNode = new Node(val);
        newNode.next = curr.next;
        curr.next = newNode;
        size++;
    }
}
  • if(index < 0 || index > size)
    • Validates the index
  • if(index == 0)
    • Add to head
  • for(int i = 0; i < index - 1; i++)
    • Traverses to the node just before the target index
  • newNode.next = curr.next; curr.next = newNode;
    • Inserts the new node at the desired position.

Step 7: Delete At Index

    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) return;
        if (index == 0) {
            head = head.next;
        } else {
            Node curr = head;
            for (int i = 0; i < index - 1; i++) {
                curr = curr.next;
            }
            curr.next = curr.next.next;
        }
        size--;
    }
  • if(index < 0 || index >= size)
    • Validates the index. If invalid, do nothing.
  • if(index == 0)
    • Special case: Adding at the head.
  • for(int i = 0; i < index - 1; i++)
    • Traverses to the node just before the target index.
  • curr.next = curr.next.next;
    • Inserts the new node at the desired position

Doubly-Linked List

2. Doubly Linked List:

• Allows bi-directional traversal, making some operations (e.g., deletion) faster.

• Uses more memory because each node stores prev and next.

• Slightly more complex to manage pointers during insertion and deletion.

class DoublyLinkedList {
    class Node {
        int value;
        Node prev, next;

        Node(int value) {
            this.value = value;
        }
    }

    private Node head, tail;
    private int size;

    public DoublyLinkedList() {
        this.head = new Node(0); // Dummy head
        this.tail = new Node(0); // Dummy tail
        this.head.next = this.tail;
        this.tail.prev = this.head;
        this.size = 0;
    }

    public int get(int index) {
        if (index < 0 || index >= size) return -1;
        Node curr = head.next;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr.value;
    }

    public void addAtHead(int val) {
        Node newNode = new Node(val);
        Node next = head.next;

        newNode.next = next;
        newNode.prev = head;
        head.next = newNode;
        next.prev = newNode;

        size++;
    }

    public void addAtTail(int val) {
        Node newNode = new Node(val);
        Node prev = tail.prev;

        newNode.prev = prev;
        newNode.next = tail;
        prev.next = newNode;
        tail.prev = newNode;

        size++;
    }

    public void addAtIndex(int index, int val) {
        if (index < 0 || index > size) return;
        Node curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        Node newNode = new Node(val);
        Node next = curr.next;

        newNode.prev = curr;
        newNode.next = next;
        curr.next = newNode;
        next.prev = newNode;

        size++;
    }

    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) return;
        Node curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        Node prev = curr;
        Node next = curr.next.next;

        prev.next = next;
        if (next != null) {
            next.prev = prev;
        }

        size--;
    }
}

Line-By-Line Explanation

Step 1: Create Node

    class Node {
        int value;
        Node prev, next;

        Node(int value) {
            this.value = value;
        }
    }
  • Represents an individual node in the list
  • Each node contains:
    • value: The data stored in the node.
    • prev: A reference to the previous node.
    • next: A reference to the next node.

Step 2: State

    private Node head, tail;
    private int size;
  • head: Dummy node representing the start of the list.
  • tail: Dummy node representing the end of the list.
  • size: Help validate indices and improves efficiency when checking the list’s length.

Step 3: Constructor

    public DoublyLinkedList() {
        this.head = new Node(0); // Dummy head
        this.tail = new Node(0); // Dummy tail
        this.head.next = this.tail;
        this.tail.prev = this.head;
        this.size = 0;
    }
  • Dummy nodes simplify operations (adding or removing nodes) by eliminating the need for special cases when the list is empty or has one element

Step 4: Get

    public int get(int index) {
        if (index < 0 || index >= size) return -1;
        Node curr = head.next;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr.value;
    }
  • if(index < 0 || index >= size)
    • Validates the index
  • Node curr = head.next;
    • Starts traversal from the first real node (after the dummy head).
  • for(int i = 0; i < index; i++)
    • Traverses the list until the desired index is reached.

Step 5: Add at head

    public void addAtHead(int val) {
        Node newNode = new Node(val);
        Node next = head.next;

        newNode.next = next;
        newNode.prev = head;
        head.next = newNode;
        next.prev = newNode;

        size++;
    }
  • Node newNode = new Node(val)
    • Creates a new node with the given value.
  • Node next = head.next;
    • Stores a reference to the current first node.
  • Link Updates
    • newNode.next = next: Points the new node to the current first node.
    • newNode.prev = head: Points the new node back to the dummy head.
    • head.next = newNode: Updates the dummy head to point to the new node.
    • next.prev = newNode: Updates the old first node to point back to the new node.
  • size++: Increments the size of the list.

Step 6: Add at tail

    public void addAtTail(int val) {
        Node newNode = new Node(val);
        Node prev = tail.prev;

        newNode.prev = prev;
        newNode.next = tail;
        prev.next = newNode;
        tail.prev = newNode;

        size++;
    }
  • Node newNode = new Node(val);
    • Creates a new node with the given value.
  • Node prev = tail.prev;
    • Retrieves the current last node.
  • Link Updates:
    • newNode.prev = prev: Points the new node back to the current last node.
    • newNode.next = tail: Points the new node forward to the dummy tail.
    • prev.next = newNode: Updates the last node to point to the new node.
    • tail.prev = newNode: Updates the dummy tail to point back to the new node.
  • size++
    • Increments the size of the list.

Step 7: Add At Index

    public void addAtIndex(int index, int val) {
        if (index < 0 || index > size) return;
        Node curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        Node newNode = new Node(val);
        Node next = curr.next;

        newNode.prev = curr;
        newNode.next = next;
        curr.next = newNode;
        next.prev = newNode;

        size++;
    }
  • if(index < 0 || index > size)
    • Validates the index
  • Node curr = head;
    • Assigns pointer so that the head is not directly manipulated during traversal
  • Node newNode = new Node(val)
    • Creates the new node.
  • Link Updates:
    • Same as in addAtHead or addAtTail, but generalized for any position.

Step 8: Delete At Index

    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) return;
        Node curr = head;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        Node prev = curr;
        Node next = curr.next.next;

        prev.next = next;
        if (next != null) {
            next.prev = prev;
        }

        size--;
    }
  • if(index < 0 || index > size)
    • Validates the index
  • Node curr = head;
    • Assigns pointer so that the head is not directly manipulated during traversal
  • Node newNode = new Node(val)
    • Creates the new node.
  • Link Updates:
    • prev.next = next skips the target node.
    • next.prev = prev updates the back pointer

Why This Code Works

• Use singly linked lists when memory efficiency and simple operations are priorities.

• Use doubly linked lists when you need fast insertions/deletions at both ends or bi-directional navigation.

Both implementations demonstrate a clear understanding of data structure fundamentals and strike a balance between performance, clarity, and adaptability, making them excellent solutions for LeetCode and real-world scenarios alike.

Leave a Reply

Your email address will not be published. Required fields are marked *