Skip to content

Linked Lists

Linked Lists are dynamic data structures that store elements in nodes connected by pointers. Unlike arrays, linked lists don't require contiguous memory allocation.

Overview

py-ds-academy provides two implementations:

  • LinkedList - Each node points only to the next node
  • DoublyLinkedList - Each node points to both next and previous nodes

Linked List

A linear data structure where each node contains data and a reference to the next node.

Operations

from py_ds import LinkedList

# Create a linked list
sll = LinkedList([1, 2, 3])

# Append at the end
sll.append(4)  # O(1) - uses tail pointer

# Prepend at the beginning
sll.prepend(0)  # O(1)

# Insert at index
sll.insert(2, 99)  # O(n)

# Remove by value
sll.remove(2)  # O(n)

# Pop from end
item = sll.pop()  # O(n)

# Find an element
node = sll.find(3)  # O(n)

# Access by index
value = sll[0]  # O(n)
sll[0] = 10  # O(n)

# Get head and tail
head = sll.head()
tail = sll.tail()

# Print the linked list (visual representation)
print(sll)  # HEAD → 10 → 1 → 99 → 3 → TAIL

# Clear all nodes
sll.clear()

Time Complexity

Operation Time Complexity
append(item) O(1)
prepend(item) O(1)
insert(index, item) O(n)
remove(item) O(n)
pop() O(n)
find(item) O(n)
__getitem__(index) O(n)
__setitem__(index, item) O(n)
head() O(1)
tail() O(1)

Doubly Linked List

A linked list where each node contains references to both next and previous nodes, enabling efficient bidirectional traversal.

Operations

from py_ds import DoublyLinkedList

# Create a doubly linked list
dll = DoublyLinkedList([1, 2, 3])

# Append at the end (O(1) with tail pointer!)
dll.append(4)  # O(1)

# Prepend at the beginning
dll.prepend(0)  # O(1)

# Insert at index
dll.insert(2, 99)  # O(n) - optimized with bidirectional search

# Remove by value
dll.remove(2)  # O(n)

# Pop from end
item = dll.pop()  # O(1)

# Find an element
node = dll.find(3)  # O(n)

# Access by index (optimized)
value = dll[0]  # O(n) - but uses bidirectional search
dll[0] = 10  # O(n)

# Forward iteration
for item in dll:
    print(item)

# Reverse iteration
for item in dll.reverse_iter():
    print(item)

# Get head and tail
head = dll.head()
tail = dll.tail()  # O(1)

# Print the doubly linked list (visual representation)
print(dll)  # HEAD ⇆ 10 → 1 ⇆ 99 ⇆ 3 ⇆ TAIL

Time Complexity

Operation Time Complexity
append(item) O(1)
prepend(item) O(1)
insert(index, item) O(n) ⚡
remove(item) O(n)
pop(index) O(n) ⚡
find(item) O(n)
__getitem__(index) O(n) ⚡
__setitem__(index, item) O(n) ⚡
head() O(1)
tail() O(1)
reverse_iter() O(n)

⚡ = Advantage over singly linked list

Space Complexity

O(n) where n is the number of elements. DoublyLinkedList uses slightly more memory due to the extra pointer per node.

Use Cases

  • Dynamic Memory Allocation - When size is unknown at compile time
  • Implementing Other Structures - Stacks, queues, deques
  • Insertion/Deletion Heavy - When frequent insertions/deletions are needed
  • Undo/Redo - DoublyLinkedList enables efficient backward traversal
  • Browser History - Forward/backward navigation

Example: Implementing a Stack with Linked List

class LinkedListStack:
    def __init__(self):
        self._list = LinkedList()

    def push(self, item):
        self._list.prepend(item)

    def pop(self):
        if self._list.head() is None:
            raise IndexError("Stack is empty")
        return self._list.pop(0)

    def peek(self):
        head = self._list.head()
        if head is None:
            raise IndexError("Stack is empty")
        return head.value

Example: Browser History

class BrowserHistory:
    def __init__(self):
        self.history = DoublyLinkedList()
        self.current = None

    def visit(self, url):
        if self.current:
            # Remove all forward history
            while self.history.tail() != self.current:
                self.history.pop()
        self.history.append(url)
        self.current = self.history.tail()

    def back(self):
        if self.current and self.current.prev:
            self.current = self.current.prev
            return self.current.value
        return None

    def forward(self):
        if self.current and self.current.next:
            self.current = self.current.next
            return self.current.value
        return None

When to Use Which?

Use LinkedList when:

  • You only need forward traversal
  • Memory is a concern
  • You're implementing a simple stack or queue

Use DoublyLinkedList when:

  • You need bidirectional traversal
  • You're implementing undo/redo functionality
  • You need efficient reverse iteration