Skip to content

Linked List#

A linked list stores elements in nodes where each node points to the next (singly linked), and optionally to the previous (doubly linked). Unlike arrays, there is no O(1) random access, but insert/delete at a known node is O(1).

When to use linked lists#

Prefer linked list (conceptually) Prefer Python list
Frequent insert/delete at iterator position Random access by index
Implement queue/stack from scratch Built-in ops suffice
LRU cache / interleaved structure Simple sequential storage

In Python interviews you rarely implement linked lists — but you must understand pointer manipulation for problems like reverse list, merge lists, cycle detection.

Dummy head trick: dummy = Node(0); dummy.next = head removes special cases when the head changes (reverse, merge, remove nth from end). Always state this before coding.

Canonical FAANG problems: reverse (LC 206), merge sorted lists (LC 21), cycle detection (LC 141/142), reorder list (LC 143), palindrome (LC 234). See also Fast and Slow Pointers.

Complexity (n nodes)#

Operation Singly Doubly
Access by index O(n) O(n)
Search O(n) O(n)
Insert/delete at head O(1) O(1)
Insert/delete at tail (with tail ptr) O(1) O(1)
Insert after known node O(1) O(1)
Space O(n) O(n)

Singly Linked List#

Each node holds data and a next pointer. The head pointer is the only entry point — losing it means losing the list.

Singly linked list with head pointer and next links

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None

    def append(self, data):
        """Add a new node to the end of the linked list."""
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            return
        current = self.head
        while current.next:
            current = current.next
        current.next = new_node

    def prepend(self, data):
        """Add a new node to the beginning of the linked list."""
        new_node = Node(data)
        new_node.next = self.head
        self.head = new_node

    def insert_at_position(self, data, position):
        """Insert a new node at the specified position."""
        new_node = Node(data)
        if position == 0:
            new_node.next = self.head
            self.head = new_node
            return
        current = self.head
        index = 0
        while current and index < position - 1:
            current = current.next
            index += 1
        if not current:
            raise IndexError("Position out of bounds")
        new_node.next = current.next
        current.next = new_node

    def delete(self, data):
        """Delete a node with the specified value."""
        if not self.head:
            return
        if self.head.data == data:
            self.head = self.head.next
            return
        current = self.head
        while current.next and current.next.data != data:
            current = current.next
        if current.next:
            current.next = current.next.next

    def search(self, data):
        """Search for a node with the specified value and return its index."""
        current = self.head
        index = 0
        while current:
            if current.data == data:
                return index
            current = current.next
            index += 1
        return -1

    def reverse(self):
        """Reverse the linked list."""
        prev = None
        current = self.head
        while current:
            next_node = current.next  # Store the next node
            current.next = prev      # Reverse the pointer
            prev = current           # Move prev to the current node
            current = next_node      # Move to the next node
        self.head = prev             # Update the head to the new first node

    def display(self):
        """Display the linked list as a sequence of nodes."""
        nodes = []
        current = self.head
        while current:
            nodes.append(str(current.data))
            current = current.next
        print(" -> ".join(nodes))

    def size(self):
        """Return the number of nodes in the linked list."""
        current = self.head
        count = 0
        while current:
            count += 1
            current = current.next
        return count

    def is_empty(self):
        """Check if the linked list is empty."""
        return self.head is None

    def get_first(self):
        """Return the first node (head) of the list."""
        if self.head:
            return self.head.data
        return None

    def get_last(self):
        """Return the last node (tail) of the list."""
        if not self.head:
            return None
        current = self.head
        while current.next:
            current = current.next
        return current.data

    def remove_duplicates(self):
        """Remove duplicate values from the linked list."""
        current = self.head
        seen = set()
        prev = None
        while current:
            if current.data in seen:
                prev.next = current.next  # Bypass the duplicate
            else:
                seen.add(current.data)
                prev = current
            current = current.next

    def clear(self):
        """Clear the entire linked list."""
        self.head = None

    def get_node_at_index(self, index):
        """Return the node at a given index."""
        current = self.head
        current_index = 0
        while current:
            if current_index == index:
                return current.data
            current = current.next
            current_index += 1
        return None  # Index out of range

    def delete_at_index(self, index):
        """Delete a node at the given index."""
        if index == 0:
            if self.head:
                self.head = self.head.next
            return
        current = self.head
        current_index = 0
        while current and current.next:
            if current_index == index - 1:
                current.next = current.next.next
                return
            current = current.next
            current_index += 1
        raise IndexError("Index out of bounds")

    def to_list(self):
        """Convert the linked list to a Python list."""
        result = []
        current = self.head
        while current:
            result.append(current.data)
            current = current.next
        return result

    def sort(self):
        """Sort the linked list."""
        if not self.head or not self.head.next:
            return
        # Implementing merge sort or any sorting algorithm
        self.head = self._merge_sort(self.head)

    def _merge_sort(self, head):
        """Helper function for merge sort."""
        if not head or not head.next:
            return head
        middle = self._get_middle(head)
        next_to_middle = middle.next
        middle.next = None
        left = self._merge_sort(head)
        right = self._merge_sort(next_to_middle)
        sorted_list = self._merge(left, right)
        return sorted_list

    def _get_middle(self, head):
        """Find the middle node of the list."""
        if not head:
            return head
        slow = head
        fast = head
        while fast.next and fast.next.next:
            slow = slow.next
            fast = fast.next.next
        return slow

    def _merge(self, left, right):
        """Merge two sorted linked lists."""
        if not left:
            return right
        if not right:
            return left
        if left.data <= right.data:
            left.next = self._merge(left.next, right)
            return left
        else:
            right.next = self._merge(left, right.next)
            return right

# Example usage
ll = LinkedList()
ll.append(10)
ll.append(20)
ll.append(30)
ll.display()  # Output: 10 -> 20 -> 30

ll.prepend(5)
ll.display()  # Output: 5 -> 10 -> 20 -> 30

ll.insert_at_position(15, 2)
ll.display()  # Output: 5 -> 10 -> 15 -> 20 -> 30

ll.insert_at_position(35, 5)
ll.display()  # Output: 5 -> 10 -> 15 -> 20 -> 30 -> 35

ll.insert_at_position(0, 0)
ll.display()  # Output: 0 -> 5 -> 10 -> 15 -> 20 -> 30 -> 35

# Reverse the linked list
ll.reverse()
ll.display()  # Output: 35 -> 30 -> 20 -> 15 -> 10 -> 5 -> 0

# Size of the list
print(f"Size: {ll.size()}")  # Output: Size: 7

# Search for an element
print(f"Search 15: {ll.search(15)}")  # Output: Search 15: 3

# Remove duplicates (if any)
ll.remove_duplicates()

# Clear the linked list
ll.clear()
ll.display()  # Output: (empty list)

# Add some nodes again
ll.append(100)
ll.append(200)
ll.append(300)
ll.display()  # Output: 100 -> 200 -> 300

# Convert to Python list
print(f"List as Python list: {ll.to_list()}")  # Output: List as Python list: [100, 200, 300]

# Delete at index 1
ll.delete_at_index(1)
ll.display()  # Output: 100 -> 300

# Sort the list
ll.sort()
ll.display()  # Output: 100 -> 300 (already sorted in this case)

Doubly Linked List#

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None
        self.prev = None

class DoublyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, data):
        """Add a new node to the end of the doubly linked list."""
        new_node = Node(data)
        if not self.head:
            self.head = self.tail = new_node
            return
        self.tail.next = new_node
        new_node.prev = self.tail
        self.tail = new_node

    def prepend(self, data):
        """Add a new node to the beginning of the doubly linked list."""
        new_node = Node(data)
        if not self.head:
            self.head = self.tail = new_node
            return
        new_node.next = self.head
        self.head.prev = new_node
        self.head = new_node

    def insert_at_position(self, data, position):
        """Insert a new node at the specified position."""
        new_node = Node(data)
        if position == 0:
            self.prepend(data)
            return
        current = self.head
        index = 0
        while current and index < position - 1:
            current = current.next
            index += 1
        if not current:
            raise IndexError("Position out of bounds")
        new_node.next = current.next
        if current.next:
            current.next.prev = new_node
        current.next = new_node
        new_node.prev = current

    def delete(self, data):
        """Delete a node with the specified value."""
        current = self.head
        while current:
            if current.data == data:
                if current.prev:
                    current.prev.next = current.next
                else:
                    self.head = current.next  # If deleting the head
                if current.next:
                    current.next.prev = current.prev
                else:
                    self.tail = current.prev  # If deleting the tail
                return
            current = current.next

    def search(self, data):
        """Search for a node with the specified value and return its index."""
        current = self.head
        index = 0
        while current:
            if current.data == data:
                return index
            current = current.next
            index += 1
        return -1

    def reverse(self):
        """Reverse the doubly linked list."""
        current = self.head
        while current:
            current.prev, current.next = current.next, current.prev
            current = current.prev
        self.head, self.tail = self.tail, self.head

    def display(self):
        """Display the doubly linked list as a sequence of nodes."""
        nodes = []
        current = self.head
        while current:
            nodes.append(str(current.data))
            current = current.next
        print(" <-> ".join(nodes))

    def size(self):
        """Return the number of nodes in the doubly linked list."""
        current = self.head
        count = 0
        while current:
            count += 1
            current = current.next
        return count

    def is_empty(self):
        """Check if the doubly linked list is empty."""
        return self.head is None

    def get_first(self):
        """Return the first node (head) of the list."""
        if self.head:
            return self.head.data
        return None

    def get_last(self):
        """Return the last node (tail) of the list."""
        if self.tail:
            return self.tail.data
        return None

    def remove_duplicates(self):
        """Remove duplicate values from the doubly linked list."""
        current = self.head
        seen = set()
        while current:
            if current.data in seen:
                self.delete(current.data)
            else:
                seen.add(current.data)
            current = current.next

    def clear(self):
        """Clear the entire doubly linked list."""
        self.head = self.tail = None

    def get_node_at_index(self, index):
        """Return the node at a given index."""
        current = self.head
        current_index = 0
        while current:
            if current_index == index:
                return current.data
            current = current.next
            current_index += 1
        return None  # Index out of range

    def delete_at_index(self, index):
        """Delete a node at the given index."""
        if index == 0:
            if self.head:
                self.head = self.head.next
                if self.head:
                    self.head.prev = None
            return
        current = self.head
        current_index = 0
        while current and current.next:
            if current_index == index - 1:
                if current.next:
                    current.next = current.next.next
                    if current.next:
                        current.next.prev = current
                return
            current = current.next
            current_index += 1
        raise IndexError("Index out of bounds")

    def to_list(self):
        """Convert the doubly linked list to a Python list."""
        result = []
        current = self.head
        while current:
            result.append(current.data)
            current = current.next
        return result

    def sort(self):
        """Sort the doubly linked list."""
        if not self.head or not self.head.next:
            return
        # Implementing merge sort or any sorting algorithm
        self.head = self._merge_sort(self.head)

    def _merge_sort(self, head):
        """Helper function for merge sort."""
        if not head or not head.next:
            return head
        middle = self._get_middle(head)
        next_to_middle = middle.next
        middle.next = None
        left = self._merge_sort(head)
        right = self._merge_sort(next_to_middle)
        sorted_list = self._merge(left, right)
        return sorted_list

    def _get_middle(self, head):
        """Find the middle node of the list."""
        if not head:
            return head
        slow = head
        fast = head
        while fast.next and fast.next.next:
            slow = slow.next
            fast = fast.next.next
        return slow

    def _merge(self, left, right):
        """Merge two sorted doubly linked lists."""
        if not left:
            return right
        if not right:
            return left
        if left.data <= right.data:
            left.next = self._merge(left.next, right)
            if left.next:
                left.next.prev = left
            return left
        else:
            right.next = self._merge(left, right.next)
            if right.next:
                right.next.prev = right
            return right

# Example usage
dll = DoublyLinkedList()
dll.append(10)
dll.append(20)
dll.append(30)
dll.display()  # Output: 10 <-> 20 <-> 30

dll.prepend(5)
dll.display()  # Output: 5 <-> 10 <-> 20 <-> 30

dll.insert_at_position(15, 2)
dll.display()  # Output: 5 <-> 10 <-> 15 <-> 20 <-> 30

dll.insert_at_position(35, 5)
dll.display()  # Output: 5 <-> 10 <-> 15 <-> 20 <-> 30 <-> 35

dll.insert_at_position(0, 0)
dll.display()  # Output: 0 <-> 5 <-> 10 <-> 15 <-> 20 <-> 30 <-> 35

# Reverse the doubly linked list
dll.reverse()
dll.display()  # Output: 35 <-> 30 <-> 20 <-> 15 <-> 10 <-> 5 <-> 0

# Size of the list
print(f"Size: {dll.size()}")  # Output: Size: 7

# Search for an element
print(f"Search 15: {dll.search(15)}")  # Output: Search 15: 3

# Remove duplicates (if any)
dll.remove_duplicates()

# Clear the doubly linked list
dll.clear()
dll.display()  # Output: (empty list)

# Add some nodes again
dll.append(100)
dll.append(200)
dll.append(300)
dll.display()  # Output: 100 <-> 200 <-> 300

# Convert to Python list
print(f"List as Python list: {dll.to_list()}")  # Output: List as Python list: [100, 200, 300]

# Delete at index 1
dll.delete_at_index(1)
dll.display()  # Output: 100 <-> 300

# Sort the list
dll.sort()
dll.display()  # Output: 100 <-> 300 (already sorted in this case)

Circular Linked List#

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class CircularLinkedList:
    def __init__(self):
        self.head = None

    def append(self, data):
        """Add a new node to the end of the circular linked list."""
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            new_node.next = self.head  # Points to itself, forming a circle
            return
        current = self.head
        while current.next != self.head:  # Traverse until we find the last node
            current = current.next
        current.next = new_node
        new_node.next = self.head  # Complete the circle

    def prepend(self, data):
        """Add a new node to the beginning of the circular linked list."""
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            new_node.next = self.head  # Points to itself
            return
        new_node.next = self.head
        current = self.head
        while current.next != self.head:  # Traverse until the last node
            current = current.next
        current.next = new_node  # Last node points to the new node
        self.head = new_node  # Update head

    def insert_at_position(self, data, position):
        """Insert a new node at the specified position in the circular linked list."""
        new_node = Node(data)
        if position == 0:
            self.prepend(data)
            return
        current = self.head
        index = 0
        while current and index < position - 1:
            current = current.next
            index += 1
            if current == self.head:  # Loop back to the start
                raise IndexError("Position out of bounds")
        if not current:
            raise IndexError("Position out of bounds")
        new_node.next = current.next
        current.next = new_node

    def delete(self, data):
        """Delete the node with the specified value."""
        if not self.head:
            return
        current = self.head
        prev = None
        while True:
            if current.data == data:
                if prev:  # If not the first node
                    prev.next = current.next
                    if current == self.head:  # If deleting the head node
                        self.head = current.next
                else:
                    # Deleting the only node (head)
                    if current.next == self.head:
                        self.head = None
                    else:
                        self.head = current.next
                        prev = self.head
                        while prev.next != current:  # Update last node's next
                            prev = prev.next
                        prev.next = self.head
                return
            prev = current
            current = current.next
            if current == self.head:  # Loop back to the start
                break

    def search(self, data):
        """Search for a node with the specified value and return its index."""
        if not self.head:
            return -1
        current = self.head
        index = 0
        while True:
            if current.data == data:
                return index
            current = current.next
            index += 1
            if current == self.head:  # Loop back to the start
                break
        return -1

    def reverse(self):
        """Reverse the circular linked list."""
        if not self.head or self.head.next == self.head:
            return  # No need to reverse if list is empty or has one node
        prev = None
        current = self.head
        first_node = self.head
        while True:
            next_node = current.next
            current.next = prev
            prev = current
            current = next_node
            if current == first_node:  # Loop back to the start
                break
        self.head.next = prev  # Complete the circular reference
        self.head = prev  # New head is the last node in original list

    def display(self):
        """Display the circular linked list."""
        if not self.head:
            print("List is empty")
            return
        nodes = []
        current = self.head
        while True:
            nodes.append(str(current.data))
            current = current.next
            if current == self.head:  # Loop back to the start
                break
        print(" -> ".join(nodes))

    def size(self):
        """Return the number of nodes in the circular linked list."""
        if not self.head:
            return 0
        count = 1
        current = self.head.next
        while current != self.head:
            count += 1
            current = current.next
        return count

    def is_empty(self):
        """Check if the circular linked list is empty."""
        return self.head is None

    def get_first(self):
        """Return the first node (head) of the list."""
        if self.head:
            return self.head.data
        return None

    def get_last(self):
        """Return the last node (tail) of the list."""
        if not self.head:
            return None
        current = self.head
        while current.next != self.head:
            current = current.next
        return current.data

    def remove_duplicates(self):
        """Remove duplicate values from the circular linked list."""
        if not self.head:
            return
        seen = set()
        current = self.head
        prev = None
        while True:
            if current.data in seen:
                prev.next = current.next
                if current == self.head:  # Update head if we deleted the first node
                    self.head = current.next
            else:
                seen.add(current.data)
                prev = current
            current = current.next
            if current == self.head:  # Loop back to the start
                break

    def clear(self):
        """Clear the entire circular linked list."""
        self.head = None

    def get_node_at_index(self, index):
        """Return the node at a given index."""
        if not self.head:
            return None
        current = self.head
        current_index = 0
        while True:
            if current_index == index:
                return current.data
            current = current.next
            current_index += 1
            if current == self.head:  # Loop back to the start
                break
        return None  # Index out of range

    def delete_at_index(self, index):
        """Delete a node at the given index."""
        if not self.head:
            raise IndexError("Index out of bounds")
        if index == 0:
            self.delete(self.head.data)
            return
        current = self.head
        prev = None
        current_index = 0
        while True:
            if current_index == index:
                if prev:
                    prev.next = current.next
                return
            prev = current
            current = current.next
            current_index += 1
            if current == self.head:  # Loop back to the start
                break
        raise IndexError("Index out of bounds")

    def to_list(self):
        """Convert the circular linked list to a Python list."""
        result = []
        if not self.head:
            return result
        current = self.head
        while True:
            result.append(current.data)
            current = current.next
            if current == self.head:  # Loop back to the start
                break
        return result

    def sort(self):
        """Sort the circular linked list."""
        if not self.head or self.head.next == self.head:
            return  # No need to sort if list is empty or has one node
        nodes = self.to_list()
        nodes.sort()
        self.clear()
        for node in nodes:
            self.append(node)

# Example usage
cll = CircularLinkedList()
cll.append(10)
cll.append(20)
cll.append(30)
cll.display()  # Output: 10 -> 20 -> 30

cll.prepend(5)
cll.display()  # Output: 5 -> 10 -> 20 -> 30

cll.insert_at_position(15, 2)
cll.display()  # Output: 5 -> 10 -> 15 -> 20 -> 30

cll.insert_at_position(35, 5)
cll.display()  # Output: 5 -> 10 -> 15 -> 20 -> 30 -> 35

cll.insert_at_position(0, 0)
cll.display()  # Output: 0 -> 5 -> 10 -> 15 -> 20 -> 30 -> 35

# Reverse the circular linked list
cll.reverse()
cll.display()  # Output: 35 -> 30 -> 20 -> 15 -> 10 -> 5 -> 0

# Size of the list
print(f"Size: {cll.size()}")  # Output: Size: 7

# Search for an element
print(f"Search 15: {cll.search(15)}")  # Output: Search 15: 3

# Remove duplicates (if any)
cll.remove_duplicates()

# Clear the circular linked list
cll.clear()
cll.display()  # Output: (empty list)

# Add some nodes again
cll.append(100)
cll.append(200)
cll.append(300)
cll.display()  # Output: 100 -> 200 -> 300

# Convert to Python list
print(f"List as Python list: {cll.to_list()}")  # Output: List as Python list: [100, 200, 300]

# Delete at index 1
cll.delete_at_index(1)
cll.display()  # Output: 100 -> 300

# Sort the list
cll.sort()
cll.display()  # Output: 100 -> 300 (already sorted in this case)