SwadhyAI Notes

All DSA notes

DSA Linked Structures

Linked Lists

Why does inserting after node B lose the rest of the playlist if you write B.next = new before saving the old suffix? The chain is reachability through links, not a block of slots.

A linked list stores each value in a reachable from . Indexing walks links; splicing a known edge can be constant-time if you practice .

Algorithm journey

1

Reachability

The list is the finite path from head by following next.

2

Save the suffix before publishing a new edge.

3

Repair /

Empty, front, and last-node transitions update metadata together.

4

Optional sentinel so front edits match interior edits.

What actually happens

Naive idea: treat a list like an array. “Insert at index i is O(1)” because you only change a pointer. That hides the Θ(i) walk to find the predecessor, and it ignores the crash when you overwrite next without saving it.

Better idea: separate find from splice. Once the predecessor handle is known, insert is a fixed number of pointer writes under . A removes the special case when the edit touches the real front.

The improved design still fails with a stale , accidental cycles, foreign node handles, or choosing a list when you mostly need random access and dense scans (a dynamic array often wins there).

Invariant (pointer safety / reachability)

After every public mutation, following next from head reaches each live data node exactly once (acyclic linear case), ends at the closed tail (or sentinel boundary), and never drops a still-needed suffix. Saves happen before overwrites; head, tail, and size agree with that reachable chain.

How to choose a linked list

1. Do you already hold node handles? Splicing around known nodes is the list's strength. Searching by index first often erases the asymptotic win.

2. Do you need stable node identity? Hash maps keyed by node, intrusive structures, and some free lists care that nodes stay put while links change.

3. Is the workload scan- and index-heavy? Prefer a dynamic array for compact storage and Θ(1) random access unless the list composition (queue, LRU, arena) is deliberate.

Quantitative check

Reaching index i always costs Θ(i) link follows. Inserting at i without a handle is Θ(i) find + Θ(1) splice. On n = 10⁵, walking to the middle each time for 10³ inserts is about 5×10⁷ link hops before any payload work. An array paying Θ(n) shifts can still win on locality for the same edits.

What the representation promises

The links define logical order; allocation addresses define physical placement. Those are independent. A node at a numerically smaller address can appear later in the list, and an allocator may sometimes place nearby nodes together. “Linked” means the algorithm follows references, not that every node must be physically far apart.

A position is a walk

To obtain node i, begin at head and take i next-edges. Accessing the middle is Θ(n) in the worst case even if the list tracks its size and tail.

An edit is an edge change

Given the correct predecessor, inserting into a singly linked list needs a fixed number of pointer writes. The lookup and the splice are separate costs; hiding the lookup causes the common “all insertions are O(1)” myth.

Interactive: walk a chain, then splice one

The addresses are deliberately out of logical order. Step through traversal, then insert 29 after node B and notice why the new node must remember the old suffix before B's link changes.

References, reachability, and cost

Follow a scattered chain, then splice a node after a predecessor you already hold.

Step 1 / 4

Start from head

Logical chain (reachable from head)head = @A8

The head reference gives the first node. No index calculation can jump directly to value 42.

Start from head. 0 of 3 nodes processed; current node A, value 18.

Pointer fields now

A.next = BB.next = CC.next = null

Heap allocations (address order is unrelated to list order)

0x3CB: 7
unallocated—
0xA8A: 18
0xF0C: 42

A minimal, maintainable representation

These implementations store head, tail, and size. Tail and size are conveniences, not properties every linked list must maintain; each adds invariants that every mutation must preserve.

Python

python
class Node:
    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node


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

    def prepend(self, value):
        node = Node(value, self.head)
        self.head = node
        if self.tail is None:       # empty -> one node
            self.tail = node
        self.size += 1

    def append(self, value):
        node = Node(value)
        if self.tail is None:
            self.head = self.tail = node
        else:
            self.tail.next = node
            self.tail = node
        self.size += 1

    def insert_after(self, predecessor, value):
        # Precondition: predecessor is a node in this list.
        if predecessor is None:
            raise ValueError("a predecessor handle is required")
        node = Node(value, predecessor.next)  # preserve suffix first
        predecessor.next = node
        if self.tail is predecessor:
            self.tail = node
        self.size += 1
        return node

Java

java
final class SinglyLinkedList<T> {
    static final class Node<T> {
        final T value;
        Node<T> next;

        Node(T value, Node<T> next) {
            this.value = value;
            this.next = next;
        }
    }

    private Node<T> head;
    private Node<T> tail;
    private int size;

    void prepend(T value) {
        Node<T> node = new Node<>(value, head);
        head = node;
        if (tail == null) tail = node; // empty -> one node
        size++;
    }

    void append(T value) {
        Node<T> node = new Node<>(value, null);
        if (tail == null) {
            head = tail = node;
        } else {
            tail.next = node;
            tail = node;
        }
        size++;
    }

    Node<T> insertAfter(Node<T> predecessor, T value) {
        // Precondition: predecessor is a node in this list.
        if (predecessor == null) {
            throw new IllegalArgumentException("a predecessor handle is required");
        }
        Node<T> node = new Node<>(value, predecessor.next); // preserve suffix
        predecessor.next = node;
        if (tail == predecessor) tail = node;
        size++;
        return node;
    }
}

Invariants: the mutation checklist

An invariant is true before and after every public operation. For an acyclic singly linked list with head, tail, and size, these statements form a useful executable specification.

1

Empty agreement

head is null if and only if tail is null; then size is 0.

2

Closed tail

When non-empty, tail is reachable from head and tail.next is null.

3

One finite chain

Following next from head reaches each member once: no accidental cycle.

4

Count agreement

Exactly size nodes are reachable, and the last reachable node is tail.

Representation matters: a circular list intentionally violates tail.next is null; a sentinel-based list may never have a null head. State invariants for the design you actually implement.

Traversal is a small correctness proof

Python

python
def find(head, target):
    current = head
    while current is not None:
        if current.value == target:
            return current       # return a handle, not an index
        current = current.next
    return None

Java

java
import java.util.Objects;

final class LinkedSearch {
    static final class Node<T> {
        final T value;
        Node<T> next;

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

    static <T> Node<T> find(Node<T> head, T target) {
        Node<T> current = head;
        while (current != null) {
            if (Objects.equals(current.value, target)) {
                return current; // return a handle, not an index
            }
            current = current.next;
        }
        return null;
    }
}

Initialization

current = head, so no reachable node appears before the unprocessed suffix.

Maintenance

Process current once, then advance to its successor. The processed prefix grows by one node.

Termination

A match returns its node; null means the finite reachable chain was exhausted.

Time is Θ(k) when the algorithm examines k nodes. Worst-case search is Θ(n); extra working space is Θ(1) iteratively. A recursive traversal uses O(n) call-stack space in the worst case.

Operation costs: include the lookup

Singly linked-list operation costs and the assumptions behind them
OperationSingly linked costCondition behind the bound
Read or update index iΘ(i + 1), worst Θ(n)Start at head and follow i links; there is no address formula.
Search by valueO(n), worst Θ(n)May stop early; an unsuccessful search examines every node.
PrependΘ(1)Update new.next and head; initialize tail when the list was empty.
AppendΘ(1) with tailWithout a maintained tail reference, finding the last node is Θ(n).
Insert after a nodeΘ(1) after lookupThe predecessor handle must already be known. Finding it may cost Θ(n).
Delete after a nodeΘ(1) after lookupRelink predecessor.next and repair tail if the removed node was last.
Remove the tailΘ(n)A singly linked list must find the predecessor; a doubly linked list can avoid that walk.
Deletion nuance: a handle to the node being removed is not generally enough in a singly linked list; the predecessor owns the link that must change. Copying the successor's value into the current node is a restricted trick: it cannot remove the tail and can violate identity or aliasing expectations.

Memory reality: more than one pointer

Layout overhead

A pointer is commonly 8 bytes in a 64-bit process, but node size also depends on field order, alignment, runtime object headers, and allocator metadata. A 4-byte payload plus an 8-byte link therefore does not justify one universal overhead percentage.

Pointer chasing

The address of the next load is discovered from the current node. This dependency can limit prefetching and memory-level parallelism; the actual penalty varies with allocation pattern, cache state, hardware, and runtime.

Allocation behavior

Growing one node at a time avoids relocating every existing element and does not require one large contiguous region. It still depends on allocator capacity and can incur fragmentation and allocation costs.

Linked list versus dynamic array

Comparison of dynamic arrays and linked lists
ConcernDynamic arrayLinked list
Random accessΘ(1) by base + index × widthΘ(n) worst case by traversal
Insert at a known interior locationΘ(n) shifts in the usual contiguous representationΘ(1) relinking when the needed handle(s) are known
AppendAmortized Θ(1) for a geometric-growth dynamic arrayΘ(1) with a maintained tail; usually one node allocation
Traversal localityContiguous storage is friendly to caches and prefetchingDependent pointer loads often have weaker locality
Per-element storageMostly payload plus spare capacityPayload, link field(s), alignment, and often allocator/object overhead
Reference stabilityGrowth may invalidate pointers/iterators; shifting changes which element an index denotesOther nodes can stay put, but removing a node invalidates handles to that node

Big-O alone misses constants and locality. For compact values and scan-heavy workloads, a dynamic array often wins even when a list advertises a better asymptotic insertion bound, especially if the list first traverses to find the position.

Ownership, identity, and aliasing

Pointer rewiring changes which nodes are reachable; it does not automatically settle who owns or may mutate them. In garbage-collected languages, references keep objects alive. In manual-memory languages, the program must define who frees a removed node and ensure no remaining reference dereferences it.

Shared suffixes

Two heads can point into the same chain. Mutating a shared node affects both views, and naïvely freeing through both heads can double-free.

External handles

A caller may retain a node reference. Removing or recycling that node can make the handle stale even though the rest of the list remains valid.

Accidental cycles

One wrong assignment can make traversal never reach null. Cycle detection helps diagnose structure; clear mutation contracts help prevent it.

Sentinel nodes remove boundary branches

Dummy head

A permanent node before the first data node gives every data node a predecessor. Insertions and removals at the front can use the same relinking logic as interior edits. The sentinel is structural metadata, not an element.

Cost: one extra node and a convention that APIs must not expose as user data. A dummy tail can help in some designs, but it does not by itself reveal a predecessor in a singly linked list.

Python

python
def remove_first(head, target):
    dummy = Node(None, head)
    previous = dummy

    while previous.next is not None:
        if previous.next.value == target:
            previous.next = previous.next.next
            break
        previous = previous.next

    return dummy.next  # works even when the old head was removed

Java

java
import java.util.Objects;

final class RemoveFirst {
    static final class Node<T> {
        final T value;
        Node<T> next;

        Node(T value, Node<T> next) {
            this.value = value;
            this.next = next;
        }
    }

    static <T> Node<T> removeFirst(Node<T> head, T target) {
        Node<T> dummy = new Node<>(null, head);
        Node<T> previous = dummy;

        while (previous.next != null) {
            if (Objects.equals(previous.next.value, target)) {
                previous.next = previous.next.next;
                break;
            }
            previous = previous.next;
        }

        return dummy.next; // works even when the old head was removed
    }
}

Patterns built from the same links

Fast and slow

Invariant

Fast moves two edges while slow moves one.

Find a midpoint, detect a cycle, or locate a cycle entrance without storing every visited node.

In-place reversal

Invariant

previous is the reversed processed prefix; current begins the untouched suffix.

Reverse a chain in Θ(n) time and Θ(1) extra space, carefully saving next before overwriting it.

Merge by relinking

Invariant

The output tail follows the smallest unmerged value.

Merge two sorted lists in Θ(n + m) time; reuse nodes when ownership permits, or copy when inputs must remain intact.

Python

python
def reverse(head):
    previous = None
    current = head

    while current is not None:
        following = current.next  # save the suffix
        current.next = previous   # reverse one edge
        previous = current
        current = following

    return previous

Java

java
final class ReverseList {
    static final class Node<T> {
        final T value;
        Node<T> next;

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

    static <T> Node<T> reverse(Node<T> head) {
        Node<T> previous = null;
        Node<T> current = head;

        while (current != null) {
            Node<T> following = current.next; // save the suffix
            current.next = previous;          // reverse one edge
            previous = current;
            current = following;
        }

        return previous;
    }
}

Choose the representation deliberately

Singly linked

next

Forward scans, stacks, simple queues, and free lists.

Doubly linked

previous + next

O(1) removal from a known node and bidirectional iteration; higher update and storage cost.

Circular

tail connects to head

Repeated rotation or round-robin traversal when a null terminator is undesirable.

Intrusive

hooks inside payload

Systems code that needs explicit allocation control or one object participating in multiple structures.

Skip list

probabilistic levels

Expected O(log n) search and update in an ordered linked structure, with extra forward pointers.

XOR list

encoded prev XOR next

A historical space trick with serious caveats: unsafe or unsupported in many managed, moving-GC, capability, and sanitizer-heavy environments.

Common practical compositions matter more than novelty: a queue pairs a singly linked list with head and tail; an LRU cache typically pairs a hash map with a doubly linked list; allocator free lists and intrusive lists appear in lower-level systems where object movement and lifetime are tightly controlled.

Failure modes worth rehearsing

Losing the suffix

Save old next before overwriting it: new.next = predecessor.next, then predecessor.next = new.

Stale tail

Repair tail when appending, deleting the last node, clearing, or transitioning between empty and non-empty.

Head special-case bugs

Handle the empty/front transition explicitly or use a sentinel whose contract is consistent.

Infinite traversal

Advance the cursor on every non-returning path, and do not assume null termination for a possibly circular structure.

Aliased mutation

Decide whether operations consume, copy, or share input nodes; document ownership and handle invalidation.

Complexity without context

Say whether a node/predecessor handle is supplied and whether head, tail, previous links, and size are stored.

Simple rule of thumb

Prefer a linked list when the workload is dominated by splicing around already-known node handles, stable node identity matters, and you accept link/allocation overhead. Prefer a dynamic array when you need indexing, compact storage, fast full scans, or simple ownership. If the reason is only “the size changes,” start with a dynamic array. It changes size too.

Exercises

  1. Nodes A→B→C. Insert X after B with the wrong order of assignments. Which nodes become unreachable, and what is the safe two-line splice under ?
  2. Delete the first occurrence of a value that may be the head. Rewrite the loop with a and show why the empty and front cases share one branch.
  3. A list maintains head, tail, and size. Give a sequence of append and delete-last operations that leaves a stale tail if you forget one repair. What breaks on the next append?

Primary sources

  • Cormen et al., Introduction to Algorithms (CLRS): linked-list representations and elementary operations.
  • Knuth, The Art of Computer Programming, Vol. 1: sequential allocation vs linked allocation.
  • Sedgewick & Wayne, Algorithms: singly and doubly linked lists as bag, stack, and queue backends.

Takeaway

A linked list is a reachability path. Constant-time splices exist only after the right handles are known, and only when every overwrite preserves the suffix you still need.