Part 9 · Lesson 35 · read
Building a Linked List
Nodes and pointers (links to the next node), built from scratch.
On this page
A chain of nodes
An array keeps its items side by side in memory. This is why arr[5] is instant. A linked list works in a different way. Each item lives in its own small object called a node. Each node holds two things: its value, and a reference to the next node (a link that tells you where the next node is; many books call it a pointer). You find the items by following the links. It is like a treasure hunt, where each clue tells you where to find the next clue.
head │ ▼[ 1 | • ]──▶[ 2 | • ]──▶[ 3 | • ]──▶ null value next- The head is the first node. The head stands for the whole list. If you lose the head, you lose the list.
- The
nextof the last node isnull(a special value that means “nothing here”). This marks the end. - An empty list is simply
head === null.
Linked lists are common in interviews because they test one skill in a clean way: changing links without losing any node. You will need the same skill later for trees and graphs.
Building nodes by hand
class ListNode { constructor(val, next = null) { this.val = val; // the data this node holds this.next = next; // the next node, or null if this is the last node }}// 1 -> 2 -> 3const c = new ListNode(3);const b = new ListNode(2, c);const a = new ListNode(1, b);const head = a; // the first node stands for the whole listconsole.log(head.val); // 1console.log(head.next.val); // 2console.log(head.next.next.val); // 3console.log(head.next.next.next); // null (the end of the list)We will use this same ListNode class (with val and next) in every lesson, because it is the class that LeetCode uses. To keep the examples short, we use two helper functions that change an array into a list and a list into an array.
// Two small helper functions used in every example of this part.function fromArray(values) { const dummy = new ListNode(0); let tail = dummy; for (const v of values) { tail.next = new ListNode(v); tail = tail.next; } return dummy.next;}function toArray(head) { const out = []; for (let cur = head; cur !== null; cur = cur.next) out.push(cur.val); return out;}console.log(toArray(fromArray([4, 5, 6]))); // [4, 5, 6]console.log(toArray(null)); // []Traversal and length
There is no list[i]. To visit every node, put a cursor (a moving marker) at the head. Move it with cur = cur.next until it becomes null. All other list code is built on this loop.
function length(head) { let count = 0; let cur = head; // a cursor (a moving marker) that walks along the list while (cur !== null) { count++; cur = cur.next; // move to the next node } return count;}function contains(head, target) { for (let cur = head; cur !== null; cur = cur.next) { if (cur.val === target) return true; } return false;}const list = fromArray([4, 5, 6]);console.log(length(list)); // 3console.log(contains(list, 5)); // trueconsole.log(contains(list, 9)); // falseTo get the i-th node, you must walk i steps. So access takes O(n) time, not O(1) like an array. This is the price you pay for a linked list.
Insert at the head, at the tail and at a position
// At the head: O(1). The new node points at the old head.function addAtHead(head, val) { return new ListNode(val, head); // the new node becomes the head}// At the tail: first walk to the last node, so it is O(n).function addAtTail(head, val) { const node = new ListNode(val); if (head === null) return node; let cur = head; while (cur.next !== null) cur = cur.next; cur.next = node; return head;}// After a node: connect the new node FIRST, and only then cut the old link.function addAfter(node, val) { node.next = new ListNode(val, node.next);}let h = fromArray([2, 3]);h = addAtHead(h, 1); // 1 -> 2 -> 3h = addAtTail(h, 4); // 1 -> 2 -> 3 -> 4addAfter(h.next, 99); // 1 -> 2 -> 99 -> 3 -> 4console.log(toArray(h)); // [1, 2, 99, 3, 4]node, write node.next = new ListNode(val, node.next). The new node points at the old next node before node.next is changed. If you change node.next first, you lose the rest of the list.| Where | Time | Why |
|---|---|---|
| at the head | O(1) | one new node and one link change |
| at the tail (only a head pointer) | O(n) | you must walk to the last node first |
| at the tail (if you also keep a pointer to the tail) | O(1) | no walk needed |
| after a node you already hold | O(1) | two link changes |
| at index i | O(i) | walk to the node before position i |
Delete a node
To remove a node, make the node before it point to the node after it. Nothing points to the removed node now, so it is not part of the list any more. (JavaScript's garbage collector, the part that cleans up unused memory, will free it.) This means you need the previous node. So you walk with a second marker that stays one step behind, or you stop one node early.
function deleteValue(head, target) { if (head === null) return null; if (head.val === target) return head.next; // we delete the head, so the next node becomes the head let prev = head; while (prev.next !== null && prev.next.val !== target) prev = prev.next; if (prev.next !== null) prev.next = prev.next.next; // skip over the node: nothing points to it now return head;}console.log(toArray(deleteValue(fromArray([1, 2, 3, 4]), 3))); // [1, 2, 4]console.log(toArray(deleteValue(fromArray([1, 2, 3, 4]), 1))); // [2, 3, 4]console.log(toArray(deleteValue(fromArray([1, 2, 3, 4]), 9))); // [1, 2, 3, 4]Traced: deleting 3 from 1 → 2 → 3 → 4
cur is a cursor (a moving marker) that walks along the list. prev always stays one node behind cur. To remove a node, you must change the node before it.
▶let prev = null, cur = head;2while (cur !== null) {3 if (cur.val === 3) {4 prev.next = cur.next;5 break;6 }7 prev = cur;8 cur = cur.next;9}| list | [1, 2, 3, 4] |
| prev | "null" |
| cur | 1 |
The dummy head trick (a fake first node)
Deleting the head needs different code from deleting any other node, because the head has no node before it. This special case causes most linked-list bugs. The fix is a dummy node (a fake extra node) that you put in front of the head. Now every real node has a node before it, including the head. The answer is dummy.next.
function removeValue(head, target) { const dummy = new ListNode(0, head); // a fake node in front of the real head let prev = dummy; while (prev.next !== null) { if (prev.next.val === target) prev.next = prev.next.next; // delete it, but stay on prev, because the new next may match too else prev = prev.next; } return dummy.next; // the real head (it may have changed)}console.log(toArray(removeValue(fromArray([6, 6, 1, 6, 2]), 6))); // [1, 2]console.log(toArray(removeValue(fromArray([7, 7]), 7))); // []Look at the loop: after a delete, it does not move prev. After you skip a node, the new prev.next might match too (for example, a row of 6s). So you check it again before you move forward.
Doubly linked lists
A doubly linked list gives every node a prev link (a link to the node before it). So you can walk in both directions, and you can remove a node in O(1) when you only have that node. The cost is one extra link in each node. Also, there are more links to keep correct on every change: four link updates to insert, instead of two. The back and forward history of a browser can be built in this way. The LRU cache design (a cache, which is a small store of recent results, that throws away the item used longest ago; it is built from a hash map, which is a lookup table, plus a doubly linked list) also needs this O(1) removal.
class DNode { constructor(val) { this.val = val; this.prev = null; this.next = null; }}// If you have the node itself, removal is O(1): you do not need to walk to find the node before it.function removeNode(node) { if (node.prev) node.prev.next = node.next; if (node.next) node.next.prev = node.prev; node.prev = node.next = null; // cut it off completely}const a = new DNode(1), b = new DNode(2), c = new DNode(3);a.next = b; b.prev = a; b.next = c; c.prev = b; // 1 <-> 2 <-> 3removeNode(b);console.log(a.next.val, c.prev.val); // 3 1Arrays vs linked lists
| Operation | Array | Linked list |
|---|---|---|
| Access by index | O(1) | O(n) |
| Insert / delete at the front | O(n) (everything moves over) | O(1) |
| Insert / delete in the middle (given the node) | O(n) | O(1) |
| Search for a value | O(n) | O(n) |
| Memory per item | just the value | value + a link (more) |
| Cache friendliness | excellent (items are side by side) | poor (items are spread out) |
Cache friendliness means how well the computer's fast CPU cache can load your data. Items that sit side by side are loaded together, so arrays are fast to scan. In everyday JavaScript, an array is almost always the better choice. We study linked lists to learn pointer skills and to understand structures built on them (queues, LRU caches, adjacency lists).
The three common mistakes
- Losing the rest of the list: you change
nextbefore you save the old next node. - Forgetting
null:cur.next.valcrashes whencur.nextisnull. Check fornullbefore you use.valor.nexton a node. - Forgetting the head case: use a dummy node, or write separate code for the head.
Practice questions
Draw the list with arrows before you write code. Draw it again after each link change.
Write a singly linked list (each node has only a next link) with get, addAtHead, addAtTail, addAtIndex and deleteAtIndex.
Hint 1
head and size. Every other method can use the same walk to the node before the position.Hint 2
Hint 3
get and delete need 0 ≤ index < size. Add needs 0 ≤ index ≤ size.Show all 2 solutions with explanations
get: walkindexsteps from the head.addAtIndexanddeleteAtIndex: walk to the node before the position, then change the links.addAtHeadandaddAtTailare just special cases ofaddAtIndex.
class ListNode { constructor(val, next = null) { this.val = val; this.next = next; } }class MyLinkedList { constructor() { this.head = null; this.size = 0; } get(index) { if (index < 0 || index >= this.size) return -1; let cur = this.head; for (let i = 0; i < index; i++) cur = cur.next; return cur.val; } addAtHead(val) { this.addAtIndex(0, val); } addAtTail(val) { this.addAtIndex(this.size, val); } addAtIndex(index, val) { if (index < 0 || index > this.size) return; const dummy = new ListNode(0, this.head); let prev = dummy; for (let i = 0; i < index; i++) prev = prev.next; prev.next = new ListNode(val, prev.next); this.head = dummy.next; this.size++; } deleteAtIndex(index) { if (index < 0 || index >= this.size) return; const dummy = new ListNode(0, this.head); let prev = dummy; for (let i = 0; i < index; i++) prev = prev.next; prev.next = prev.next.next; this.head = dummy.next; this.size--; }}const L = new MyLinkedList();L.addAtHead(1); L.addAtTail(3); L.addAtIndex(1, 2);console.log(L.get(1)); // 2L.deleteAtIndex(1);console.log(L.get(1)); // 3console.log(L.get(5)); // -1In the worst case, every operation is O(n) because of the walk. Adding at the head can be O(1) if you write it as a special case.
A tail reference makes addAtTail O(1). The price is that you must keep it correct whenever the last node changes.
class ListNode { constructor(val, next = null) { this.val = val; this.next = next; } }class FastTailList { constructor() { this.head = null; this.tail = null; this.size = 0; } addAtHead(val) { this.head = new ListNode(val, this.head); if (this.tail === null) this.tail = this.head; // the first node is also the last node this.size++; } addAtTail(val) { const node = new ListNode(val); if (this.tail === null) this.head = this.tail = node; else { this.tail.next = node; this.tail = node; } // O(1): no walk needed this.size++; } toArray() { const out = []; for (let c = this.head; c; c = c.next) out.push(c.val); return out; }}const L = new FastTailList();L.addAtTail(2); L.addAtTail(3); L.addAtHead(1);console.log(L.toArray()); // [1, 2, 3]To delete the last node, you need the node before it so you can move tail back. So deleting at the tail is still O(n) in a singly linked list.
Use the first one, because that is what the question tests. Mention the tail pointer as a way to make it faster. (LeetCode 707.)
Remove every node of a linked list that has the value val. Return the new head.
Hint 1
Hint 2
Show all 3 solutions with explanations
Walk with prev, starting at a dummy node. If prev.next matches, delete it. If not, move forward.
function removeElements(head, val) { const dummy = new ListNode(0, head); let prev = dummy; while (prev.next !== null) { if (prev.next.val === val) prev.next = prev.next.next; else prev = prev.next; } return dummy.next;}console.log(toArray(removeElements(fromArray([1, 2, 6, 3, 4, 5, 6]), 6))); // [1, 2, 3, 4, 5]console.log(toArray(removeElements(fromArray([7, 7, 7, 7]), 7))); // []console.log(toArray(removeElements(null, 1))); // []O(n) time, O(1) space.
Move the head forward while it matches. Then handle the rest of the list with one pointer.
function removeElements(head, val) { while (head !== null && head.val === val) head = head.next; // the special case for the head let cur = head; while (cur !== null && cur.next !== null) { if (cur.next.val === val) cur.next = cur.next.next; else cur = cur.next; } return head;}console.log(toArray(removeElements(fromArray([1, 2, 6, 3, 4, 5, 6]), 6))); // [1, 2, 3, 4, 5]console.log(toArray(removeElements(fromArray([7, 7, 7, 7]), 7))); // []The cost is the same, but you must write the head case yourself. The dummy node saves you from writing this code.
First clean the rest of the list. Then decide if you keep this node.
function removeElements(head, val) { if (head === null) return null; head.next = removeElements(head.next, val); // the rest of the list, after cleaning return head.val === val ? head.next : head; // drop this node if it matches}console.log(toArray(removeElements(fromArray([1, 2, 6, 3, 4, 5, 6]), 6))); // [1, 2, 3, 4, 5]It looks neat, but it uses O(n) stack space. A very long list could cause a stack overflow (the call stack, the memory that keeps track of open function calls, runs out of room).
Use the dummy-head loop. (LeetCode 203.)
You get only one node of a singly linked list (not the head), and it is not the tail. Delete it from the list.
Hint 1
Hint 2
Show the solution with explanations
- Copy
node.next.valintonode.val. - Set
node.next = node.next.next.
function deleteNode(node) { node.val = node.next.val; node.next = node.next.next;}const head = fromArray([4, 5, 1, 9]);const five = head.next;deleteNode(five);console.log(toArray(head)); // [4, 1, 9]O(1) time and space. It does not delete the given node object. It copies the value of the next node and deletes that next node. The list looks exactly the same afterwards. It works only because the node is never the tail.
This is a trick question, not a real algorithm. (LeetCode 237.)
Insert a value into a linked list that is sorted from small to big. The list must stay sorted. Return the head.
Hint 1
prev is the node to insert after.Hint 2
Show the solution with explanations
Stop with prev on the last node that is smaller than val. Then connect the new node.
function insertSorted(head, val) { const dummy = new ListNode(0, head); let prev = dummy; while (prev.next !== null && prev.next.val < val) prev = prev.next; prev.next = new ListNode(val, prev.next); // connect first (the new node points at the old next node) return dummy.next;}console.log(toArray(insertSorted(fromArray([1, 3, 5]), 4))); // [1, 3, 4, 5]console.log(toArray(insertSorted(fromArray([2, 3]), 1))); // [1, 2, 3]console.log(toArray(insertSorted(null, 7))); // [7]O(n) time, O(1) space. This is the main step of insertion sort (sorting by putting each item in its place) on a list.
This is a warm-up for merging two sorted lists in Lesson 37.
You get a sorted linked list. Delete the repeated values, so that each value appears only once.
Hint 1
Hint 2
Show the solution with explanations
While cur.next exists, delete it if it is equal to cur.
function deleteDuplicates(head) { let cur = head; while (cur !== null && cur.next !== null) { if (cur.val === cur.next.val) cur.next = cur.next.next; else cur = cur.next; } return head;}console.log(toArray(deleteDuplicates(fromArray([1, 1, 2])))); // [1, 2]console.log(toArray(deleteDuplicates(fromArray([1, 1, 2, 3, 3])))); // [1, 2, 3]console.log(toArray(deleteDuplicates(fromArray([2, 2, 2])))); // [2]O(n) time, O(1) space. You do not need a dummy, because the head is never removed. The first node of a group of equal values stays.
(LeetCode 83.) The harder version (LeetCode 82) removes every value that appears more than once. That version needs a dummy head.
Put all the nodes at odd positions together, followed by the nodes at even positions. Keep the original order inside each group. Use only O(1) extra space.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
Read the values into two arrays, join the two arrays, and build a new list.
function oddEvenList(head) { const odd = [], even = []; let i = 1; for (let c = head; c !== null; c = c.next, i++) (i % 2 === 1 ? odd : even).push(c.val); return fromArray([...odd, ...even]);}console.log(toArray(oddEvenList(fromArray([1, 2, 3, 4, 5])))); // [1, 3, 5, 2, 4]O(n) time, but it uses O(n) extra space and makes new nodes. The question asks for O(1) space by changing the links.
- Set
odd = headandeven = head.next, and rememberevenHead. - While the even node has a next node: set
odd.next = even.next, moveoddforward, seteven.next = odd.next, then moveevenforward. - At the end, set
odd.next = evenHead.
function oddEvenList(head) { if (head === null || head.next === null) return head; let odd = head, even = head.next; const evenHead = even; // where the even chain starts while (even !== null && even.next !== null) { odd.next = even.next; // the odd node jumps over the even node odd = odd.next; even.next = odd.next; // the even node jumps over the odd node even = even.next; } odd.next = evenHead; // join the two chains return head;}console.log(toArray(oddEvenList(fromArray([1, 2, 3, 4, 5])))); // [1, 3, 5, 2, 4]console.log(toArray(oddEvenList(fromArray([2, 1, 3, 5, 6, 4, 7])))); // [2, 3, 6, 7, 1, 5, 4]console.log(toArray(oddEvenList(fromArray([1, 2])))); // [1, 2]O(n) time, O(1) space. The loop stops when even or even.next is null. This works for lists with an odd length and for lists with an even length.
Use the version that changes the links. It tests whether you can keep track of several pointers at once. (LeetCode 328.)
A linked list holds the bits of a binary number (a number written with only 0 and 1). The first bit is the biggest one. Return its normal decimal value.
Hint 1
Hint 2
Show all 2 solutions with explanations
Start at 0. For each node, do n = n * 2 + node.val.
function getDecimalValue(head) { let n = 0; for (let cur = head; cur !== null; cur = cur.next) n = n * 2 + cur.val; return n;}console.log(getDecimalValue(fromArray([1, 0, 1]))); // 5console.log(getDecimalValue(fromArray([0]))); // 0console.log(getDecimalValue(fromArray([1, 1, 0, 0]))); // 12O(n) time, O(1) space. Going from left to right does not need the length of the list. A linked list does not tell you its length for free.
This is the same idea, written with bit operations. Shift the bits left by one place, then add the new bit with OR.
function getDecimalValue(head) { let n = 0; for (let cur = head; cur !== null; cur = cur.next) n = (n << 1) | cur.val; return n;}console.log(getDecimalValue(fromArray([1, 0, 1, 1]))); // 11The cost is the same. n << 1 means the same as n * 2. (JavaScript bit operations work on 32-bit numbers, which is enough here because the list has at most 30 bits. Part 15 covers bit tricks.)
(LeetCode 1290.)
| Edit | Link changes | Watch out for |
|---|---|---|
| insert after p | new.next = p.next; p.next = new | do not swap these two lines |
| delete after p | p.next = p.next.next | p.next may be null |
| delete the head | head = head.next | use a dummy head to avoid this case |
| delete a node given only itself | copy next.val, skip next | fails on the tail |
| swap two adjacent nodes | three link changes | save a reference before you overwrite a link |
Make it stick
- Write the
ListNodeclass and the loop that visits every node. - Explain why, when you insert after a node, you must set the
nextof the new node before you change the old link. - Explain what problem the dummy head solves, and write the loop that deletes all matching nodes using it.
- Name two operations where a linked list is better than an array, and two where it is worse.
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
What's next
Lesson 36 uses these building blocks for three classic list techniques: reversing a list, finding the middle with a slow pointer and a fast pointer, and finding a cycle (a loop in the list).
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series