Coding Interview Patterns Module 10 – Linked list patterns
Linked-list toolkit: dummies and test helpers
Build and print linked lists from arrays to test them fast, remove nodes with a dummy head and no special case, and rewire without losing the list.
What you will learn
- Build linked lists from arrays and print them back, so that each test of linked-list code is one line
- Use a dummy (sentinel) node so that removing or adding at the head needs no special case
- Draw the pointers before rewiring, and save next before you overwrite it
- Catch lost references and accidental cycles with a cycle-checking helper and AddressSanitizer
Before you start
What you’ll learn · Before you start
What you will learn
- Build linked lists from arrays and print them back, so that each test of linked-list code is one line
- Use a dummy (sentinel) node so that removing or adding at the head needs no special case
- Draw the pointers before rewiring, and save next before you overwrite it
- Catch lost references and accidental cycles with a cycle-checking helper and AddressSanitizer
Before you start
On this page
Linked-list problems are short to state and easy to break. One assignment in the wrong order loses the rest of the
list, and the first node needs different code from all the others. Three tools remove most of that risk. Test
helpers build a list from an array and turn it back into one, so every test is one line and every printed list
reads like an array. A dummy node, also called a sentinel, sits in front of the head, so the first real node is
handled by the same code as every other node. And one habit: draw the pointers before you rewire them, and save
next before you overwrite it. This lesson builds the helpers, uses all three tools on one problem solved in full in
Python, JavaScript, Java and C++, and reproduces the two bugs that the habit prevents.
A node, and helpers that make lists testable
A singly linked list is a chain of nodes. Each node holds a value and a reference to the next node, and the last node’s reference is empty. A coding problem hands you the first node, the head, and nothing else: no length and no index, so reaching the fifth node means following four references. The structure itself is covered in the DSA track; this module is about the patterns that coding rounds build on it.
Linked-list problems normally define the node for you, with a value field and a next field. Before you solve
anything, write two helpers: from_list turns an array into a linked list and returns its head, and to_list walks a
list and returns its values as an array. A test is then one line, to_list(f(from_list([...]))) == [...], and a
failing test prints something you can read.
py/try_helpers.py
from lists import from_list, show, to_list
route = from_list([12, 7, 31])
print(show(route))
print(route.val, route.next.val, route.next.next.val, route.next.next.next)
print(to_list(route), to_list(from_list([])), show(None))
# A test is one line: build from a list, call the function, compare as a list.
assert to_list(from_list([5, 5, 2])) == [5, 5, 2]
# A rewiring bug can make the last node point back into the list. to_list stops and says so.
loop = from_list([3, 7, 1, 9])
loop.next.next.next.next = loop.next # 9 now points back at 7
try:
to_list(loop)
except ValueError as error:
print("ValueError:", error) Output
12 -> 7 -> 31 -> None 12 7 31 None [12, 7, 31] [] None ValueError: cycle: after 4 nodes the list returns to position 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 try_helpers.py
py/lists.py
"""Test helpers for singly linked lists: build one from a Python list, read it back, and print it."""
class ListNode:
"""One node: a value and a reference to the next node (None after the last node)."""
def __init__(self, val, next=None):
self.val = val
self.next = next
def from_list(values):
"""Build a linked list holding `values` in order and return its head (None for no values)."""
dummy = ListNode(None) # a node in front of the head, so the first append is like every other
tail = dummy
for v in values:
tail.next = ListNode(v)
tail = tail.next
return dummy.next
def to_list(head):
"""Return the values from `head` to the end, in order.
A list that loops back on itself has no end; instead of running forever, this raises ValueError and says where
the loop closes.
"""
values = []
position = {} # id of each node seen -> its position
node = head
while node is not None:
if id(node) in position:
raise ValueError(f"cycle: after {len(values)} nodes the list returns to position {position[id(node)]}")
position[id(node)] = len(values)
values.append(node.val)
node = node.next
return values
def show(head):
"""The list as one line of text, such as 3 -> 1 -> 4 -> None."""
return " -> ".join([*map(str, to_list(head)), "None"]) Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Two details in lists.py pay off later. from_list already uses a dummy node: it appends every value behind a
tail that starts on a node in front of the real head, so the first value needs no special case, and it returns
dummy.next. And to_list refuses to run forever. A rewiring bug can make the last node point back into the list,
and then a plain loop never reaches the end; this one remembers the position of every node it has seen and raises
ValueError when it meets one again, saying where the loop closes. It records nodes by id(), which the Python
documentation guarantees to be unique among objects that exist at the same time, as all the nodes of one list do. It
cannot record values instead, because values repeat: the list 5 → 5 → 2 is not a cycle.
The same node looks a little different in each language, and so does the mistake of following the end marker.
For each language: the next field, the end of the list, a dummy in front of head, what happens to a node nothing
refers to, and what reading the next field of the end marker does.
- Python:
next;None;ListNode(None, head); reclaimed by the runtime;AttributeError. - JavaScript:
next;null;new ListNode(null, head); reclaimed;TypeError. - Java:
ListNode next;null;new ListNode(0, head); reclaimed;NullPointerException. - C++:
ListNode* next;nullptr; a localListNode dummy{0, head};; yours todelete; undefined behaviour. - Go:
Next *Node;nil;&Node{Next: head}; reclaimed; a run-time panic.
The dummy’s value is never read, so any value will do. Memory matters in C++ only: Python, JavaScript, Java and Go
reclaim objects that nothing refers to any more, while C++ code that unlinks a node created with new must also
delete it. The last item on each line is why the removal loop below always checks that a node exists before
reading its next. Go is not run on this page; its line comes from the language specification and the Go FAQ, and
the specification also makes prev.Next legal on a pointer without writing (*prev).Next.
The standard libraries have linked lists of their own. Java’s LinkedList, Go’s container/list and C++’s
std::list are doubly linked. C++’s std::forward_list is singly linked, and its interface is built on the idea of
this lesson: it inserts and removes with insert_after and erase_after, and before_begin() gives a position in
front of the first element, a dummy in all but name. Interview problems hand you bare nodes instead, because
rewiring them is the skill being tested. And when all you need is a queue, the Java documentation expects
ArrayDeque to be faster than LinkedList.
A worked problem: cancelled bus stops
The problem
A bus route is stored as a singly linked list of stop numbers, from the depot to the last stop. Road works cancel
some stops for the day, given as an array cancelled. Remove every stop whose number is cancelled, keep the other
stops in their order, and return the head of the shortened route. Reuse the nodes you are given rather than
building new ones. A route can pass the same stop twice; both visits go. The route has up to 100,000 stops, and up
to 100,000 stop numbers can be cancelled.
Four examples, worked by hand before any code, cover the places where a removal can happen:
4 → 9 → 4 → 15 → 2without[4, 2]gives9 → 15: the head, a middle stop and the last stop go.8 → 8 → 3without[8]gives3: two cancelled stops in a row at the front.5 → 6without[5, 6]gives an empty route.- An empty route stays empty.
The brute force, and why it is too slow
The most direct method takes one cancelled number at a time and walks the whole route, unlinking every stop that carries it. It is correct, and it costs k walks of n stops: with n = k = 100,000 that is 10¹⁰ checks. The lesson on reading constraints plans with roughly 10⁷ simple steps a second for CPython and 10⁸ for compiled code, so this brute force would run for over a quarter of an hour in Python and well over a minute in Java or C++.
Copying the stops that stay into an array and building a new list from it is fast, but it creates new nodes, which the problem rules out: other parts of a program may hold references to the original stops. It also needs memory for a second copy of the route, where rewiring the existing nodes needs none.
One pass with a set, and the trouble with the head
What the brute force repeats is the question “is this stop cancelled?”, asked once per cancelled number. A set
answers it in one step on average: the Python documentation lists x in s for a set as O(1) on average. So put the
cancelled numbers in a set and walk the route once. Removing a stop means changing the next of the stop before it,
so the loop stands on a node and looks at the node after it.
The trouble is the head: it has no node before it. The first version below handles that with a loop of its own, which skips cancelled stops at the front before the main loop starts. It works, but the head is now handled by different code from every other node, and two copies of the same decision are two places to get it wrong.
A dummy node in front of the head
The second version adds one node, dummy, whose next is the head. Now every real stop, the first one included,
has a node before it. The loop keeps prev on the last stop kept so far and looks at prev.next: a cancelled stop
is unlinked with one assignment, any other stop becomes the new prev.
One unlink step, with a dummy node in front of the head
Text description of the diagram
The route 8, 3, 5 with stop 8 cancelled, drawn from top to bottom.
The pointer prev is on the dummy node, which sits in front of the first real stop. Before the step, the dummy's next is 8, a cancelled stop; this link is drawn dashed. The 8 points at 3, 3 points at 5, and 5 points at None.
The one assignment prev.next = prev.next.next changes the dummy's next to 3, drawn as a solid link. The node 8 is drawn dashed: it still points at 3, but nothing points at it any more, so it is no longer part of the route. The pointer prev has not moved: it stays on the dummy, because its new next, 3, has not been checked yet.
Without the dummy, removing the first stop would mean changing the head itself, which needs code of its own. With the dummy, the first stop is unlinked by the same assignment as any other stop.
After an unlink, prev stays where it is, because its new next has not been checked yet; that is what removes the
second 8 in 8 → 8 → 3. The loop keeps an invariant, true every time it tests prev.next: the nodes from the
dummy to prev are exactly the stops kept so far, in their order, and prev.next is the first stop not checked
yet. When prev.next is empty, every stop has been checked, so dummy.next is the answer, even when the old head
was removed or every stop was. (The lesson on dry runs and invariants
shows how to find such statements.) Each step either removes a node or moves prev, so the loop runs n times: the
whole function is O(n + k) time and O(k) extra memory for the set.
py/stops.py
import random
from lists import ListNode, from_list, show, to_list
def remove_cancelled_no_dummy(head, cancelled):
"""Without a dummy: the head needs a loop of its own."""
gone = set(cancelled)
while head is not None and head.val in gone: # cancelled stops at the front
head = head.next
node = head
while node is not None and node.next is not None:
if node.next.val in gone:
node.next = node.next.next # unlink, and look at the new next before moving on
else:
node = node.next
return head
def remove_cancelled(head, cancelled):
"""With a dummy in front of the head: one loop for every stop, the first one included."""
gone = set(cancelled)
dummy = ListNode(None, head)
prev = dummy # the last stop kept so far
while prev.next is not None:
if prev.next.val in gone:
prev.next = prev.next.next # unlink; prev stays, its new next is not checked yet
else:
prev = prev.next
return dummy.next
EXAMPLES = [
([4, 9, 4, 15, 2], [4, 2]),
([8, 8, 3], [8]),
([5, 6], [5, 6]),
([], [1]),
]
for route, cancelled in EXAMPLES:
print(f"{route} without {cancelled}: {show(remove_cancelled(from_list(route), cancelled))}")
# The nodes that stay are the caller's own nodes: no copies.
head = from_list([4, 9, 4, 15, 2])
nine = head.next
print("the 9 node is the new head:", remove_cancelled(head, [4, 2]) is nine)
# Both versions against a plain list filter, on 2,000 random routes with repeated stop numbers.
rng = random.Random(7)
for _ in range(2000):
route = [rng.randint(1, 6) for _ in range(rng.randint(0, 9))]
cancelled = rng.sample(range(1, 7), rng.randint(0, 4))
expected = [stop for stop in route if stop not in cancelled]
assert to_list(remove_cancelled(from_list(route), cancelled)) == expected
assert to_list(remove_cancelled_no_dummy(from_list(route), cancelled)) == expected
print("2000 random routes: both versions match the list filter") Output
[4, 9, 4, 15, 2] without [4, 2]: 9 -> 15 -> None [8, 8, 3] without [8]: 3 -> None [5, 6] without [5, 6]: None [] without [1]: None the 9 node is the new head: True 2000 random routes: both versions match the list filter
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stops.py
py/lists.py
"""Test helpers for singly linked lists: build one from a Python list, read it back, and print it."""
class ListNode:
"""One node: a value and a reference to the next node (None after the last node)."""
def __init__(self, val, next=None):
self.val = val
self.next = next
def from_list(values):
"""Build a linked list holding `values` in order and return its head (None for no values)."""
dummy = ListNode(None) # a node in front of the head, so the first append is like every other
tail = dummy
for v in values:
tail.next = ListNode(v)
tail = tail.next
return dummy.next
def to_list(head):
"""Return the values from `head` to the end, in order.
A list that loops back on itself has no end; instead of running forever, this raises ValueError and says where
the loop closes.
"""
values = []
position = {} # id of each node seen -> its position
node = head
while node is not None:
if id(node) in position:
raise ValueError(f"cycle: after {len(values)} nodes the list returns to position {position[id(node)]}")
position[id(node)] = len(values)
values.append(node.val)
node = node.next
return values
def show(head):
"""The list as one line of text, such as 3 -> 1 -> 4 -> None."""
return " -> ".join([*map(str, to_list(head)), "None"]) Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Three checks back up the hand examples. The four routes print what was worked out above. The identity check shows that the 9 node itself became the new head, so no node was copied. And the stress test compares both versions with a plain list filter on 2,000 random routes with repeated stop numbers; small values make repeats, runs at the front and empty results common, which is where linked-list code breaks.
The same function in JavaScript, Java and C++
The loop is the same in every language; only the spelling changes. The JavaScript version runs in your browser; the Java and C++ versions were built and run by the session below them.
js/stops.mjs
import { ListNode, fromList, show } from './lists.mjs';
/** Remove every stop whose number is in `cancelled`; return the new head. */
export function removeCancelled(head, cancelled) {
const gone = new Set(cancelled);
const dummy = new ListNode(null, head);
let prev = dummy; // the last stop kept so far
while (prev.next !== null) {
if (gone.has(prev.next.val)) prev.next = prev.next.next;
else prev = prev.next;
}
return dummy.next;
}
const examples = [
[[4, 9, 4, 15, 2], [4, 2]],
[[8, 8, 3], [8]],
[[5, 6], [5, 6]],
[[], [1]],
];
for (const [route, cancelled] of examples) {
const result = show(removeCancelled(fromList(route), cancelled));
console.log(`[${route.join(', ')}] without [${cancelled.join(', ')}]: ${result}`);
} Output
[4, 9, 4, 15, 2] without [4, 2]: 9 -> 15 -> null [8, 8, 3] without [8]: 3 -> null [5, 6] without [5, 6]: null [] without [1]: null
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node stops.mjs
js/lists.mjs
// Test helpers for singly linked lists: build one from an array, read it back, and print it.
export class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next; // null after the last node
}
}
/** Build a linked list holding `values` in order and return its head (null for no values). */
export function fromList(values) {
const dummy = new ListNode(null);
let tail = dummy;
for (const v of values) {
tail.next = new ListNode(v);
tail = tail.next;
}
return dummy.next;
}
/** The values from `head` to the end. A list that loops back on itself throws instead of running forever. */
export function toList(head) {
const values = [];
const position = new Map(); // node -> its position
for (let node = head; node !== null; node = node.next) {
if (position.has(node)) {
throw new Error(`cycle: after ${values.length} nodes the list returns to position ${position.get(node)}`);
}
position.set(node, values.length);
values.push(node.val);
}
return values;
}
/** The list as one line of text, such as 3 -> 1 -> 4 -> null. */
export const show = (head) => [...toList(head), 'null'].join(' -> '); Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Java · compiled/Stops.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Stops {
static final class ListNode {
int val;
ListNode next; // null after the last node
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
static ListNode fromList(int... values) {
ListNode dummy = new ListNode(0, null); // its value is never read
ListNode tail = dummy;
for (int v : values) {
tail.next = new ListNode(v, null);
tail = tail.next;
}
return dummy.next;
}
/** The values from head to the end; a list that loops back on itself throws instead of running forever. */
static List<Integer> toList(ListNode head) {
List<Integer> values = new ArrayList<>();
Map<ListNode, Integer> position = new IdentityHashMap<>();
for (ListNode node = head; node != null; node = node.next) {
Integer seen = position.putIfAbsent(node, values.size());
if (seen != null) {
throw new IllegalStateException("cycle: after " + values.size() + " nodes the list returns to position " + seen);
}
values.add(node.val);
}
return values;
}
static String show(ListNode head) {
StringBuilder text = new StringBuilder();
for (int v : toList(head)) text.append(v).append(" -> ");
return text.append("null").toString();
}
static ListNode removeCancelled(ListNode head, int[] cancelled) {
Set<Integer> gone = new HashSet<>();
for (int stop : cancelled) gone.add(stop);
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy; // the last stop kept so far
while (prev.next != null) {
if (gone.contains(prev.next.val)) prev.next = prev.next.next;
else prev = prev.next;
}
return dummy.next;
}
public static void main(String[] args) {
int[][][] examples = {
{{4, 9, 4, 15, 2}, {4, 2}},
{{8, 8, 3}, {8}},
{{5, 6}, {5, 6}},
{{}, {1}},
};
for (int[][] e : examples) {
String result = show(removeCancelled(fromList(e[0]), e[1]));
System.out.println(Arrays.toString(e[0]) + " without " + Arrays.toString(e[1]) + ": " + result);
}
}
} C++ · compiled/stops.cpp
#include <cstddef>
#include <iostream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
struct ListNode {
int val;
ListNode* next; // nullptr after the last node
};
ListNode* from_list(const std::vector<int>& values) {
ListNode dummy{0, nullptr}; // on the stack: it is never deleted
ListNode* tail = &dummy;
for (int v : values) {
tail->next = new ListNode{v, nullptr};
tail = tail->next;
}
return dummy.next;
}
// The values from head to the end; a list that loops back on itself throws instead of running forever.
std::vector<int> to_list(const ListNode* head) {
std::vector<int> values;
std::unordered_map<const ListNode*, std::size_t> position;
for (const ListNode* node = head; node != nullptr; node = node->next) {
auto [it, added] = position.try_emplace(node, values.size());
if (!added) {
throw std::runtime_error("cycle: after " + std::to_string(values.size()) +
" nodes the list returns to position " + std::to_string(it->second));
}
values.push_back(node->val);
}
return values;
}
std::string show(const ListNode* head) {
std::string text;
for (int v : to_list(head)) text += std::to_string(v) + " -> ";
return text + "nullptr";
}
void free_list(ListNode* head) {
while (head != nullptr) {
ListNode* next = head->next; // save next before the node is deleted
delete head;
head = next;
}
}
ListNode* remove_cancelled(ListNode* head, const std::vector<int>& cancelled) {
std::unordered_set<int> gone(cancelled.begin(), cancelled.end());
ListNode dummy{0, head};
ListNode* prev = &dummy; // the last stop kept so far
while (prev->next != nullptr) {
ListNode* cur = prev->next;
if (gone.contains(cur->val)) {
prev->next = cur->next; // unlink first, then free: nothing points to cur any more
delete cur;
} else {
prev = cur;
}
}
return dummy.next;
}
std::string bracketed(const std::vector<int>& values) {
std::string text = "[";
for (std::size_t i = 0; i < values.size(); ++i) text += (i ? ", " : "") + std::to_string(values[i]);
return text + "]";
}
int main() {
const std::vector<std::pair<std::vector<int>, std::vector<int>>> examples = {
{{4, 9, 4, 15, 2}, {4, 2}},
{{8, 8, 3}, {8}},
{{5, 6}, {5, 6}},
{{}, {1}},
};
for (const auto& [route, cancelled] : examples) {
ListNode* head = remove_cancelled(from_list(route), cancelled);
std::cout << bracketed(route) << " without " << bracketed(cancelled) << ": " << show(head) << '\n';
free_list(head);
}
} compiled/run_stops.py
"""Builds and runs Stops.java (compiled for Java 21 and later) and stops.cpp (C++23 with sanitizers)."""
from terminal import run
run("""
java --version | head -n 1 | cut -d ' ' -f 1,2
javac --release 21 -d classes Stops.java
java -cp classes Stops
clang++ --version | head -n 1
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined stops.cpp -o stops
./stops
""") Output
$ java --version | head -n 1 | cut -d ' ' -f 1,2 openjdk 27 $ javac --release 21 -d classes Stops.java $ java -cp classes Stops [4, 9, 4, 15, 2] without [4, 2]: 9 -> 15 -> null [8, 8, 3] without [8]: 3 -> null [5, 6] without [5, 6]: null [] without [1]: null $ clang++ --version | head -n 1 Apple clang version 21.0.0 (clang-2100.3.34.2) $ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined stops.cpp -o stops $ ./stops [4, 9, 4, 15, 2] without [4, 2]: 9 -> 15 -> nullptr [8, 8, 3] without [8]: 3 -> nullptr [5, 6] without [5, 6]: nullptr [] without [1]: nullptr
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 run_stops.py
compiled/terminal.py
"""Runs commands as you would type them in a terminal and prints the session: each command after "$ ", then what it
printed (standard output and standard error together). A failing command adds its exit status, and the session stops.
Java is the OpenJDK that Homebrew installs (its folder goes first on PATH when it exists); C++ is built with the
system's clang++. An AddressSanitizer report is cut to the lines that say what went wrong and where: the error, the
access, and the stack frames in this folder's own files. Memory addresses and the process number change on every run,
so they are printed as 0x… and ==…==.
"""
import os
import re
import subprocess
JDK_BIN = "/opt/homebrew/opt/openjdk/bin"
ADDRESS = re.compile(r"0x[0-9a-fA-F]+")
FRAME = re.compile(r"^\s*#(\d+) 0x[0-9a-fA-F]+ in (.+?) (\S+?):(\d+)(?::\d+)?$")
def shorten(lines):
"""Keeps the lines of a sanitizer report that a reader needs; other lines pass through unchanged."""
start = next((i for i, line in enumerate(lines) if "ERROR: AddressSanitizer" in line), None)
if start is None:
return lines
own = {name for name in os.listdir(".") if name.endswith(".cpp")}
kept, dropped = [line for line in lines[:start] if not line.startswith("=====")], 0
for line in lines[start:]:
frame = FRAME.match(line)
if "ERROR: AddressSanitizer" in line:
kept.append(ADDRESS.sub("0x…", re.sub(r"^==\d+==", "==…==", line).split(" at pc ")[0]))
elif re.match(r"^(READ|WRITE) of size \d+", line):
kept.append(ADDRESS.sub("0x…", line))
elif re.match(r"^(freed|previously allocated) by thread", line):
kept.append(line)
elif frame and os.path.basename(frame.group(3)) in own:
kept.append(f" #{frame.group(1)} in {frame.group(2)} {os.path.basename(frame.group(3))}:{frame.group(4)}")
else:
dropped += 1
kept.append(f"[{dropped} more lines of the report left out]")
return kept
def run(script):
env = dict(os.environ, ASAN_OPTIONS="color=never", UBSAN_OPTIONS="color=never")
if os.path.isdir(JDK_BIN):
env["PATH"] = os.pathsep.join([JDK_BIN, env.get("PATH", "")])
for command in script.strip().splitlines():
command = command.strip()
print(f"$ {command}", flush=True)
result = subprocess.run(command, shell=True, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
for line in shorten(result.stdout.splitlines()):
print(line.rstrip())
if result.returncode != 0:
status = result.returncode if result.returncode > 0 else 128 - result.returncode
print(f"[exit status {status}]")
return Java’s toList uses an IdentityHashMap, which compares keys by reference rather than with equals, for the same
reason that Python records id(). The Java file is compiled with --release 21, which checks it against the rules
and the library of Java 21, so the same code also builds where only that version is offered. The C++ version is the
only one that has to free memory: it unlinks a cancelled node first, then deletes it, and its dummy is a local
variable that disappears when the function returns. It was built with AddressSanitizer and UndefinedBehaviorSanitizer,
which stayed silent on all four examples.
A dry run of the loop
Before writing a rewiring loop, trace one small case with the pointers written out. This run prints the state at every check of the loop, for a route with a cancelled run at the front and a cancelled last stop:
from lists import ListNode, from_list, to_list
def remove_cancelled_traced(head, cancelled):
"""remove_cancelled from stops.py, printing the pointers at every check of the loop."""
gone = set(cancelled)
dummy = ListNode("dummy", head)
prev = dummy
step = 0
print(f"{'step':<5} {'prev':<6} {'prev.next':<10} {'action':<16} list after the step")
while prev.next is not None:
step += 1
here, nxt = prev.val, prev.next.val
if nxt in gone:
prev.next = prev.next.next
action = f"unlink {nxt}"
else:
prev = prev.next
action = f"keep, prev = {nxt}"
print(f"{step:<5} {here!s:<6} {nxt!s:<10} {action:<16} {' -> '.join(map(str, to_list(dummy)))}")
print(f"{'end':<5} {prev.val!s:<6} {'None':<10} return dummy.next, which is {to_list(dummy.next)}")
return dummy.next
shortened = remove_cancelled_traced(from_list([8, 8, 3, 5]), [8, 5]) Output
step prev prev.next action list after the step 1 dummy 8 unlink 8 dummy -> 8 -> 3 -> 5 2 dummy 8 unlink 8 dummy -> 3 -> 5 3 dummy 3 keep, prev = 3 dummy -> 3 -> 5 4 3 5 unlink 5 dummy -> 3 end 3 None return dummy.next, which is [3]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trace_stops.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Steps 1 and 2 show prev staying on the dummy while two 8s are unlinked in a row. Step 3 keeps 3 and moves prev
onto it. Step 4 unlinks the last stop, and the loop ends with prev.next empty.
Save next before you overwrite it
Most linked-list bugs are lost references. A node’s next is often the only way to reach the rest of the list, so
the moment you assign to it, the old value is gone unless you saved it first. Inserting a new stop after a given stop
shows the difference that order makes:
from lists import ListNode, from_list, show, to_list
def insert_after_wrong(node, val):
new = ListNode(val)
node.next = new # the only reference to the rest of the route is overwritten here
new.next = node.next # so this makes the new node point to itself
def insert_after(node, val):
new = ListNode(val)
new.next = node.next # save the rest of the route in the new node first
node.next = new
route = from_list([4, 9, 15])
insert_after(route.next, 6)
print("right order:", show(route))
route = from_list([4, 9, 15])
insert_after_wrong(route.next, 6)
try:
print(to_list(route))
except ValueError as error:
print("wrong order: ValueError:", error)
print("the node after 6 is 6 itself:", route.next.next.next is route.next.next) Output
right order: 4 -> 9 -> 6 -> 15 -> None wrong order: ValueError: cycle: after 3 nodes the list returns to position 2 the node after 6 is 6 itself: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lost_link.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The right order first copies the rest of the route into the new node, then links the new node in. The wrong order
links the new node first, so the second assignment reads node.next when it already holds the new node: the new
node now points to itself, the old stop 15 is lost, and to_list reports a cycle at position 2 instead of hanging.
Without the check in the helper, this bug looks like a test that never finishes.
In C++ the same habit protects memory. The function below frees a list but reads each node’s next after deleting
the node:
#include <iostream>
struct ListNode {
int val;
ListNode* next;
};
// Frees every node, but reads each node's next after deleting it.
void free_list_wrong(ListNode* head) {
while (head != nullptr) {
delete head;
head = head->next; // head was just deleted: this reads freed memory
}
}
int main() {
ListNode* route = new ListNode{4, new ListNode{9, new ListNode{15, nullptr}}};
std::cout << "freeing 4 -> 9 -> 15" << std::endl;
free_list_wrong(route);
std::cout << "done" << std::endl;
} """Builds free_bug.cpp with AddressSanitizer and runs it: the report names the line that read a deleted node."""
from terminal import run
run("""
clang++ -std=c++23 -g -fsanitize=address free_bug.cpp -o free_bug
./free_bug
""") Output
$ clang++ -std=c++23 -g -fsanitize=address free_bug.cpp -o free_bug
$ ./free_bug
freeing 4 -> 9 -> 15
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 8 at 0x… thread T0
#0 in free_list_wrong(ListNode*) free_bug.cpp:12
#1 in main free_bug.cpp:19
freed by thread T0 here:
#1 in free_list_wrong(ListNode*) free_bug.cpp:11
#2 in main free_bug.cpp:19
previously allocated by thread T0 here:
#1 in main free_bug.cpp:17
[42 more lines of the report left out]
[exit status 134]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 run_free_bug.py
AddressSanitizer stops the program at the first read of freed memory and names the line: free_bug.cpp:12, the read,
of a node deleted one line earlier. Without the sanitizer nothing guarantees a crash, because reading freed memory
is undefined behaviour and the program may well seem to work. The fix is the order used by free_list in
stops.cpp: save next in a local variable, then delete the node.
Building a new list behind a dummy
The dummy has a second use: building a list. Keep a tail that starts on the dummy, append each new node at
tail.next, move tail onto it, and return dummy.next at the end. from_list works this way, and so does
arithmetic on numbers stored one digit per node, least significant digit first. Adding one to such a number shows
the case that is easiest to forget, a carry left over at the end:
from lists import ListNode, from_list, show, to_list
def plus_one(head):
"""A new list holding the number plus one. Digits are stored least significant first: 7 -> 2 -> 1 is 127."""
dummy = ListNode(None)
tail = dummy # the last node of the result so far
carry = 1
node = head
while node is not None:
total = node.val + carry
tail.next = ListNode(total % 10)
tail = tail.next
carry = total // 10
node = node.next
if carry:
tail.next = ListNode(carry) # a carry left at the end needs one more node: 999 + 1 = 1000
return dummy.next
for digits in ([7, 2, 1], [9, 9, 9], [5, 9], [0]):
print(f"{show(from_list(digits)):<22} plus one: {show(plus_one(from_list(digits)))}")
def digits_of(n):
"""The digits of n >= 0, least significant first: 127 -> [7, 2, 1]."""
return [int(d) for d in reversed(str(n))]
# Checked against Python's own integers for every number below 20,000.
assert all(to_list(plus_one(from_list(digits_of(n)))) == digits_of(n + 1) for n in range(20_000))
print("0 to 19999: every result matches n + 1") Output
7 -> 2 -> 1 -> None plus one: 8 -> 2 -> 1 -> None 9 -> 9 -> 9 -> None plus one: 0 -> 0 -> 0 -> 1 -> None 5 -> 9 -> None plus one: 6 -> 9 -> None 0 -> None plus one: 1 -> None 0 to 19999: every result matches n + 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 plus_one.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
9 → 9 → 9 is 999, and 999 + 1 = 1000 has one digit more than the input, so the carry left after the loop needs a
node of its own. The last line checks every number below 20,000 against Python’s own integers. Real programs store
big numbers in Python’s int, Java’s BigInteger or JavaScript’s BigInt; digit lists exist to practise building
lists and carrying, and the big integers make an exact checker for them.
Practise: remove a value, then add two digit lists
The exercise asks for two functions in Python or JavaScript, with the helpers already in the starter file:
remove_all(head, value), which unlinks every node equal to value, and add_digit_lists(a, b), which adds two
digit lists of any lengths. The sample tests include runs of matches at the front, a carry at the end, empty lists,
and random cases checked against a list filter and against big integers. Draw the pointers for 7 → 7 → 2 → 7
before you start.
Exercise · Easy · Python, JavaScript
Remove a value, then add two digit lists
Two functions, one technique: a dummy node in front of the list you work on. The starter file already has the ListNode class and the from_list and to_list helpers from this lesson (fromList and toList in JavaScript), so every test is one line.
1. remove_all(head, value) unlinks every node whose val equals value and returns the new head. Matches can sit at the head, in a row and at the tail, and every node may match. Reuse the nodes you are given: the tests check that the nodes that stay are the caller's own. 2. add_digit_lists(a, b) adds two numbers stored one digit per node, least significant digit first, and returns the sum as a new list in the same form. The lists may have different lengths, an empty list counts as zero, and a and b must not change.
to_list(remove_all(from_list([7, 7, 2, 7]), 7)) # [2]
to_list(add_digit_lists(from_list([7, 2, 4]), from_list([5, 8]))) # [2, 1, 5], because 427 + 85 = 512
to_list(add_digit_lists(from_list([9, 9]), from_list([1]))) # [0, 0, 1], because 99 + 1 = 100
Draw the pointers for [7, 7, 2, 7] before you write the loop, then run the sample tests.
Python · Starter code · linked.py
class ListNode:
def __init__(self, val, next=None):
self.val = val
self.next = next
def from_list(values):
"""Build a linked list holding values in order and return its head (None for no values)."""
dummy = ListNode(None)
tail = dummy
for v in values:
tail.next = ListNode(v)
tail = tail.next
return dummy.next
def to_list(head):
"""The values from head to the end; raises ValueError if the list loops back on itself."""
values, seen = [], set()
while head is not None:
if id(head) in seen:
raise ValueError("cycle: the list loops back on itself")
seen.add(id(head))
values.append(head.val)
head = head.next
return values
def remove_all(head, value):
"""Unlink every node whose val equals value and return the new head. Reuse the nodes: create none."""
# Replace this line with your code.
return head
def add_digit_lists(a, b):
"""Return a new list holding the sum of a and b.
a and b hold numbers of zero or more, one digit (0-9) per node, least significant digit first, so 7 -> 2 -> 4 is
427. An empty list counts as zero. The result uses the same form, without leading zeros except for the number 0
itself, which is a single 0 node. Leave a and b unchanged.
"""
# Replace this line with your code.
return None The sample tests · test_linked.py
import random
from linked import add_digit_lists, from_list, remove_all, to_list
def digits_of(n):
"""The digits of n, least significant first: 427 -> [7, 2, 4]."""
return [int(d) for d in reversed(str(n))]
def test_remove_in_the_middle_and_at_the_head():
"""removes every match, wherever it is"""
assert to_list(remove_all(from_list([1, 2, 1, 3]), 1)) == [2, 3]
def test_remove_a_run_at_the_head():
"""several matches in a row at the front"""
assert to_list(remove_all(from_list([7, 7, 7, 2]), 7)) == [2]
def test_remove_at_the_tail():
"""matches at the end, one after another"""
assert to_list(remove_all(from_list([1, 2, 2]), 2)) == [1]
def test_remove_everything_or_nothing():
"""every node removed, an empty list, and a value that is not there"""
assert remove_all(from_list([5, 5]), 5) is None
assert remove_all(None, 5) is None
assert to_list(remove_all(from_list([1, 2]), 9)) == [1, 2]
def test_remove_reuses_the_nodes():
"""the nodes that stay are the caller's own nodes"""
head = from_list([3, 8, 3])
eight = head.next
assert remove_all(head, 3) is eight
assert eight.next is None
def test_remove_against_a_list_filter():
"""300 random lists with repeated values"""
rng = random.Random(11)
for _ in range(300):
values = [rng.randint(0, 3) for _ in range(rng.randint(0, 8))]
target = rng.randint(0, 3)
assert to_list(remove_all(from_list(values), target)) == [v for v in values if v != target], (values, target)
def test_add_lists_of_different_lengths():
"""427 + 85 = 512"""
assert to_list(add_digit_lists(from_list([7, 2, 4]), from_list([5, 8]))) == [2, 1, 5]
def test_add_with_a_carry_at_the_end():
"""99 + 1 = 100: the carry needs a new last node"""
assert to_list(add_digit_lists(from_list([9, 9]), from_list([1]))) == [0, 0, 1]
def test_add_zeros_and_empty_lists():
"""0 + 0 is a single 0 node, and an empty list counts as zero"""
assert to_list(add_digit_lists(from_list([0]), from_list([0]))) == [0]
assert to_list(add_digit_lists(from_list([]), from_list([4, 1]))) == [4, 1]
assert to_list(add_digit_lists(None, None)) == [0]
def test_add_leaves_the_inputs_unchanged():
"""a and b still hold their numbers afterwards"""
a, b = from_list([9, 9, 9]), from_list([3])
add_digit_lists(a, b)
assert to_list(a) == [9, 9, 9] and to_list(b) == [3]
def test_add_against_integers():
"""300 random pairs, up to 30 digits, checked with Python's own integers"""
rng = random.Random(5)
for _ in range(300):
x, y = rng.randrange(10 ** rng.randint(1, 30)), rng.randrange(10 ** rng.randint(1, 30))
assert to_list(add_digit_lists(from_list(digits_of(x)), from_list(digits_of(y)))) == digits_of(x + y), (x, y) JavaScript · Starter code · linked.mjs
export class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
/** Build a linked list holding `values` in order and return its head (null for no values). */
export function fromList(values) {
const dummy = new ListNode(null);
let tail = dummy;
for (const v of values) {
tail.next = new ListNode(v);
tail = tail.next;
}
return dummy.next;
}
/** The values from head to the end; throws if the list loops back on itself. */
export function toList(head) {
const values = [];
const seen = new Set();
for (let node = head; node !== null; node = node.next) {
if (seen.has(node)) throw new Error('cycle: the list loops back on itself');
seen.add(node);
values.push(node.val);
}
return values;
}
/** Unlink every node whose val equals value and return the new head. Reuse the nodes: create none. */
export function removeAll(head, value) {
// Replace this line with your code.
return head;
}
/**
* Return a new list holding the sum of a and b. a and b hold numbers of zero or more, one digit (0-9) per node,
* least significant digit first, so 7 -> 2 -> 4 is 427. An empty list (null) counts as zero. The result uses the
* same form, without leading zeros except for the number 0 itself, which is a single 0 node. Leave a and b unchanged.
*/
export function addDigitLists(a, b) {
// Replace this line with your code.
return null;
} The sample tests · linked.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { addDigitLists, fromList, removeAll, toList } from './linked.mjs';
let seed = 11;
const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
const randInt = (lo, hi) => lo + Math.floor(random() * (hi - lo + 1));
test('removes every match, wherever it is', () => {
assert.deepEqual(toList(removeAll(fromList([1, 2, 1, 3]), 1)), [2, 3]);
assert.deepEqual(toList(removeAll(fromList([7, 7, 7, 2]), 7)), [2]);
assert.deepEqual(toList(removeAll(fromList([1, 2, 2]), 2)), [1]);
});
test('every node removed, an empty list, and a value that is not there', () => {
assert.equal(removeAll(fromList([5, 5]), 5), null);
assert.equal(removeAll(null, 5), null);
assert.deepEqual(toList(removeAll(fromList([1, 2]), 9)), [1, 2]);
});
test('the nodes that stay are the caller\'s own nodes', () => {
const head = fromList([3, 8, 3]);
const eight = head.next;
assert.equal(removeAll(head, 3), eight);
assert.equal(eight.next, null);
});
test('300 random lists with repeated values', () => {
for (let k = 0; k < 300; k++) {
const values = Array.from({ length: randInt(0, 8) }, () => randInt(0, 3));
const target = randInt(0, 3);
assert.deepEqual(toList(removeAll(fromList(values), target)), values.filter((v) => v !== target), JSON.stringify([values, target]));
}
});
test('427 + 85 = 512, and 99 + 1 = 100 with a carry at the end', () => {
assert.deepEqual(toList(addDigitLists(fromList([7, 2, 4]), fromList([5, 8]))), [2, 1, 5]);
assert.deepEqual(toList(addDigitLists(fromList([9, 9]), fromList([1]))), [0, 0, 1]);
});
test('0 + 0 is a single 0 node, and an empty list counts as zero', () => {
assert.deepEqual(toList(addDigitLists(fromList([0]), fromList([0]))), [0]);
assert.deepEqual(toList(addDigitLists(fromList([]), fromList([4, 1]))), [4, 1]);
assert.deepEqual(toList(addDigitLists(null, null)), [0]);
});
test('a and b still hold their numbers afterwards', () => {
const a = fromList([9, 9, 9]);
const b = fromList([3]);
addDigitLists(a, b);
assert.deepEqual(toList(a), [9, 9, 9]);
assert.deepEqual(toList(b), [3]);
});
test('300 random pairs, up to 30 digits, checked with BigInt', () => {
const digitsOf = (n) => [...n.toString()].reverse().map(Number);
const randomBig = () => BigInt(Array.from({ length: randInt(1, 30) }, () => randInt(0, 9)).join(''));
for (let k = 0; k < 300; k++) {
const x = randomBig();
const y = randomBig();
assert.deepEqual(toList(addDigitLists(fromList(digitsOf(x)), fromList(digitsOf(y)))), digitsOf(x + y), `${x} + ${y}`);
}
}); A hint
Both functions start with dummy = ListNode(None). For remove_all, set dummy.next = head and walk with prev, moving it only when its next node stays. For add_digit_lists, append each digit of the sum at a tail that starts at the dummy, and keep looping while either list has digits left or the carry is not zero. What must the function return when both lists are empty?
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is a dummy head node, and why do linked-list solutions use one?
A dummy head, or sentinel, is an extra node placed in front of the real first node, with a value that is never
read. Code that removes or inserts nodes works through the node before the one it changes, and the real head has
none, so without a dummy every such function needs a separate branch for the head and must return a head that may
have changed. With a dummy, the head is an ordinary node: the loop starts with prev on the dummy, and the function
returns dummy.next, which is the right head even when the old one was removed, or every node was. It costs one
extra node. The same trick builds new lists: append at a tail that starts on the dummy, so the first append is like
every other.
Fresher · To remove a node from a singly linked list, which node do you need, and why?
You need the node before it. Removing B from A → B → C means changing A’s next from B to C, and a singly linked
list can only be followed forwards, so from B alone there is no way back to A. That is why removal loops keep prev
and look at prev.next instead of stopping on the node to remove, and why the head, which has no node before it,
is easiest to handle with a dummy. In C++ you also delete B after unlinking it, reading its next first if you still
need it; in Python, JavaScript, Java and Go, a node that nothing refers to any more is reclaimed by the runtime.
Key takeaways
- Write
from_listandto_listfirst: tests become one line, and ato_listthat tracks the nodes it has seen turns an accidental cycle into an error message instead of a hang. - A dummy node in front of the head gives every real node a node before it, so removals and insertions at the head
need no special case; return
dummy.next. - In a removal loop, stand on
prevand look atprev.next; after an unlink, do not moveprev. - Save
nextbefore you overwrite it: the wrong order makes a node point to itself, and in C++ reading a deleted node is undefined behaviour that AddressSanitizer reports at its line. - To build a list, append behind a tail that starts on a dummy, and remember the carry, or any leftover, at the end.
References
- Data model: objects, values and types (Python Software Foundation)
- Built-in functions: id() (Python Software Foundation)
- Time complexity of operations on built-in types (Python Software Foundation)
- Built-in exceptions: AttributeError (Python Software Foundation)
- JavaScript memory management (MDN Web Docs)
- TypeError: "x" is (not) "y" (MDN Web Docs)
- IdentityHashMap (Java SE 25) (Oracle)
- LinkedList (Java SE 25) (Oracle)
- NullPointerException (Java SE 25) (Oracle)
- ArrayDeque (Java SE 25) (Oracle)
- The javac command: the --release option (Oracle)
- The Java Virtual Machine Specification: the heap (Oracle)
- Undefined behavior (cppreference.com)
- C++ working draft: compound types (using an invalid pointer value) (ISO C++ committee working draft (eel.is))
- std::forward_list (cppreference.com)
- std::unordered_set::contains (cppreference.com)
- AddressSanitizer (The LLVM Project)
- The Go Programming Language Specification: selectors (The Go Authors)
- The Go Programming Language Specification: pointer types (The Go Authors)
- Go FAQ: why do garbage collection? (The Go Authors)
- Package container/list (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress