DSA sheet · Linked List · Merge & sort pattern (last problem)

Merge K Sorted Lists

This is the last problem of the merge & sort pattern, and it's a famous hard interview question. It builds directly on Merge Two Sorted Lists: instead of two sorted lists, we get k of them and must merge them all into one sorted list. The teacher spreads four approaches over two videos. Video 1: (A) dump every value in an array and sort it, (B) merge the lists one by one, (C) a min-heap (priority queue) that always hands us the smallest front node. Video 2: (D) divide and conquer, exactly like merge sort, but the "elements" are whole lists. The key lesson: C and D both bring the cost down to N log k, by never comparing more than they need to.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a linked list node?

A linked list is a chain of nodes. Each node has a value (val) and a link to the next node (next). The first node is the head. The last node points to None. In this problem we receive a Python list lists of k heads, and each head may be None (an empty list).

given by LeetCode, don't write this in the solution
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
lists[0]:  [1] → [4] → [5] → None
lists[1]:  [1] → [3] → [4] → None
lists[2]:  [2] → [6] → None

Walking, and no index access

We move along a list with curr = curr.next until curr is None. There is no list[i]. Reaching position i costs i steps (O(n)), so we only ever read the front of each list and move forward.

Save next before you change a pointer

When we take a node out of its list and attach it to our answer, we write tail.next = node. The node's own next is still the road to the rest of its old list. So we move that list's pointer forward (l1 = l1.next), or push node.next into the heap, before that link is ever overwritten. If we lose it, the rest of that list is gone.

Dummy node + tail pointer (merging with a tail)

The answer list is built one node at a time. A fake dummy = ListNode(0) sits in front, and tail always stands on the last node. To add: tail.next = node, then tail = tail.next. The answer is dummy.next. The dummy means the "first node" never needs a special case.

Merge Two Sorted Lists (the prerequisite)

The teacher quickly recaps it with 1 → 4 → 5 and 1 → 3 → 4. Put l1 and l2 on the two heads. Each time, attach the smaller front node to the tail and move that list forward (on a tie, take l1). When one list runs out, the other one is already sorted, so we don't walk through it: just hang the whole remaining part on the tail with one link.

merge two sorted lists (relinks the existing nodes)
def merge_two(l1, l2):
    dummy = ListNode(0)
    tail = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            tail.next = l1        # take l1's front node
            l1 = l1.next          # move l1 BEFORE that link is ever changed
        else:
            tail.next = l2
            l2 = l2.next
        tail = tail.next
    tail.next = l1 if l1 else l2  # the leftover part is already sorted
    return dummy.next
l1: [1] → [4] → [5]         l2: [1] → [3] → [4]
take 1(l1), 1(l2), 3, 4(l1), 4(l2) → l2 is empty → hang [5] on the tail
result: [1] → [1] → [3] → [4] → [4] → [5] → None

Cost: one step per node, so O(len1 + len2).

A heap (priority queue)

A min-heap is a box that always gives you its smallest item first. Adding an item (push) and removing the smallest (pop) each cost O(log size), much cheaper than scanning everything. Inside, it is stored as a tree-shaped array where every parent is ≤ its children, so the smallest is always at the top. In Python we use heapq on a normal list: heapq.heappush(h, x), heapq.heappop(h). Python's heapq is already a min-heap.

heap array [1, 1, 2, 2]  as a tree:
        1
       / \
      1   2          every parent ≤ its children
     /                → the top (1) is the minimum
    2

Merge sort in one picture (for Part D)

The teacher recalls merge sort with [2, 5, 4, 3]: keep splitting in half until each piece has one element (a single element is always sorted), then merge the pieces back up in pairs.

          [2 5 4 3]
          /        \
      [2 5]        [4 3]          split
      /   \        /   \
    [2]   [5]    [4]   [3]        size 1 → sorted
      \   /        \   /
      [2 5]        [3 4]          merge pairs
          \        /
          [2 3 4 5]               merge again

Part A · Brute force: collect, sort, rebuild

LeetCode 23 · Merge k Sorted Lists

1The question in simple words

You get an array of k linked lists. Each list is already sorted in increasing order. Merge all of them into one sorted linked list and return its head.

input:  [1 → 4 → 5,  1 → 3 → 4,  2 → 6]
output:  1 → 1 → 2 → 3 → 4 → 4 → 5 → 6

input:  []        → output: None   (no lists at all)
input:  [None]    → output: None   (one empty list)

2What the constraints tell us

Notation for this page: k = number of lists, n = length of one list, N = total nodes (about k·n). The teacher writes "kn" for the total.

3Intuition

Forget that each list is sorted. Pour every value into one bucket, sort the bucket, and build a new list from it. This works even if you've never seen the merge algorithm.

4Building the logic

Doubt: what if lists is empty or holds only empty lists?
→ The loops add nothing, nums stays empty, the build loop doesn't run, and dummy.next is None. Correct with no extra code.

5Approach steps

  1. Walk every list and collect all values into nums.
  2. Sort nums.
  3. Build a new list from nums with a dummy and a tail. Return dummy.next.

6Code (Python)

Brute force: collect, sort, rebuild
class Solution:
    def mergeKLists(self, lists):
        nums = []
        for head in lists:               # k lists
            while head:                  # n nodes each
                nums.append(head.val)
                head = head.next
        nums.sort()                      # N log N

        dummy = ListNode(0)
        curr = dummy
        for v in nums:                   # build a fresh list
            curr.next = ListNode(v)
            curr = curr.next
        return dummy.next

7Code line by line

linewhat it means
for head in lists:Visit each of the k lists. head is a local copy, so moving it doesn't change lists.
while head: nums.append(head.val) head = head.nextCopy every value of this list. An empty list (None) is just skipped.
nums.sort()All values in order. This ignores the fact that each list was already sorted.
curr.next = ListNode(v)Make a new node for each value and attach it at the tail.
return dummy.nextThe real head (None if there were no values).

8Dry run

stagestate
after list 0nums = [1, 4, 5]
after list 1nums = [1, 4, 5, 1, 3, 4]
after list 2nums = [1, 4, 5, 1, 3, 4, 2, 6]
sortnums = [1, 1, 2, 3, 4, 4, 5, 6]
build0 → 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6 → return dummy.next

9Complexity & remember

RememberWorks, but wastes the "already sorted" information: we pay N log N for a sort we didn't need. Next idea: use merging, which is made for sorted input.

Part B · Merge the lists one by one

1The question

Same question. Now we use the fact that each list is sorted: two sorted lists can be merged in linear time, with no sort at all.

2What the constraints tell us

3Intuition: a snowball

Merge list 0 and list 1 into one sorted list. Merge that result with list 2. Merge that result with list 3… Like a snowball rolling forward, the result keeps growing and swallows one more list each time.

L0 + L1 → R1
R1 + L2 → R2
R2 + L3 → R3   = answer

4Building the logic

Doubt (correction): the teacher calls this O(k·n), the same as the total node count. Is that right?
→ Not quite. Each merge walks the whole growing result again. The 1st merge touches about 2n nodes, the 2nd about 3n, …, the last about k·n. Adding up: 2n + 3n + … + kn ≈ k²·n / 2 = O(k·N). That is k times more than the total N. In the example below the merges touch 6 + 8 + 11 = 25 nodes, even though there are only 11. It still passes LeetCode (where N ≤ 10⁴), but this is exactly the waste that Parts C and D remove.
Doubt: the teacher says this uses extra space because new lists get created each time. Does it?
→ It depends on how you write merge_two. If it creates new nodes, yes. The version on this page relinks the existing nodes, so the extra space is O(1). Both ways are shown in the Merge Two Sorted Lists video.

5Approach steps

  1. If there are no lists, return None.
  2. result = lists[0].
  3. For each next list: result = merge_two(result, that list).
  4. Return result.

6Code (Python)

Merge one by one
class Solution:
    def mergeTwo(self, l1, l2):
        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
        tail.next = l1 if l1 else l2
        return dummy.next

    def mergeKLists(self, lists):
        if not lists:                    # k = 0
            return None
        result = lists[0]
        for i in range(1, len(lists)):
            result = self.mergeTwo(result, lists[i])   # snowball
        return result

7Code line by line

linewhat it means
if not lists: return NoneNo lists at all. Nothing to merge.
result = lists[0]The snowball starts as the first list.
result = self.mergeTwo(result, lists[i])Swallow the next list. The new result is sorted and contains everything so far.
tail.next = l1 if l1 else l2(inside mergeTwo) Once one side is empty, attach the rest of the other side in one step.

8Dry run (the 4-list example from the videos)

L0: 1 → 4 → 5     L1: 1 → 3 → 4     L2: 2 → 6     L3: 2 → 5 → 7
mergeinputsresultnodes touched
1L0 (3) + L1 (3)1 → 1 → 3 → 4 → 4 → 56
2R1 (6) + L2 (2)1 → 1 → 2 → 3 → 4 → 4 → 5 → 68
3R2 (8) + L3 (3)1 → 1 → 2 → 2 → 3 → 4 → 4 → 5 → 5 → 6 → 711

Notice how the early nodes (the 1s) get walked over in every merge. That repeated work is what makes this O(k·N).

9Complexity & remember

RememberSnowball merging reuses Merge Two, but the growing result is re-walked k times. Better: always pick the global smallest directly (heap), or merge in balanced pairs (divide and conquer).

Part C · Min-heap of the k front nodes

1The question

Same question. Goal: pick each next node of the answer cheaply.

2What the constraints tell us

k can be up to 10⁴. Scanning all k fronts for every node would cost N·k. A heap finds the minimum of k items in log k instead.

3Intuition: only the fronts matter

The very first node of the answer must be the smallest value overall. Since each list is sorted, the smallest value of each list is its first node. So the overall smallest is the smallest of the k front nodes. We never need to look deeper.

Picture k queues at a ticket counter, each queue already lined up by height. To call the shortest person, you only compare the people at the front of each queue. When you call someone, the next person in that queue steps up to the front.

To find "the smallest of k fronts" quickly, again and again, we keep the fronts in a min-heap. Each pop gives the smallest in O(log k), instead of comparing all k.

4Building the logic

Doubt: why push node.next right after popping?
→ The heap must always contain the current front of every list that still has nodes. Popping removed one list's front, so its replacement (the next node in that list) has to go in. If node.next is None, that list is finished and we push nothing.
Doubt (Python detail): can I push the nodes straight into heapq?
→ No. heapq compares items with <, and ListNode objects can't be compared, so two equal values would crash with a TypeError. Push a tuple (node.val, i, node) instead: the value decides the order, and the list index i breaks ties, so Python never has to compare two nodes. (Each list has at most one node in the heap at a time, so (val, i) is always unique.) In Java you'd give the PriorityQueue a comparator (a, b) -> a.val - b.val, which is what the teacher writes.
Doubt (correction): the teacher says that in Python you negate numbers to get a min-heap.
→ It's the other way round. Python's heapq is a min-heap by default, which is what we need here. Negating values is the trick for a max-heap. So no minus sign in this problem.

5Approach steps

  1. Push (head.val, i, head) for every non-empty list.
  2. dummy = ListNode(0), tail = dummy.
  3. While the heap is not empty: pop the smallest node, tail.next = node, tail = node; if node.next, push it.
  4. Return dummy.next.

6Code (Python)

Min-heap of front nodes: O(N log k)
import heapq

class Solution:
    def mergeKLists(self, lists):
        heap = []
        for i, head in enumerate(lists):
            if head:                                  # skip empty lists
                heapq.heappush(heap, (head.val, i, head))

        dummy = ListNode(0)
        tail = dummy
        while heap:
            val, i, node = heapq.heappop(heap)        # smallest front node
            tail.next = node
            tail = node
            if node.next:                             # its list's new front
                heapq.heappush(heap, (node.next.val, i, node.next))
        return dummy.next

7Code line by line

linewhat it means
for i, head in enumerate(lists): if head: heappush(...)Load the k front nodes. Empty lists have no front, so they're skipped.
(head.val, i, head)Sort by value; on a tie, by list index; the node rides along.
while heap:As long as any list still has nodes.
val, i, node = heappop(heap)The smallest front among all lists, in O(log k).
tail.next = node tail = nodeAttach that node to the answer (no new node created).
if node.next: heappush(...)Its list moves forward; the new front joins the heap. We read node.next here, before it gets rewritten when the next node is attached after this one.
return dummy.nextHead of the merged list (None if all lists were empty).

8Dry run: the heap step by step

L0: 1 → 4 → 5     L1: 1 → 3 → 4     L2: 2 → 6     L3: 2 → 5 → 7

Heap items are written value(list). I show the heap contents in sorted order to make them easy to read. Inside, it's a tree, but the item popped is always the first one shown. Yellow = the item popped at that step.

start1(L0)1(L1)2(L2)2(L3)push the 4 heads
step 11(L0)1(L1)2(L2)2(L3)pop 1(L0) → attach · push 4(L0)
step 21(L1)2(L2)2(L3)4(L0)pop 1(L1) → attach · push 3(L1)
step 32(L2)2(L3)3(L1)4(L0)pop 2(L2) → attach · push 6(L2)
step 42(L3)3(L1)4(L0)6(L2)pop 2(L3) → attach · push 5(L3)
step 53(L1)4(L0)5(L3)6(L2)pop 3(L1) → attach · push 4(L1)
step 64(L0)4(L1)5(L3)6(L2)pop 4(L0) → attach · push 5(L0)
step 74(L1)5(L0)5(L3)6(L2)pop 4(L1) → attach · L1 is finished, push nothing
step 85(L0)5(L3)6(L2)pop 5(L0) → attach · L0 finished
step 95(L3)6(L2)pop 5(L3) → attach · push 7(L3)
step 106(L2)7(L3)pop 6(L2) → attach · L2 finished
step 117(L3)pop 7(L3) → attach · L3 finished
endemptyreturn dummy.next

Answer list, built one attach at a time:

dummy → [1] → [1] → [2] → [2] → [3] → [4] → [4] → [5] → [5] → [6] → [7] → None
                                                                        ↑
                                                                      tail

Snapshot of the heap as a tree, after step 4 (items 3(L1), 4(L0), 5(L3), 6(L2)):

         3
        / \
       4   5        the smallest (3) sits at the top,
      /             so the next pop is O(log k)
     6

The heap never holds more than k = 4 items, one per list that still has nodes. That's where the "log k" comes from.

The teacher also runs the 3-list example (1→4→5, 1→3→4, 2→6): heap 1,1,2 → pop 1, push 4 → 1,2,4 → pop 1, push 3 → 2,3,4 → pop 2, push 6 → 3,4,6 → pop 3, push 4 → 4,4,6 → pop 4, push 5 → 4,5,6 → pop 4 → 5,6 → pop 5 → 6 → pop 6 → empty. Answer 1 1 2 3 4 4 5 6.

9Complexity & remember

RememberThe next answer node is always one of the k fronts. Keep the fronts in a min-heap: pop the smallest, attach it, push its next. In Python push (val, i, node).

Part D · Divide and conquer (video 2)

1The question

Same question, solved the merge-sort way: the "elements" are whole lists, and "merge" is Merge Two Sorted Lists.

2What the constraints tell us

3Intuition: a knockout tournament

In merge sort, an array of length 1 is already sorted. Here, one linked list is already sorted (the question promises it). So, exactly like merge sort: split the k lists into a left half and a right half, solve each half (recursively) into one sorted list, then merge the two halves with Merge Two.

It's like a knockout tournament: lists are paired up and merged in round 1, the winners are paired up in round 2, and so on until one list is left. Unlike the snowball in Part B, every round merges lists of similar size, so no node gets re-walked again and again.

The teacher's point: 1 → 4 → 5 and 1 → 3 → 4 are each sorted, but together they aren't. If they had been 1 → 4 → 5 and 6 → 7 → 8, simply joining them would have worked. Since we can't count on that, we have to merge.

4Building the logic

Divide by index, not by copying

We don't slice the array. A function divide(lists, left, right) works on the index range left..right. The first call is divide(lists, 0, k-1).

Base case: one list → it's already sorted

When left == right, the range holds exactly one list. Don't divide further: just return lists[left].

Conquer: merge what comes back

l1 = divide(left, mid) returns one sorted list for the left half, and l2 = divide(mid+1, right) one for the right half. Two sorted lists → return merge_two(l1, l2). That returned list becomes the l1 or l2 of the caller one level up.

Doubt: how does the function know a range has "size one"?
→ We pass left and right into every call. The left child receives (left, mid), so its "right" is the parent's mid. When both indices are equal (e.g. 0 and 0), only one list is in the range.
Doubt: does it matter if k is odd?
→ No. With 3 lists (0..2), mid = 1: the left half is 0..1 (two lists) and the right half is 2..2 (one list, returned as is). The halves just differ by one.

5Approach steps

  1. If lists is empty → return None.
  2. Return divide(0, k-1).
  3. In divide(l, r): if l == r return lists[l].
  4. mid = l + (r − l) // 2; left = divide(l, mid); right = divide(mid+1, r).
  5. Return merge_two(left, right).

6Code (Python)

Divide and conquer: O(N log k)
class Solution:
    def mergeTwo(self, l1, l2):
        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
        tail.next = l1 if l1 else l2
        return dummy.next

    def divide(self, lists, left, right):
        if left == right:                        # one list: already sorted
            return lists[left]
        mid = left + (right - left) // 2
        l1 = self.divide(lists, left, mid)       # sorted left half
        l2 = self.divide(lists, mid + 1, right)  # sorted right half
        return self.mergeTwo(l1, l2)             # conquer

    def mergeKLists(self, lists):
        if not lists:                            # no lists at all
            return None
        return self.divide(lists, 0, len(lists) - 1)

7Code line by line

linewhat it means
if not lists: return Nonek = 0. Without this, divide(0, -1) would go wrong.
if left == right: return lists[left]Base case: one list in the range. It's sorted already (it may even be None, which is fine).
mid = left + (right - left) // 2Split point. Left half gets the extra list when the count is odd.
l1 = self.divide(lists, left, mid)Fully solve the left half first (Python pauses here until it's done).
l2 = self.divide(lists, mid + 1, right)Then the right half.
return self.mergeTwo(l1, l2)Two sorted lists → one sorted list, handed back to the caller.

8Dry run: the pairing tree

                       divide(0, 3)   mid = 1
                     /                           \
          divide(0, 1)  mid = 0             divide(2, 3)  mid = 2
          /            \                    /            \
   divide(0,0)     divide(1,1)       divide(2,2)     divide(3,3)
   1 → 4 → 5       1 → 3 → 4         2 → 6           2 → 5 → 7
          \            /                    \            /
     merge → 1 1 3 4 4 5                merge → 2 2 5 6 7
                     \                           /
               merge → 1 1 2 2 3 4 4 5 5 6 7     ← final answer
  1. divide(0,3): mid = 1 → first solve the left half 0..1. (0,3) waits.
  2. divide(0,1): mid = 0 → left half 0..0.
  3. divide(0,0): one list → return 1 → 4 → 5.
  4. Back in (0,1): right half divide(1,1) → return 1 → 3 → 4.
  5. (0,1) merges them → 1 1 3 4 4 5, returned to (0,3) as its l1.
  6. (0,3) now solves the right half: divide(2,3), mid = 2 → divide(2,2) returns 2 → 6, divide(3,3) returns 2 → 5 → 7 → merge → 2 2 5 6 7. This is (0,3)'s l2.
  7. (0,3) merges l1 and l2 → 1 1 2 2 3 4 4 5 5 6 7. The stack is empty. Done ✓
stack at step 3 (deepest)
(0,3)(0,1)(0,0) → 1 4 5
stack at step 5
(0,3)(0,1) → merge → 1 1 3 4 4 5
stack during step 6
(0,3) l1 ready(2,3)(3,3) → 2 5 7

The newest call is on top (red). Each call returns one sorted list to the call below it.

Why it's N log k: count nodes per level

The teacher counts the nodes at each level of the tree. There are 11 nodes in total, and every level merges all 11 once (bottom level: 6 + 5 nodes across the two merges; top level: 11 nodes). The number of levels is how many times you can halve k: log₂ 4 = 2.

levelmergesnodes walked
1 (pairs)(L0,L1) and (L2,L3)6 + 5 = 11 = N
2 (final)(R01, R23)11 = N
totallog₂ k = 2 levelsN · log k = 22 (snowball in Part B: 25, and the gap grows fast with k)

9Complexity & remember

RememberMerge sort on lists: l == r → return that list; else split at mid, solve both halves, mergeTwo them. Balanced pairs → N log k.

Part E · Revision page

A · collect + sortB · one by oneC · min-heapD · divide & conquer
ideaignore sortednesssnowball with Merge Twosmallest of the k frontsmerge in balanced pairs
timeO(N log N)O(k·N)O(N log k)O(N log k)
extra spaceO(N) array + new nodesO(1) (relinking)O(k) heapO(log k) stack (+ O(N) if merge makes new nodes)
base casenone neededif not listsskip empty headsif not lists; l == r

N = total nodes, k = number of lists. The teacher writes the total as "kn", so her "n log k" for C and D means the same as N log k here.

If you remember only 5 lines 1. Merge Two Sorted Lists (dummy + tail, attach the leftover in one step) is the building block.
2. The next answer node is always one of the k list fronts.
3. Heap: push the heads; pop smallest, attach, push its next. Python: (val, i, node).
4. D&C: split the index range at mid, base case l == r, merge the two halves.
5. Both C and D are O(N log k); the snowball is O(k·N); sort-everything is O(N log N).
Mistakes to avoid ✗ pushing bare ListNodes into heapq (TypeError on equal values)
✗ negating values (that makes a max-heap; heapq is already a min-heap)
✗ forgetting to push node.next after a pop
✗ pushing None heads into the heap
✗ no if not lists check before divide(0, k-1)
✗ thinking the snowball merge is O(N) total (it re-walks the result every time)
test it yourself (paste under any solution)
def build(vals):
    dummy = ListNode(0)
    t = dummy
    for v in vals:
        t.next = ListNode(v)
        t = t.next
    return dummy.next

def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

s = Solution()
lists = [build([1, 4, 5]), build([1, 3, 4]), build([2, 6]), build([2, 5, 7])]
print(to_list(s.mergeKLists(lists)))        # [1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7]
print(to_list(s.mergeKLists([])))           # []
print(to_list(s.mergeKLists([None])))       # []

Based on these videos: Merge K Sorted Lists · Part 1 (brute force, one by one, priority queue) · Merge K Sorted Lists · Part 2 (divide and conquer)