Singly Linked List

A singly-linked list is a chain of nodes, each holding a value and a pointer to the next node. Insert/delete at the head is O(1); access by index is O(n) because you must walk from the head.

Complexity

Average Worst Space
AccessSearchInsertionDeletion AccessSearchInsertionDeletion Worst
Θ(n) Θ(n) Θ(1) Θ(1) O(n) O(n) O(1) O(1) O(n)

How it works

Each node = (value, next_pointer). The list owns a head pointer. To insert at the head: new_node.next = head; head = new_node. To delete the head: head = head.next. Insert/delete at the tail or in the middle requires first walking to that position — O(n).

Python implementation

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


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

    def push_front(self, val):           # O(1)
        self.head = Node(val, self.head)

    def pop_front(self):                 # O(1)
        if self.head is None:
            raise IndexError("pop from empty list")
        val = self.head.val
        self.head = self.head.next
        return val

    def find(self, val):                 # O(n)
        node = self.head
        while node:
            if node.val == val:
                return node
            node = node.next
        return None

    def __iter__(self):
        node = self.head
        while node:
            yield node.val
            node = node.next


ll = SinglyLinkedList()
ll.push_front(3); ll.push_front(2); ll.push_front(1)
print(list(ll))            # [1, 2, 3]
ll.pop_front()
print(list(ll))            # [2, 3]

Trade-offs

← Back to Algorithms

5. Operations Step by Step

Insert After a Node: Pointer Updates head: 3 next 7 next 9 next = None new node 5 new.next = a.next a.next = new link in 3 7 5 inserted 9 Order matters: set new.next before overwriting a.next, or the tail is lost.

Every operation on a singly linked list is a matter of rewiring next pointers. Inserting after a known node touches two pointers and is O(1). Inserting at the head is the same, with the head reference playing the role of the previous node. Deleting the node after a known node sets prev.next = prev.next.next. What is not O(1) is finding the place to operate: locating index i, a value, or the node before a given node requires walking from the head. That is why the complexity table lists insertion and deletion as O(1) only when the position is already known.

A tail pointer makes appends O(1) and turns the list into an efficient queue, but deleting the tail is still O(n) because there is no back pointer to the second-to-last node. That limitation is the main reason doubly linked lists exist.

6. Reversal and Two-Pointer Techniques

Reversing a List In Place prev = None cur = head nxt = cur.next save cur.next = prev flip prev, cur = cur, nxt advance head = prev done

Reversal is the classic linked-list exercise: walk the list once, flipping each next pointer to point backwards, keeping three references so nothing is lost. Many other problems use two pointers moving at different speeds. A fast pointer moving two steps per iteration and a slow pointer moving one find the middle node when the fast pointer reaches the end, and detect a cycle if the two ever meet (Floyd's algorithm). A pointer started k nodes ahead finds the k-th node from the end in one pass.

A sentinel, or dummy head, node simplifies many of these algorithms. By starting the list with a node that holds no value, insertion and deletion at the front become the same code as anywhere else, because there is always a previous node. Merging two sorted lists, removing all nodes with a given value, and partitioning a list around a pivot are all shorter and less error-prone with a dummy head that is discarded at the end.

7. Worked Example

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

def from_list(xs):
    head = None
    for x in reversed(xs):
        head = Node(x, head)
    return head

def to_list(head):
    out = []
    while head:
        out.append(head.val); head = head.next
    return out

def reverse(head):
    prev = None
    while head:
        head.next, prev, head = prev, head, head.next
    return prev

def middle(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
    return slow

def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:
            return True
    return False

h = from_list([3, 7, 9, 12])
print(to_list(reverse(h)))        # [12, 9, 7, 3]
print(middle(from_list([1, 2, 3, 4, 5])).val)   # 3

__slots__ removes the per-instance dictionary, which noticeably reduces memory for lists with many nodes.

8. Linked List vs Array

OperationSingly linked listDynamic array (Python list)
Index accessO(n)O(1)
Insert / delete at frontO(1)O(n)
Append at endO(1) with tail pointerO(1) amortized
Insert after known nodeO(1)O(n)
Memory per elementValue + pointer + object headerOne pointer
Cache localityPoorGood

In Python, a list or collections.deque is almost always faster in practice. Linked lists matter for understanding, for interview problems, and in languages or systems where nodes are embedded in other structures, such as kernel lists, LRU caches, and hash table chaining.

9. Pitfalls

When testing list code, draw the pointers for a three-node list on paper and check each assignment; most bugs show up as a lost or cyclic link.