Back to Notes

Linked List

Overview

Linear data structure where elements are linked via references — not stored contiguously in memory.

FeatureArrayLinked List
Access by indexO(1)O(n)
Insert/Delete at headO(n)O(1)
Insert/Delete at tailO(1) amortizedO(1) with tail ref
Memory overheadLowHigh (next pointer)
Random accessYesNo

Use over array when: O(1) insertions/deletions at both ends matter (e.g., implementing Queue, Deque). Avoid when: random access is needed — arrays are almost always faster in practice.


Singly Linked List

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

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

    def append(self, val):          # O(n) without tail ref
        node = Node(val)
        if not self.head:
            self.head = node
            return
        curr = self.head
        while curr.next:
            curr = curr.next
        curr.next = node

    def prepend(self, val):         # O(1)
        node = Node(val)
        node.next = self.head
        self.head = node

    def delete(self, val):          # O(n)
        if not self.head:
            return
        if self.head.val == val:
            self.head = self.head.next
            return
        curr = self.head
        while curr.next:
            if curr.next.val == val:
                curr.next = curr.next.next
                return
            curr = curr.next

    def to_list(self):
        result, curr = [], self.head
        while curr:
            result.append(curr.val)
            curr = curr.next
        return result

Doubly Linked List

class DNode:
    def __init__(self, val):
        self.val = val
        self.prev = None
        self.next = None

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

    def append(self, val):          # O(1) with tail ref
        node = DNode(val)
        if not self.tail:
            self.head = self.tail = node
            return
        node.prev = self.tail
        self.tail.next = node
        self.tail = node

    def pop(self):                  # O(1) remove from tail
        if not self.tail:
            return None
        val = self.tail.val
        self.tail = self.tail.prev
        if self.tail:
            self.tail.next = None
        else:
            self.head = None
        return val

Key Techniques

# Reverse a linked list — O(n) time, O(1) space
def reverse(head):
    prev, curr = None, head
    while curr:
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt
    return prev

# Find middle (slow/fast pointers)
def find_middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow

# Detect cycle (Floyd's)
def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False

Interview Patterns → see [[Linked Lists]]