Module P-2·14 min read

Node-based structures, singly vs doubly linked lists, and the reversal/cycle-detection operations every list problem builds on.

Written by Jatin Jain Saraf · Architect of SupraScan

Module P-2 — Linked Lists: Singly, Doubly, and Core Operations

What this module covers: Every structure so far in this course has been array-backed — one contiguous block of memory (Module F-2). A linked list breaks that assumption: each element is its own independently allocated node, connected by pointers rather than physical adjacency. This module covers singly and doubly linked lists, why insertion/deletion trade places with arrays on cost, reversal, and the cycle-detection technique previewed in Module F-8, now applied directly.


A Node-Based Structure

Where an array is one block with elements at predictable offsets, a linked list is a chain of individually allocated nodes, each holding a value and a pointer to the next node:

class ListNode<T> {
  value: T;
  next: ListNode<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}
 
// 1 -> 2 -> 3 -> null
const third = new ListNode(3);
const second = new ListNode(2);
second.next = third;
const head = new ListNode(1);
head.next = second;

There is no address = base + i × size arithmetic here (Module F-2) — to reach the third node, you have to follow head.next.next, visiting each node in between. That's the fundamental trade: array indexing is O(1); linked list indexing (or "get the k-th element") is O(n).

Analogy: An array is the apartment building from Module F-2 — walk straight to unit 47. A linked list is a treasure hunt where each clue only reveals the location of the next clue — you cannot skip to clue 47 without having followed clues 1 through 46 first.


Where Linked Lists Win

The trade runs the other way for insertion and deletion at a known position. Inserting into the middle of an array means shifting every subsequent element (Module F-2); inserting into a linked list is just pointer reassignment — no other node moves at all:

function insertAfter<T>(node: ListNode<T>, value: T): void {
  const newNode = new ListNode(value);
  newNode.next = node.next;
  node.next = newNode;
  // O(1) — no other node in the entire list was touched or moved
}

This is genuinely O(1), regardless of how long the list is — a real, meaningful advantage over an array's O(n) middle-insertion cost, provided you already have a reference to the node you're inserting after. Finding that node in the first place is still O(n) if you don't already have a pointer to it, which is the honest caveat behind "linked lists have O(1) insertion" — it's O(1) insertion at a known point, not O(1) insertion at an arbitrary position you still have to search for.


Singly vs Doubly Linked Lists

A singly linked list (above) only points forward — from a given node, you can reach the next one, but not the previous one. A doubly linked list adds a prev pointer too:

class DoublyListNode<T> {
  value: T;
  next: DoublyListNode<T> | null = null;
  prev: DoublyListNode<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
}

The extra pointer costs a small amount of memory per node, but it buys something a singly linked list can't do cheaply: deleting a node in O(1), given only a reference to that node — no need to separately track or search for its predecessor, because the node already knows who it is:

function deleteNode<T>(node: DoublyListNode<T>): void {
  if (node.prev) node.prev.next = node.next;
  if (node.next) node.next.prev = node.prev;
  // O(1) — both neighbors are reachable directly from `node`, no search needed
}

In a singly linked list, deleting a given node requires first finding its predecessor by traversing from the head — O(n) — specifically because there's no prev pointer to jump backward through. This exact capability — O(1) deletion of an arbitrary known node — is the reason Module P-3's LRU Cache is built on a doubly linked list, not a singly linked one.


Reversing a Linked List

Reversal is the linked-list equivalent of Module F-2's array reversal, but the mechanism is entirely different — instead of swapping values in place, you re-point every next pointer to face the other direction:

function reverseList<T>(head: ListNode<T> | null): ListNode<T> | null {
  let prev: ListNode<T> | null = null;
  let curr = head;
 
  while (curr !== null) {
    const nextTemp = curr.next; // save the next node before we overwrite curr.next
    curr.next = prev;           // reverse this node's pointer
    prev = curr;                // advance prev
    curr = nextTemp;            // advance curr to the node we saved
  }
 
  return prev; // prev is now the new head
}

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...