LeetCode #21 — Easy

Merge Two
Sorted Lists

A complete visual walkthrough of the iterative merge approach — from brute force to optimal, with linked list animations.

Solve on LeetCode
The Problem

Problem Statement

You are given the heads of two sorted linked lists list1 and list2.

Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists.

Return the head of the merged linked list.

Example 1:

Input: list1 = [1,2,4], list2 = [1,3,4]
Output: [1,1,2,3,4,4]

Example 2:

Input: list1 = [], list2 = []
Output: []

Example 3:

Input: list1 = [], list2 = [0]
Output: [0]

Constraints:

  • The number of nodes in both lists is in the range [0, 50].
  • -100 <= Node.val <= 100
  • Both list1 and list2 are sorted in non-decreasing order.
Patterns Used

Roadmap

  1. Brute Force Approach
  2. The Core Insight — Two-Pointer Merge
  3. Algorithm Walkthrough
  4. Edge Cases
  5. Full Annotated Code
  6. Interactive Demo
  7. Complexity Analysis
Step 01

Brute Force Approach

The simplest idea: collect all node values into an array, sort it, then build a new linked list from the sorted array.

Collect, Sort, Rebuild

list1
1
2
4
null
list2
1
3
4
null
collect → [1, 2, 4, 1, 3, 4] → sort → [1, 1, 2, 3, 4, 4]
1
1
2
3
4
4
null

This runs in O((n+m) log(n+m)) time because of the sort. But both lists are already sorted — we're throwing away that information and doing unnecessary work.

💡

The wasted information: Both lists are already in order. If we just compare the heads of each list, we always know which element comes next — no sorting required. This is exactly the merge step of merge sort.

Step 02

The Core Insight — Two-Pointer Merge

Since both lists are sorted, we can build the result in one pass. Use a dummy head node and a current pointer. At each step, compare the heads of both lists and attach the smaller one to the result.

The Merge Strategy

Think of two queues of people sorted by height. To merge them into one sorted line, you repeatedly compare the person at the front of each queue and move the shorter one into the result line.

list1.val ≤ list2.val
Pick from list1, advance list1 pointer
list2.val < list1.val
Pick from list2, advance list2 pointer

When one list is exhausted, just attach the remainder of the other list — it's already sorted!

Why a dummy head? Without it, we'd need special-case logic to initialize the head of the result list. The dummy node lets us always do curr.next = chosen without checking if the result is empty. We return dummy.next at the end to skip it.

Step 03

Algorithm Walkthrough

Let's trace through the merge of list1 = [1,2,4] and list2 = [1,3,4] step by step.

ITERATION 1
1
vs
1
1 ≤ 1, pick from list1
result
D
1
ITERATION 2
2
vs
1
1 < 2, pick from list2
result
D
1
1
ITERATION 3
2
vs
3
2 ≤ 3, pick from list1
result
D
1
1
2
ITERATION 4
4
vs
3
3 < 4, pick from list2
result
D
1
1
2
3
ITERATION 5
4
vs
4
4 ≤ 4, pick from list1
result
D
1
1
2
3
4
ATTACH REMAINDER

list1 is exhausted (null). Attach all remaining nodes of list2.

1
1
2
3
4
4
null
Step 04

Edge Cases

The algorithm handles these naturally due to the dummy head and the final remainder attachment.

Both lists empty
list1 = null, list2 = null → The while loop never executes. curr.next remains null. We return dummy.next which is null.
One list empty
list1 = [1,2,3], list2 = null → The while loop never executes. The final line curr.next = list1 attaches the entire non-empty list.
Different lengths
list1 = [1], list2 = [2,5,8,10] → After one comparison picks 1 from list1, list1 becomes null. The remainder of list2 is attached directly.
One list entirely smaller
list1 = [1,2,3], list2 = [7,8,9] → All three comparisons pick from list1. Then list1 exhausts and the entire list2 is appended. Result: [1,2,3,7,8,9].
🎯

No special cases needed. The dummy head pattern and the single line curr.next = (list1 != null) ? list1 : list2 handle every edge case without any if branching for empty inputs. This is why the dummy head technique is so valuable.

Step 05

Full Annotated Code

Iterative Solution (Recommended)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int val) { this.val = val; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {

        // Dummy head avoids special-case logic for empty result
        ListNode dummy = new ListNode(0);
        ListNode curr = dummy;            // tail of result list

        // Compare heads while both lists have nodes
        while (list1 != null && list2 != null) {
            if (list1.val <= list2.val) {
                curr.next = list1;       // attach list1's head
                list1 = list1.next;      // advance list1
            } else {
                curr.next = list2;       // attach list2's head
                list2 = list2.next;      // advance list2
            }
            curr = curr.next;            // advance result tail
        }

        // Attach whichever list still has remaining nodes
        curr.next = (list1 != null) ? list1 : list2;

        return dummy.next;               // skip dummy, return real head
    }
}

Recursive Alternative

The recursive approach is elegant but uses O(n+m) stack space. For very long lists, this could cause a stack overflow. The iterative version above is preferred in interviews.

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;       // base case: l1 exhausted
        if (l2 == null) return l1;       // base case: l2 exhausted

        if (l1.val <= l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;                   // l1 becomes the head
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;                   // l2 becomes the head
        }
    }
}
Step 06

Interactive Demo

Enter two sorted lists and step through the merge. Watch nodes move from the input lists into the result.

⚙ Merge Visualizer



Step 07

Complexity Analysis

Time
O(m + n)
Space
O(m + n)

Each node is visited exactly once. The while loop runs at most n + m iterations (where n and m are the lengths of the two lists), and each iteration does O(1) work: one comparison and one pointer reassignment. Space is O(1) for the iterative approach — we reuse existing nodes by rewiring their next pointers.

Approach Breakdown

BRUTE FORCE
O((n+m) log(n+m)) time
O(n+m) space

Collect all node values from both lists into an array of size n+m, then sort it. The sort dominates at O((n+m) log(n+m)). Building the output list from the sorted array requires O(n+m) additional space for the array and new nodes.

ITERATIVE MERGE
O(n+m) time
O(1) space

A single while loop compares the heads of both lists, picking the smaller node each iteration. Each node is visited exactly once, giving O(n+m) time. Existing nodes are rewired in place with only a dummy head and a tail pointer, so auxiliary space is O(1).

RECURSIVE MERGE
O(n+m) time
O(n+m) space

Each recursive call processes one node by picking the smaller head and recursing on the rest. There are n+m calls total, each doing O(1) work. However, the call stack grows to depth n+m in the worst case, using O(n+m) space for stack frames.

🎯

The dummy head node eliminates all edge cases. Without it, you need special logic to initialize the result's first node. With it, curr.next = chosen always works, even when the result is empty. Merging is inherently linear — you must inspect every node at least once — so O(n + m) is optimal.

Coach Notes

Common Mistakes

Review these before coding to avoid predictable interview regressions.

Losing head/tail while rewiring

Wrong move: Pointer updates overwrite references before they are saved.

Usually fails on: List becomes disconnected mid-operation.

Fix: Store next pointers first and use a dummy head for safer joins.