Part 9 · Lesson 37 · read
Merging and Cycle Problems
Join sorted lists, find where a loop starts, and remove the nth node from the end.
On this page
Merging two sorted lists
A linked list is a chain of small objects called nodes. Each node holds a value and a link (next) to the following node. A pointer here just means a variable that holds one node, so it "points" at a place in the chain. Merging means joining two sorted lists into one sorted list. You already merged two sorted arrays in the sorting part. With linked lists the idea is the same, but it is even neater. You do not copy anything. You just change the links of the existing nodes to put them in the right order. A dummy head and a tail pointer keep the code short. The dummy is a fake first node that you throw away at the end. It means attaching the first real node is no different from attaching any other.
function mergeTwoLists(a, b) { const dummy = new ListNode(0); let tail = dummy; // tail is where the next node will be attached while (a !== null && b !== null) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a !== null ? a : b; // one list is used up: attach what is left of the other return dummy.next;}console.log(toArray(mergeTwoLists(fromArray([1, 3, 5]), fromArray([2, 4])))); // [1, 2, 3, 4, 5]console.log(toArray(mergeTwoLists(fromArray([]), fromArray([0])))); // [0]Traced: merging 1 → 3 → 5 with 2 → 4
The dummy node is a fake first node. It means attaching the very first real node is no different from attaching any other. tail marks where to attach the next node.
▶const dummy = new ListNode(0);2let tail = dummy;3while (a !== null && b !== null) {4 if (a.val <= b.val) { tail.next = a; a = a.next; }5 else { tail.next = b; b = b.next; }6 tail = tail.next;7}8tail.next = a !== null ? a : b;9return dummy.next;| a | [1, 3, 5] |
| b | [2, 4] |
| merged | [] |
Time is O(m + n) (m and n are the two list lengths, and every node is attached once). Extra space is O(1). Using <= instead of < keeps the merge stable. Stable means that equal values from the first list stay ahead of equal values from the second list.
Where a cycle starts
A cycle is a loop in a linked list: some node's next points back to an earlier node, so the chain never ends. Floyd's algorithm (also called tortoise and hare) is a method that finds a cycle with two pointers. The slow one moves 1 node per step and the fast one moves 2 nodes per step. If there is a cycle, they must meet inside it.
Lesson 36 checked whether there is a cycle. To find where it begins, add a second phase. When slow and fast first meet, leave slow where it is and put a new pointer at the head. Move both one step at a time. They meet at the first node of the cycle.
function detectCycle(head) { let slow = head, fast = head; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; if (slow === fast) { // phase 1: they met inside the loop let p = head; // phase 2: restart one pointer from the head while (p !== slow) { p = p.next; slow = slow.next; } return p; // both now stand on the first node of the loop } } return null; // no cycle}const list = fromArray([3, 2, 0, -4, 7]);list.next.next.next.next.next = list.next; // the tail points back at the node holding 2console.log(detectCycle(list).val); // 2console.log(detectCycle(fromArray([1, 2]))); // nullWhy it works. Let the straight part before the loop have length a. Let the meeting point be b steps into the loop, and let the loop have length c. Slow has walked a + b steps. Fast has walked twice as many. Fast is at the same node as slow, so it must have walked some whole number of extra laps (call it k). That gives 2(a + b) = a + b + k·c, so a + b = k·c, which means a = k·c − b. Now start at the meeting point and walk a more steps. You end at the loop start (k laps, minus the b steps already covered). The pointer from the head also walks asteps and lands at the loop start. So they arrive together.
Remove the n-th node from the end
You do not know the length, and you want to delete a node counted from the back. Use a gap. First, send fast n nodes ahead. Then move both pointers together. The gap between them never changes. So when fast is on the last node, slow is exactly one node before the node to delete. Start both at a dummy head. Then removing the real head works too.
function removeNthFromEnd(head, n) { const dummy = new ListNode(0, head); let fast = dummy, slow = dummy; for (let i = 0; i < n; i++) fast = fast.next; // open a gap of n nodes while (fast.next !== null) { // move both until fast is on the LAST node fast = fast.next; slow = slow.next; } slow.next = slow.next.next; // slow is just before the node to delete return dummy.next;}console.log(toArray(removeNthFromEnd(fromArray([1, 2, 3, 4, 5]), 2))); // [1, 2, 3, 5]console.log(toArray(removeNthFromEnd(fromArray([1]), 1))); // []| Moment | slow | fast |
|---|---|---|
| start | dummy | dummy |
| after moving fast 2 steps | dummy | 2 |
| fast.next exists: move both | 1 | 3 |
| again | 2 | 4 |
| again | 3 | 5 |
| fast.next is null: stop. slow.next (4) is deleted | 3 | 5 |
The result is 1 → 2 → 3 → 5. Fast sets a fixed distance. Slow always stays that far behind fast.
Where two lists meet
Two lists can share their tails. This means the same node objects, not just equal values. Here is the trick. Walk pointer p along list A and then along list B. Walk q along list B and then along list A. Both walk the same total distance, len(A) + len(B). So they arrive at the shared node at the same moment. If the lists never share a node, both pointers reach null together, and the loop ends with null.
function getIntersectionNode(a, b) { let p = a, q = b; while (p !== q) { p = p === null ? b : p.next; // after finishing one list, continue on the other q = q === null ? a : q.next; } return p; // the shared node, or null if they never meet}// 1 -> 2 -> 8 -> 9 and 5 -> 8 -> 9 (8 is shared)const shared = fromArray([8, 9]);const x = new ListNode(1, new ListNode(2, shared));const y = new ListNode(5, shared);console.log(getIntersectionNode(x, y).val); // 8console.log(getIntersectionNode(fromArray([1]), fromArray([2]))); // null===. If you compare val, you would wrongly match two separate lists that just happen to contain equal numbers.Adding two numbers held in lists
Each list holds one digit per node, with the smallest place first (the ones digit comes first). So adding works like the addition you learned at school. You go from left to right and keep a carry (the extra 1 you carry to the next digit). Keep going while either list has digits or there is a carry left. If you stop too early, 99 + 1 would lose its final 1.
// Digits are stored in reverse order: 342 is 2 -> 4 -> 3.function addTwoNumbers(a, b) { const dummy = new ListNode(0); let tail = dummy, carry = 0; while (a !== null || b !== null || carry > 0) { const sum = (a ? a.val : 0) + (b ? b.val : 0) + carry; tail.next = new ListNode(sum % 10); carry = Math.floor(sum / 10); tail = tail.next; if (a) a = a.next; if (b) b = b.next; } return dummy.next;}console.log(toArray(addTwoNumbers(fromArray([2, 4, 3]), fromArray([5, 6, 4])))); // [7, 0, 8] (342 + 465 = 807)console.log(toArray(addTwoNumbers(fromArray([9, 9]), fromArray([1])))); // [0, 0, 1] (99 + 1 = 100)Sorting a list with merge sort
Merge sort is a sorting algorithm that splits the list into two halves, sorts each half (by calling itself), and then merges the two sorted halves. A list of 0 or 1 nodes is already sorted, so that is where the splitting stops. Merge sort works well on linked lists. Finding the middle uses the slow and fast trick. Splitting takes one pointer change. Merging needs no extra array. It runs in O(n log n) time. Binary search is a poor fit for lists, because it needs random access, which means jumping straight to any position. A linked list cannot do that. Array-style quick sort, which swaps items by position, has the same problem.
function mergeTwoLists(a, b) { const dummy = new ListNode(0); let tail = dummy; while (a !== null && b !== null) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a !== null ? a : b; return dummy.next;}function sortList(head) { if (head === null || head.next === null) return head; // 0 or 1 node: sorted // find the end of the first half (slow stops BEFORE the middle) let slow = head, fast = head.next; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; } const second = slow.next; slow.next = null; // cut the list in two return mergeTwoLists(sortList(head), sortList(second));}console.log(toArray(sortList(fromArray([4, 2, 1, 3])))); // [1, 2, 3, 4]console.log(toArray(sortList(fromArray([-1, 5, 3, 4, 0])))); // [-1, 0, 3, 4, 5][1, 2]. If both pointers start at head, slow ends on the second node. Then the first half would be both nodes and the second half would be empty. The recursion would never get smaller. If fast starts one step ahead, slow stops on the last node of the first half. Then both halves are always smaller.Practice questions
All of these use the same tools: a dummy head, a trailing pointer, slow and fast pointers, and careful changes to the links.
Join two sorted linked lists into one sorted list by changing the links of their nodes. Return its head.
Hint 1
Hint 2
Show all 2 solutions with explanations
Keep a tail. Attach the smaller front node, move that list forward, and move tail forward. At the end, attach whatever is left.
function mergeTwoLists(a, b) { const dummy = new ListNode(0); let tail = dummy; while (a !== null && b !== null) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a !== null ? a : b; return dummy.next;}console.log(toArray(mergeTwoLists(fromArray([1, 2, 4]), fromArray([1, 3, 4])))); // [1, 1, 2, 3, 4, 4]console.log(toArray(mergeTwoLists(fromArray([]), fromArray([0])))); // [0]O(m + n) time, O(1) space.
The smaller head stays at the front. Its next becomes the merge of the rest.
function mergeTwoLists(a, b) { if (a === null) return b; if (b === null) return a; if (a.val <= b.val) { a.next = mergeTwoLists(a.next, b); return a; } b.next = mergeTwoLists(a, b.next); return b;}console.log(toArray(mergeTwoLists(fromArray([1, 2, 4]), fromArray([1, 3, 4])))); // [1, 1, 2, 3, 4, 4]The code is very short, but it uses O(m + n) stack space (memory for unfinished function calls).
Use the loop version for real-world code. The recursive version is a good second answer. (LeetCode 21.)
Return the node where the cycle begins, or null if there is no cycle.
Hint 1
Hint 2
Show all 2 solutions with explanations
A Set is a collection that keeps each item once and can check quickly whether an item is inside. Walk the list and add each node to the Set. The first node that you reach a second time is the start of the cycle.
function detectCycle(head) { const seen = new Set(); for (let c = head; c !== null; c = c.next) { if (seen.has(c)) return c; seen.add(c); } return null;}const a = fromArray([3, 2, 0, -4]);a.next.next.next.next = a.next;console.log(detectCycle(a).val); // 2console.log(detectCycle(fromArray([1]))); // nullO(n) time and O(n) space.
- Move slow and fast until they meet. If they never meet, there is no cycle.
- Start a pointer at the head. Move it and slow one step at a time until they are at the same node.
function detectCycle(head) { let slow = head, fast = head; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; if (slow === fast) { let p = head; while (p !== slow) { p = p.next; slow = slow.next; } return p; } } return null;}const a = fromArray([3, 2, 0, -4]);a.next.next.next.next = a.next;console.log(detectCycle(a).val); // 2O(n) time, O(1) space. The distance from the head to the loop start is the same as the distance from the meeting point to the loop start, if you ignore whole laps around the loop. (The proof is in the lesson.)
Start with the Set. Switch to Floyd's version when the interviewer asks for O(1) space. (LeetCode 142.)
Remove the n-th node counted from the end of the list and return the head. Assume n is valid.
Hint 1
Hint 2
Show all 2 solutions with explanations
Count the length L. Then walk L − n steps from the dummy to the node just before the one to delete.
function removeNthFromEnd(head, n) { let len = 0; for (let c = head; c !== null; c = c.next) len++; const dummy = new ListNode(0, head); let prev = dummy; for (let i = 0; i < len - n; i++) prev = prev.next; prev.next = prev.next.next; return dummy.next;}console.log(toArray(removeNthFromEnd(fromArray([1, 2, 3, 4, 5]), 2))); // [1, 2, 3, 5]console.log(toArray(removeNthFromEnd(fromArray([1, 2]), 2))); // [2]O(n) time, O(1) space, but you walk the list twice.
Move fast n steps ahead. Then move both pointers until fast is on the last node.
function removeNthFromEnd(head, n) { const dummy = new ListNode(0, head); let fast = dummy, slow = dummy; for (let i = 0; i < n; i++) fast = fast.next; while (fast.next !== null) { fast = fast.next; slow = slow.next; } slow.next = slow.next.next; return dummy.next;}console.log(toArray(removeNthFromEnd(fromArray([1, 2, 3, 4, 5]), 2))); // [1, 2, 3, 5]console.log(toArray(removeNthFromEnd(fromArray([1]), 1))); // []Same cost, but you walk the list only once. The dummy makes deleting the head work like any other case.
Both are fine. The one-pass version is the answer interviewers expect. (LeetCode 19.)
Return the node where two singly linked lists start to share nodes. If they share none, return null.
Hint 1
Hint 2
Show all 2 solutions with explanations
A Set is a collection that keeps each item once and can check quickly whether an item is inside. Put every node of A in a Set. Then walk B and return the first node that is in the Set.
function getIntersectionNode(a, b) { const seen = new Set(); for (let c = a; c !== null; c = c.next) seen.add(c); for (let c = b; c !== null; c = c.next) if (seen.has(c)) return c; return null;}const shared = fromArray([8, 9]);const x = new ListNode(1, new ListNode(2, shared));const y = new ListNode(5, shared);console.log(getIntersectionNode(x, y).val); // 8O(m + n) time, O(m) space.
Walk p along A and then B. Walk q along B and then A. Both walk m + n nodes, so they reach the shared node together (or both reach null).
function getIntersectionNode(a, b) { let p = a, q = b; while (p !== q) { p = p === null ? b : p.next; q = q === null ? a : q.next; } return p;}const shared = fromArray([8, 9]);const x = new ListNode(1, new ListNode(2, shared));const y = new ListNode(5, shared);console.log(getIntersectionNode(x, y).val); // 8console.log(getIntersectionNode(fromArray([1]), fromArray([2]))); // nullO(m + n) time, O(1) space. The difference in length cancels out. Each pointer walks the other list's extra length on its second trip.
The switching trick is the best answer. The Set version is a safe first answer. (LeetCode 160.)
Each list is a whole number that is not negative. It has one digit per node, with the ones digit first. Return their sum as a list in the same form.
Hint 1
Hint 2
Show all 2 solutions with explanations
At each position, add both digits (use 0 if a list has no digit left) and the carry. Store sum % 10 (the last digit of the sum). The new carry is floor(sum / 10) (the sum divided by 10, rounded down).
function addTwoNumbers(a, b) { const dummy = new ListNode(0); let tail = dummy, carry = 0; while (a !== null || b !== null || carry > 0) { const sum = (a ? a.val : 0) + (b ? b.val : 0) + carry; tail.next = new ListNode(sum % 10); carry = Math.floor(sum / 10); tail = tail.next; if (a) a = a.next; if (b) b = b.next; } return dummy.next;}console.log(toArray(addTwoNumbers(fromArray([2, 4, 3]), fromArray([5, 6, 4])))); // [7, 0, 8]console.log(toArray(addTwoNumbers(fromArray([9, 9]), fromArray([1])))); // [0, 0, 1]O(max(m, n)) time (the length of the longer list). It never turns the list into a number. So it works for lists much longer than the 15 or so digits that a JavaScript number can hold exactly.
Read each list into a BigInt (a JavaScript type for very large whole numbers). Add them, then split the result back into a list. It is easy to write, but it skips the point of the question.
function toBig(head) { let digits = ""; for (let c = head; c !== null; c = c.next) digits = c.val + digits; return BigInt(digits);}function addTwoNumbers(a, b) { const total = (toBig(a) + toBig(b)).toString(); const dummy = new ListNode(0); let tail = dummy; for (let i = total.length - 1; i >= 0; i--) { tail.next = new ListNode(Number(total[i])); tail = tail.next; } return dummy.next;}console.log(toArray(addTwoNumbers(fromArray([2, 4, 3]), fromArray([5, 6, 4])))); // [7, 0, 8]It gives the right answer, but interviewers expect the carry loop, and this version builds large strings. Use it only to double-check your answer.
Use the carry loop. (LeetCode 2.)
Sort a linked list from smallest to largest in O(n log n) time.
Hint 1
Hint 2
Show all 2 solutions with explanations
Read the values, sort them with sort((x, y) => x - y), and write them back into the nodes.
function sortList(head) { const vals = []; for (let c = head; c !== null; c = c.next) vals.push(c.val); vals.sort((x, y) => x - y); let i = 0; for (let c = head; c !== null; c = c.next) c.val = vals[i++]; return head;}console.log(toArray(sortList(fromArray([4, 2, 1, 3])))); // [1, 2, 3, 4]O(n log n) time, O(n) space. It is fine in real code, but it does not show that you can work with lists.
Split the list in the middle. Sort each half with recursion (the function calls itself). Then merge the two sorted halves.
function merge(a, b) { const dummy = new ListNode(0); let tail = dummy; while (a !== null && b !== null) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a !== null ? a : b; return dummy.next;}function sortList(head) { if (head === null || head.next === null) return head; let slow = head, fast = head.next; while (fast !== null && fast.next !== null) { slow = slow.next; fast = fast.next.next; } const second = slow.next; slow.next = null; return merge(sortList(head), sortList(second));}console.log(toArray(sortList(fromArray([-1, 5, 3, 4, 0])))); // [-1, 0, 3, 4, 5]O(n log n) time. The recursion goes O(log n) levels deep. A bottom-up version (it merges groups of size 1, 2, 4, and so on, with no recursion) needs only O(1) extra space.
Use merge sort. (LeetCode 148.)
Reorder L0 → L1 → … → Ln into L0 → Ln → L1 → Ln−1 → … by changing the links of the nodes in place (no new nodes).
Hint 1
Show all 2 solutions with explanations
Store the nodes in an array. Then link them again, working from both ends towards the middle.
function reorderList(head) { const nodes = []; for (let c = head; c !== null; c = c.next) nodes.push(c); let i = 0, j = nodes.length - 1; while (i < j) { nodes[i].next = nodes[j]; i++; if (i === j) break; nodes[j].next = nodes[i]; j--; } nodes[i].next = null;}const h = fromArray([1, 2, 3, 4, 5]);reorderList(h);console.log(toArray(h)); // [1, 5, 2, 4, 3]O(n) time, O(n) space. The last line is the important one. Without it, the final node still points at its old neighbour, and the list becomes a cycle.
- Find the end of the first half and cut the list at that point.
- Reverse the second half.
- Take one node from each half in turn (interleave them).
function reorderList(head) { if (head === null || head.next === null) return; let slow = head, fast = head; while (fast.next !== null && fast.next.next !== null) { slow = slow.next; fast = fast.next.next; } let prev = null, cur = slow.next; slow.next = null; while (cur !== null) { const next = cur.next; cur.next = prev; prev = cur; cur = next; } let a = head, b = prev; while (b !== null) { const an = a.next, bn = b.next; a.next = b; b.next = an; a = an; b = bn; }}const h = fromArray([1, 2, 3, 4]);reorderList(h);console.log(toArray(h)); // [1, 4, 2, 3]const g = fromArray([1, 2, 3, 4, 5]);reorderList(g);console.log(toArray(g)); // [1, 5, 2, 4, 3]O(n) time, O(1) space. The first half is never shorter than the second. So when the length is odd, the list ends with its middle node after interleaving.
The array version is a good first answer. The three-step version is the follow-up. (LeetCode 143.)
Make it stick
- Write the merge loop with a dummy head. Explain the last line (
tail.next = a || b). - Describe the two phases for finding where a cycle starts. Say what the second phase does.
- Explain how a gap of n nodes finds the n-th node from the end in one pass.
- Explain why the intersection trick makes both pointers walk the same distance.
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 38 leaves lists and brings a new structure: the stack. A stack is last in, first out, like a pile of plates. You use it to match brackets, to build a stack that knows its minimum, and to work out expressions.
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