↓Skip to main content
  1. LeetCode/

LeetCode 23: Merge k Sorted Lists

·4 mins· ·
LeetCode Hard Linked-List Heap Merge-Sort Divide-and-Conquer
Wei Yi Chung
Author
Wei Yi Chung
Working at the contributing of open source, distributed systems, and data engineering.
Table of Contents

Meta Data
#

Difficulty: hard First Attempt: 2026-10-20 Source: extension of the merge sort practice ladder (high frequency)

Approach
#

Let N be the total number of nodes and k the number of lists. The target is O(N log k).

  • Heap: each list is sorted, so the smallest remaining node is always the head of some list. The heap only needs the k heads.
  • Divide and conquer: treat each list as one element and run merge sort over the lists. The merge step is LC 21.

My Attempts
#

Attempt 1: push every value into a heap
#

pq = []
dummy = tail = ListNode()
for node in lists:
    while node:
        heapq.heappush(pq, node.val)
        node = node.next

while pq:
    node = heapq.heappop(pq)
    tail.next = ListNode(node)
    tail = tail.next

return dummy.next

It passes, but runs in O(N log N) time and O(N) space. It is equivalent to dumping all values, sorting them, and rebuilding the list, so it ignores that each list is already sorted. It also creates N new nodes, and node = heappop(...) actually holds a value, which is a misleading name.

Attempt 2: divide and conquer
#

def mergeAndSort(low: int, high: int):
    if high - low == 0:
        return None
    if high - low == 1:
        return list[low]          # should be lists[low]

    mid = (low + high) // 2
    return merge(mergeAndSort(low, mid), mergeAndSort(mid, high))

The structure is right except for one typo. list is a built-in type, and list[low] is valid type-hint syntax, so it does not fail immediately. The AttributeError only appears later when merge reads .val. When the error shows up somewhere other than where the bug is, it is the hardest kind to find.

A typo that produces an undefined name fails right away with NameError. A typo that happens to match an existing name (list, dict, sum, max, id) keeps running silently.

Solution
#

Divide and conquer (merge sort)
#

class Solution:
    def mergeKLists(self, lists: list[ListNode | None]) -> ListNode | None:

        def merge(a: ListNode | None, b: ListNode | None) -> ListNode | None:   # LC 21
            dummy = tail = ListNode()
            while a and b:
                if a.val <= b.val:
                    tail.next = a
                    a = a.next
                else:
                    tail.next = b
                    b = b.next
                tail = tail.next
            tail.next = a or b
            return dummy.next

        def mergeAndSort(low: int, high: int) -> ListNode | None:
            if high - low == 0:
                return None                    # empty input; avoids infinite recursion
            if high - low == 1:
                return lists[low]
            mid = (low + high) // 2
            return merge(mergeAndSort(low, mid), mergeAndSort(mid, high))

        return mergeAndSort(0, len(lists))

Heap with only k heads
#

import heapq

class Solution:
    def mergeKLists(self, lists: list[ListNode | None]) -> ListNode | None:
        pq = []
        for i, node in enumerate(lists):
            if node:
                heapq.heappush(pq, (node.val, i, node))

        dummy = tail = ListNode()
        while pq:
            _, i, node = heapq.heappop(pq)
            tail.next = node                   # reuse the original node
            tail = tail.next
            if node.next:
                heapq.heappush(pq, (node.next.val, i, node.next))
        return dummy.next

The i in the middle of the tuple matters: on equal values, Python compares the next field, and comparing ListNode objects raises TypeError. Each list has at most one node in the heap at a time, so i is never repeated.

Bottom-up iteration (O(1) space)
#

k = len(lists)
if k == 0:
    return None
step = 1
while step < k:
    for i in range(0, k - step, step * 2):
        lists[i] = merge(lists[i], lists[i + step])
    step *= 2
return lists[0]

This modifies the input lists; mention it in an interview.

Complexity
#

ApproachTimeSpace
Push every value into a heapO(N log N)O(N)
Heap with k headsO(N log k)O(k)
Divide and conquer, recursiveO(N log k)O(log k)
Divide and conquer, bottom-upO(N log k)O(1)
Merge lists one by oneO(N k)O(1)

Divide and conquer has log k levels, and every node is merged once per level, so each level costs O(N). When merging one list at a time, nodes from the first list are rescanned in every later merge, about N k / 2 work in total.

Mistakes to Avoid
#

  • Only handling high - low == 1: with lists = [], mergeAndSort(0, 0) recurses forever.
  • Leaving out a tie-breaker in the heap tuple.
  • Naming a variable after a built-in.

Interview Explanation
#

Each list is sorted, so there are two O(N log k) approaches. One keeps only the current head of each list in a heap: pop the smallest, attach it, and push its successor. The other is divide and conquer: treat the k lists as merge sort elements and merge them in pairs using LC 21, giving log k levels at O(N) each. If the lists arrive as streams and I cannot see them all at once, I would choose the heap, because it only needs the current head of each list.

Related

LeetCode 2426: Number of Pairs Satisfying Inequality
·3 mins
LeetCode Hard Merge-Sort Divide-and-Conquer Two-Pointers Counting
LeetCode 2426 note: simplify the inequality, then decide whether the matches form a prefix or a suffix
LeetCode 315: Count of Smaller Numbers After Self
·4 mins
LeetCode Hard Merge-Sort Divide-and-Conquer Binary-Indexed-Tree Counting
LeetCode 315 note: merge sort with indices, plus a BIT solution
LeetCode LCR 170: Count Inversions (Sword Offer 51)
·3 mins
LeetCode Hard Merge-Sort Divide-and-Conquer Counting
LCR 170 note: count inversions during the merge step
LeetCode 493: Reverse Pairs
·4 mins
LeetCode Hard Merge-Sort Divide-and-Conquer Two-Pointers Counting
LeetCode 493 note: when the counting condition differs from the sort order, scan first, then merge
LeetCode 327: Count of Range Sum
·3 mins
LeetCode Hard Merge-Sort Divide-and-Conquer Prefix-Sum Binary-Indexed-Tree
LeetCode 327 note: prefix sums plus merge sort counting with two pointers bounding a range, and a log of retyping it over a dozen times
LeetCode 4051: Count Subarrays with Distant Sums
·5 mins
LeetCode Hard Merge-Sort Prefix-Sum Binary-Indexed-Tree Biweekly-Contest
LeetCode 4051 note: prefix sums, complement counting, range counting, and the k = 0 edge case