The Frontend Engineer's LeetCode Blueprint, Part 2: The Pointer & Memory Playground
React Fiber and the DOM tree are linked lists in disguise. Part 2 covers fast-and-slow pointers, in-place reversal, the monotonic stack and a from-scratch LRU cache built on a doubly linked list plus hash map.
Part 2: The Pointer & Memory Playground (Linked Lists, Monotonic Stacks, and In-Memory System Design)
The Frontend Paradox: "We Don't Use Linked Lists"
Ask any frontend engineer when they last used a linked list in production, and they’ll probably say: "Never. We have Array.prototype.map and immutable state."
And they’d be wrong.
- The DOM Tree is connected via pointer references:
node.nextSibling,node.previousSibling, andnode.parentNode. - React Fiber—the reconciler architecture powering React 16 through 19—is literally an in-memory singly linked list tree of fiber nodes with
child,sibling, andreturnpointers. - The Browser History Stack and Undo/Redo buffers in rich text editors are built on doubly linked lists.
When Toptal, Meta, or Stripe throw linked list and stack problems at a senior frontend candidate, they aren't testing whether you can build a todo app. They are testing pointer discipline, reference safety, and your ability to reason about mutating heap objects without triggering an infamous:
TypeError: Cannot read properties of null (reading 'next')In this article, we’re going to master pointer manipulation, uncover the "cheat code" data structure known as the Monotonic Stack, and build an industrial-strength LRU Cache from scratch without relying on language-level shortcuts.
1. The Anatomy of a JS Linked List & The "Dummy Head" Superweapon
In languages like C++, nodes sit in contiguous blocks or explicit heap allocations with manual memory management. In JavaScript:
class ListNode {
constructor(val = 0, next = null) {
this.val = val;
this.next = next; // Just a reference to another object on the V8 heap
}
}Because variables in JavaScript hold references to objects, copying a node does not duplicate the node—it creates another pointer pointing to the exact same memory address.
The Number One Bug in Interview Code
Consider this common disaster:
// You want to modify the list, but you lose the original head:
function badOperation(head) {
head = head.next; // The original head is now disconnected or lost from your return path!
return head;
}The Universal Cure: The Sentinel (Dummy) Node
Whenever you need to:
- Delete the first node of a list
- Insert before the head
- Merge or reorganize lists where the new
headis uncertain
Always create a dummy node.
const dummy = new ListNode(-1);
dummy.next = head;
let current = dummy;By returning dummy.next at the end of the function, you eliminate 90% of if (head === null) or if (current === head) branch conditions.
2. Fast & Slow Pointers (Floyd’s Cycle Algorithm)
The "Tortoise and Hare" algorithm relies on two pointers moving at different speeds across references.
Slow (1 step): (A) -> (B) -> (C) -> (D)
Fast (2 steps): (A) --------> (C) --------> (E)Key Use Cases:
- Finding the midpoint of a list in one pass: When
fastreaches the end (null),slowis guaranteed to be at the center. - Detecting cycles: If there is a loop,
fastwill eventually lapslowinside the cycle.
Challenge 1: Reverse Nodes in k-Group (LeetCode 25 - Hard)
Let's bypass easy problems and tackle one of the most notoriously failed linked list problems on LeetCode.
Problem: Given the
headof a linked list, reverse the nodes of the listkat a time, and return the modified list.kis a positive integer less than or equal to the length of the linked list. If the number of nodes is not a multiple ofk, then left-out nodes at the end should remain as they are. You must use onlyO(1)extra memory.
Input: [1] -> [2] -> [3] -> [4] -> [5], k = 3
Output: [3] -> [2] -> [1] -> [4] -> [5]Why People Fail This:
They try to swap node values. Swapping values in an interview will get you disqualified instantly—interviewers want to see you rewire the pointers.
The Blueprint:
- Create a
dummynode pointing tohead. - Find the
k-th node from the current group anchor. If fewer thanknodes remain, stop. - Disconnect the
k-group, reverse it in-place using standard 3-pointer reversal (prev,curr,nextTemp). - Re-stitch the reversed segment back to the preceding group and the next remaining segment.
- Move the anchor forward and repeat.
Before reversal (k = 2):
groupPrev -> [1] -> [2] -> [3] -> ...
^ ^
curr kth
After reversing [1] -> [2]:
groupPrev -> [2] -> [1] -> [3] -> ...
^
groupPrev moves here for the next group!The Production Code:
/**
* Time Complexity: O(n) - Each node is traversed and reversed exactly once
* Space Complexity: O(1) - Constant pointer mutations
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
function reverseKGroup(head, k) {
if (!head || k <= 1) return head;
const dummy = new ListNode(0);
dummy.next = head;
let groupPrev = dummy;
while (true) {
// 1. Verify if k nodes exist in the current group
const kth = getKthNode(groupPrev, k);
if (!kth) break;
const groupNext = kth.next;
// 2. Reverse the k nodes between groupPrev and groupNext
let prev = groupNext;
let curr = groupPrev.next;
while (curr !== groupNext) {
const tempNext = curr.next;
curr.next = prev;
prev = curr;
curr = tempNext;
}
// 3. Rewire the connection from the previous group
const nextGroupPrev = groupPrev.next; // [1] is now the tail of this reversed segment
groupPrev.next = kth; // groupPrev now points to [2] (the new head)
groupPrev = nextGroupPrev; // move groupPrev to [1]
}
return dummy.next;
}
/**
* Helper to advance k steps forward
*/
function getKthNode(curr, k) {
while (curr && k > 0) {
curr = curr.next;
k--;
}
return curr;
}3. The Monotonic Stack: The Ultimate Cheat Code
If a problem asks for:
- "The next greater element for every index"
- "The nearest smaller element on the left"
- "The maximum area bounded by adjacent elements"
Stop thinking about nested loops. You are looking at a Monotonic Stack.
What is It?
A stack that preserves a strictly increasing or decreasing order of values. As you iterate through an array:
- If the current element violates the monotonic property, you pop elements off the stack until the order is restored.
- Each popped element has just met its "destiny"—the current element is the first element that is larger (or smaller) than it!
Challenge 2: Largest Rectangle in Histogram (LeetCode 84 - Hard)
Problem: Given an array of integers
heightsrepresenting the histogram's bar height where the width of each bar is1, return the area of the largest rectangle in the histogram.
Heights: [2, 1, 5, 6, 2, 3]
[6]
[5][#]
[#][#] [3]
[2] [#][#][2][#]
[#][1][#][#][#][#]
-------------------
Max rectangle is formed by heights 5 and 6:
Width = 2, Height = 5 -> Area = 10The Brute-Force Trap:
For every bar i, extend outward to the left and right until hitting a bar shorter than heights[i]. Calculate width * heights[i]. This takes O(n^2) time and triggers an instant Time Limit Exceeded.
The Monotonic Stack Intuition:
We want to find:
- The first index to the left that is shorter than
heights[i]. - The first index to the right that is shorter than
heights[i].
If we keep a stack of indices with monotonically increasing heights:
- When we see a bar
currthat is shorter than the bar atstack.peek(), that means the bar atstack.peek()cannot extend any further to the right! - We pop the bar from the stack: this is our
h(height). - The right boundary is
i. - The left boundary is the new
stack.peek()(the nearest shorter bar to its left). - Width
= i - stack.peek() - 1. - Area
= h × width.
The Code (with Sentinel Values):
/**
* Time Complexity: O(n) - Each index is pushed and popped exactly once
* Space Complexity: O(n) - Stack stores indices
* @param {number[]} heights
* @return {number}
*/
function largestRectangleArea(heights) {
if (!heights || heights.length === 0) return 0;
// Append a 0 at the end as a dummy sentinel.
// This guarantees that all remaining elements in the stack will be popped out at the end!
const extendedHeights = [...heights, 0];
const stack = []; // Stores indices
let maxArea = 0;
for (let i = 0; i < extendedHeights.length; i++) {
const currentHeight = extendedHeights[i];
// Maintain a strictly increasing stack
while (stack.length > 0 && extendedHeights[stack[stack.length - 1]] > currentHeight) {
const poppedIndex = stack.pop();
const height = extendedHeights[poppedIndex];
// If stack is empty, it means 'height' was the smallest seen so far; width extends to index 0
const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}Look at how concise that is. A problem that terrorizes 80% of candidates collapses into 15 lines of JavaScript once you wield the monotonic stack.
4. In-Memory System Design: The LRU Cache (LeetCode 146)
In frontend architecture, LRU (Least Recently Used) caching is everywhere:
- Caching computed selectors (Reselect/Memoize).
- Image caching in canvas / WebGL texture renderers.
- Storing paginated API results in Apollo Client or React Query.
Problem: Design a data structure that follows the constraints of a Least Recently Used (LRU) cache.
get(key): Return the value if the key exists, otherwise return-1.O(1)time.put(key, value): Update or insert the value. If keys exceed capacity, evict the least recently used key.O(1)time.
The JavaScript Cheat (And Why Interviewers Reject It)
In JavaScript, standard Map objects remember the original insertion order of keys:
// The "cheater" interview trick:
const map = new Map();
map.set('a', 1);
map.delete('a');
map.set('a', 1); // Moves 'a' to the most recent insertion position!If you write this in a senior interview at Toptal, the interviewer will say:
"Great. You know the ECMAScript specification for
Mapiteration order. Now build it without usingMap's internal linked hash map mechanics."
The True Engineering Solution: Doubly Linked List + Hash Map
To achieve guaranteed O(1) operations:
- Hash Map (
Map): Mapskey→DListNode reference(O(1)lookup). - Doubly Linked List (
DListNode):- Head sentinel: Represents Most Recently Used (MRU).
- Tail sentinel: Represents Least Recently Used (LRU).
- Node removal:
O(1)because each node has.prevand.next. No array shifts!
[HEAD] <-> [Node A (MRU)] <-> [Node B] <-> [Node C (LRU)] <-> [TAIL]
| ^
|_______________| (New or updated nodes inserted directly behind HEAD)The Industrial-Grade Implementation:
class DListNode {
constructor(key = 0, val = 0) {
this.key = key;
this.val = val;
this.prev = null;
this.next = null;
}
}
class LRUCache {
/**
* @param {number} capacity
*/
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map(); // key -> DListNode
// Sentinel dummy nodes to avoid edge-case pointer checks
this.head = new DListNode();
this.tail = new DListNode();
this.head.next = this.tail;
this.tail.prev = this.head;
}
/**
* @param {number} key
* @return {number}
*/
get(key) {
if (!this.cache.has(key)) {
return -1;
}
const node = this.cache.get(key);
// Accessed node becomes most recently used: move to head
this._moveToHead(node);
return node.val;
}
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
put(key, value) {
if (this.cache.has(key)) {
const node = this.cache.get(key);
node.val = value;
this._moveToHead(node);
} else {
const newNode = new DListNode(key, value);
this.cache.set(key, newNode);
this._addNode(newNode);
if (this.cache.size > this.capacity) {
// Evict the least recently used node (before tail)
const lru = this._popTail();
this.cache.delete(lru.key);
}
}
}
// --- Internal Pointer Helpers ---
/**
* Always insert new node right after dummy head
*/
_addNode(node) {
node.prev = this.head;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
}
/**
* Unlink an existing node from the list
*/
_removeNode(node) {
const prevNode = node.prev;
const nextNode = node.next;
prevNode.next = nextNode;
nextNode.prev = prevNode;
}
/**
* Move an existing node to head (mark as most recently used)
*/
_moveToHead(node) {
this._removeNode(node);
this._addNode(node);
}
/**
* Remove and return the node immediately before dummy tail
*/
_popTail() {
const lruNode = this.tail.prev;
this._removeNode(lruNode);
return lruNode;
}
}Notice how clean the code becomes when using this.head and this.tail sentinels. No if (this.size === 1) or if (!this.head) checks anywhere!
5. Memory Management & V8 Traps in Linked Lists
When writing linked structures in long-running Node.js or browser environments:
The Dangling Pointer Memory Leak
In JavaScript, garbage collection (GC) uses Mark-and-Sweep. If you detach a node from your linked list:
node.prev.next = node.next;
node.next.prev = node.prev;If that node is still referenced in a closure, global variable, or a cache map, the entire linked list connected to it will not be collected!
Always sever references when discarding nodes:
node.prev = null;
node.next = null;6. The Part 2 Practice Gauntlet
Before proceeding to Part 3, build muscle memory by solving these:
- LeetCode 141 (Easy) & 142 (Medium): Linked List Cycle I & II (Floyd's algorithm basics).
- LeetCode 143 (Medium): Reorder List (Combines finding midpoint, reversing second half, and merging lists).
- LeetCode 739 (Medium): Daily Temperatures (Your gateway monotonic stack drill).
- LeetCode 84 (Hard): Largest Rectangle in Histogram (Implement without looking at this guide).
- LeetCode 146 (Medium): LRU Cache (Write the Doubly Linked List version from scratch in an empty editor).
What’s Coming Next in Part 3
Now that we have conquered linear data structures, pointers, and custom in-memory systems, we enter the world of non-linear algorithms:
- Trees & Graphs through the Frontend Lens: Turning the Virtual DOM and dependency graphs into algorithmic graphs.
- Mastering DFS and BFS: Level-order traversals, topological sorting (Webpack bundle resolution), and cycle detection.
- Binary Search on Answer Spaces: How to solve Hard problems (like Median of Two Sorted Arrays or Split Array Largest Sum) using binary search without looking at a simple sorted array.