Learn/DSA Patterns
DSA PatternsLinked Listseasy10 min read

Merge Two Sorted Lists

Compare the heads of two sorted lists, pick the smaller one, advance that list's pointer — repeat until one list is exhausted, then attach the remainder. This simple O(n + m) merge is the heart of merge sort and the building block for merging K sorted lists with a heap.

#linked-list#merge#sorted#two-pointers#heap#merge-sort#beginner#interview
Table of contents

Before we start

This is one of the most fundamental operations in computer science — it is the "merge" in merge sort. Master it on linked lists and you will understand merge sort deeply enough to implement it on arrays, on lists, and eventually extend it to K lists using a heap. By the end you will be able to:

  • Write the two-list merge from memory with a dummy head node.
  • Trace the pointer dance step by step.
  • Extend the idea to Merge K Sorted Lists using a min-heap.

Picture this first (no code yet)

A real-life story

Two airport check-in queues, both already sorted by ticket number. A single agent is merging them into one line. She looks at the first person in each queue. Whoever has the lower ticket number goes next. She waves that person forward, and the next person in that queue steps up. She glances again — lower number goes next. She repeats until one queue empties entirely, then she waves the whole remaining queue through at once.

She never looks more than one person deep into each queue. She never re-examines anyone already waved through. Total work: exactly n + m comparisons for queues of sizes n and m.


The actual problem

Merge Two Sorted Lists (LC #21):

Given the heads of two sorted linked lists l1 and l2, merge them into one sorted list and return its head.

l1: 1 → 2 → 4
l2: 1 → 3 → 4
Output: 1 → 1 → 2 → 3 → 4 → 4

First, the slow way (so you feel the pain)

Collect all values from both lists into an array, sort the array, build a new linked list. O((n+m) log(n+m)) for the sort, O(n+m) extra space for the array. The comparison-based merge is O(n+m) time, O(1) extra space (just a few pointer variables) — no sorting, no extra array.


The turning point

Pause & think

You are at the point where l1 starts with 2 and l2 starts with 3. You pick 2 and advance l1 to 4. Now l1 starts with 4 and l2 starts with 3. You pick 3. Now l1 starts with 4 and l2 is empty. What do you do? Do you need to copy the remaining l1 nodes one by one?

Answer

No — you just attach the rest of l1 directly. Since both lists were sorted, every remaining node in l1 is ≥ everything already placed in the merged list. You point the tail of the merged list to l1 (or whichever list is non-empty). One pointer assignment finishes the rest of the merge — O(1) at the end, not O(n).


The one idea to remember

The entire pattern in one sentence

Use a dummy head node to eliminate edge-case handling; keep a tail pointer at the last merged node; compare l1.val and l2.val, link the smaller one to tail, advance that list and tail; when one list empties, attach the other entirely.


The code, line by line

def mergeTwoLists(l1, l2):
    dummy = ListNode(0)   # dummy eliminates "is the result list empty?" checks
    tail  = dummy         # tail always points to the last node of the merged list

    while l1 and l2:      # while both lists have remaining nodes
        if l1.val <= l2.val:
            tail.next = l1     # link smaller node
            l1 = l1.next       # advance l1
        else:
            tail.next = l2
            l2 = l2.next
        tail = tail.next       # advance tail to the newly linked node

    # at least one list is now empty — attach the other
    tail.next = l1 if l1 else l2

    return dummy.next          # skip the dummy node

Line by line:

  • dummy = ListNode(0) — a sentinel that lets tail.next = ... work even for the very first node, with no special "is result empty?" check.
  • tail = dummy — tail starts at dummy and advances with every node added.
  • while l1 and l2: — keep comparing while both lists are non-empty.
  • tail.next = l1 — we are not creating a new node; we are rewiring l1's existing node into the merged list.
  • tail = tail.next — must advance tail after each link, or tail.next would always overwrite the same connection.
  • tail.next = l1 if l1 else l2 — one line handles the remainder; l1 and l2 are already sorted, so the remaining nodes are all valid and in order.
  • return dummy.next — the dummy node itself isn't part of the answer.

Watch it happen, frame by frame

l1: 1 → 2 → 4, l2: 1 → 3 → 4

dummy → ?     tail = dummy

Iter 1: l1.val=1 == l2.val=1 → pick l1 (<=).
  tail.next = node1(l1). l1 → node2. tail → node1.
  merged so far: dummy → 1

Iter 2: l1.val=2, l2.val=1 → pick l2.
  tail.next = node1(l2). l2 → node3. tail → node1(l2).
  merged so far: dummy → 1 → 1

Iter 3: l1.val=2, l2.val=3 → pick l1.
  tail.next = node2(l1). l1 → node4(l1). tail → node2.
  merged so far: dummy → 1 → 1 → 2

Iter 4: l1.val=4, l2.val=3 → pick l2.
  tail.next = node3. l2 → node4(l2). tail → node3.
  merged so far: dummy → 1 → 1 → 2 → 3

Iter 5: l1.val=4, l2.val=4 → pick l1 (<=).
  tail.next = node4(l1). l1 → None. tail → node4(l1).
  merged so far: dummy → 1 → 1 → 2 → 3 → 4

Loop exits (l1 is None).
tail.next = l2 = node4(l2).

Final: 1 → 1 → 2 → 3 → 4 → 4  ✅

Recursive version (elegant, O(n+m) stack space)

def mergeTwoLists_recursive(l1, l2):
    if not l1: return l2
    if not l2: return l1
    if l1.val <= l2.val:
        l1.next = mergeTwoLists_recursive(l1.next, l2)
        return l1
    else:
        l2.next = mergeTwoLists_recursive(l1, l2.next)
        return l2

Beautiful, but uses O(n+m) stack space. Prefer the iterative version for long lists in production.


Extension: Merge K Sorted Lists (LC #23)

Given K sorted linked lists, merge all of them into one sorted list.

Naive: merge pairs repeatedly. K lists × n nodes each. Merge l1+l2 → n+n = 2n. Merge result + l3 → 3n. Total work: n + 2n + 3n + … + Kn = O(K² × n). For K=1000 and n=1000 that is 1 billion operations.

Better: min-heap. Push the head of each list into a min-heap. Always extract the minimum (O(log K)), link it to the result, and push the extracted node's next (if it exists) back into the heap.

import heapq

def mergeKLists(lists):
    dummy = ListNode(0)
    tail  = dummy
    heap  = []

    # Initialise heap with head of each non-empty list
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))   # (value, index, node)

    while heap:
        val, i, node = heapq.heappop(heap)    # extract minimum
        tail.next = node
        tail = tail.next
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))

    return dummy.next

Why (node.val, i, node)? Python's heap compares tuples element by element. If two nodes have the same value, it would try to compare the nodes themselves — which fails (ListNode has no < operator). The index i breaks ties without comparing nodes.

Complexity: O(N log K) where N = total nodes, K = number of lists. For K=1000 and N=10⁶, that is 10⁷ operations — 100× faster than the naive approach.


Where to spot this pattern

Trigger words:

  • "merge two sorted lists/arrays"
  • "merge k sorted lists"
  • "sort a linked list" — merge sort: split at middle, sort halves, merge
  • "find the median of a stream" — conceptually similar (Chapter 9.4)

Common traps

Watch out for these

  • Forgetting tail = tail.next inside the loop. If you link tail.next = l1 but don't advance tail, the next iteration overwrites the same connection — every node gets linked to dummy.next and only the last survives.
  • Not using a dummy head. Without a dummy, the first node addition requires an if result is None: result = node; tail = result branch — messy. The dummy node makes every addition uniform: tail.next = node; tail = tail.next.
  • Using tail.next = l1 or l2 instead of tail.next = l1 if l1 else l2. In Python, l1 or l2 works correctly for None-checks, but it's semantically fragile — if l1 is a node with val=0 (falsy in some languages), or fails. Use the explicit ternary or if/else.
  • In Merge K Lists: comparing nodes directly in the heap. Without a tiebreaker index, heapq crashes when two node values are equal and tries to compare ListNode objects (which don't support <). Always include an index in the heap tuple.

Remember this forever

Merge Two Sorted Lists

dummy = ListNode(0);  tail = dummy

while l1 and l2:
    if l1.val <= l2.val:
        tail.next = l1;  l1 = l1.next
    else:
        tail.next = l2;  l2 = l2.next
    tail = tail.next        # ← must advance tail!

tail.next = l1 if l1 else l2
return dummy.next

Merge K Lists: min-heap with (val, index, node) — extract min, push next. O(N log K).

Trap: advance tail every iteration. Use index tiebreaker in K-list heap.


Check yourself

Why does the dummy head node make the code simpler? What would the code look like without it?

Without a dummy, you need to initialise the result head separately:

result = None
if l1.val <= l2.val:
    result = l1; l1 = l1.next
else:
    result = l2; l2 = l2.next
tail = result

Then the loop body is the same. This is 4 extra lines and an edge case (if not l1 and not l2: return None). The dummy node absorbs all of this — tail.next = node works identically for the first node and every subsequent node. One idiom, zero special cases.

For Merge K Sorted Lists with a heap, why is the complexity O(N log K) and not O(N log N)?

The heap never holds more than K elements at a time — one per list. Every heap push/pop operation costs O(log K), not O(log N). We perform exactly N pushes and N pops (one push and one pop per node across all lists). Total: O(N log K). Since K ≤ N always, this is at least as good as O(N log N) and often much better (K = 100, N = 100 000 → 100 000 × 7 ≈ 700 000 vs 100 000 × 17 ≈ 1 700 000).


Practice problems

ProblemDifficultyWhat to noticeLink
Merge Two Sorted ListsEasyDummy node; advance tail every iterationLC #21
Merge K Sorted ListsHardMin-heap with (val, idx, node) tiebreakerLC #23
Sort ListMediumSplit at middle (Chapter 4.2) + merge recursivelyLC #148
Merge Sorted ArrayEasySame merge idea but on arrays in-place from the backLC #88

Next up: Remove Nth Node from End — the N-gap two-pointer trick that finds the node to delete in a single pass without knowing the list length.