Skip to content

K-way Merge

You already know how to merge two sorted lists in O(n)O(n) — it’s the merge step from merge sort. The interview twist is merging kk of them at once. Doing that with repeated two-way merges works, but a min-heap that always holds one candidate per list turns it into a single clean pass, and it’s the pattern behind an entire family of “smallest across k sequences” questions.

What you’ll learn

  • Why merging kk lists two-at-a-time is worse than it looks.
  • The k-way merge template: a min-heap of (value, list_index, element_index)(value, list_index, element_index) tuples, one entry per list.
  • How the same heap-of-heads idea generalizes to a sorted matrix, not just a list of lists.
  • Why the pattern costs O(Nlogk)O(N \log k), where NN is the total element count.

The naive approach, and why it’s worse than it looks

Merging kk sorted lists two at a time — merge list 1 into list 2, merge that into list 3, and so on — costs O(Nk)O(N \cdot k) in the worst case: each of the kk merge passes touches close to all NN elements. As kk grows, that quadratic-ish blowup gets painful. The fix is to never compare more than kk candidates at once, no matter how many elements are still buried inside each list.

The pattern: one heap slot per list

Keep a min-heap with at most one entry per list — always its current smallest unconsumed element. Popping the heap’s minimum gives you the next overall smallest value; you then push that list’s next element back in to replace it. The heap tuple carries (value, list_idx, elem_idx)(value, list_idx, elem_idx) so you always know exactly which list to advance.

merge_k_sorted_lists.py
import heapq
 
def merge_k_sorted(lists):
    result = []
    heap = []
 
    # Seed the heap with each list's first element (skip empty lists).
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst[0], i, 0))
 
    while heap:
        val, list_idx, elem_idx = heapq.heappop(heap)
        result.append(val)
 
        # Advance the list this value came from, if it has more elements.
        if elem_idx + 1 < len(lists[list_idx]):
            next_val = lists[list_idx][elem_idx + 1]
            heapq.heappush(heap, (next_val, list_idx, elem_idx + 1))
 
    return result
 
 
lists = [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
print(merge_k_sorted(lists))   # expect [1, 2, 3, 4, 5, 6, 7, 8, 9]
merge_k_sorted_lists.py
import heapq
 
def merge_k_sorted(lists):
    result = []
    heap = []
 
    # Seed the heap with each list's first element (skip empty lists).
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst[0], i, 0))
 
    while heap:
        val, list_idx, elem_idx = heapq.heappop(heap)
        result.append(val)
 
        # Advance the list this value came from, if it has more elements.
        if elem_idx + 1 < len(lists[list_idx]):
            next_val = lists[list_idx][elem_idx + 1]
            heapq.heappush(heap, (next_val, list_idx, elem_idx + 1))
 
    return result
 
 
lists = [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
print(merge_k_sorted(lists))   # expect [1, 2, 3, 4, 5, 6, 7, 8, 9]

How it works

diagram K-way merge: each list's head feeds one heap slot mermaid

The heap never holds more than kk items at once — one per list — no matter how large each individual list is. Every element from every list gets pushed and popped from the heap exactly once over the whole run.

Worked example: Kth Smallest Element in a Sorted Matrix

A matrix with sorted rows (and sorted columns) is just kk sorted lists in disguise — one list per row. Seed the heap with the first element of every row, then pop kk times, advancing along each row exactly like before.

kth_smallest_in_matrix.py
import heapq
 
def kth_smallest_in_matrix(matrix, k):
    n = len(matrix)
    # Seed the heap with the first element of every row.
    heap = [(matrix[row][0], row, 0) for row in range(n)]
    heapq.heapify(heap)
 
    val = None
    for _ in range(k):
        val, row, col = heapq.heappop(heap)
        if col + 1 < n:
            heapq.heappush(heap, (matrix[row][col + 1], row, col + 1))
    return val
 
 
matrix = [
    [1, 5, 9],
    [10, 11, 13],
    [12, 13, 15],
]
print(kth_smallest_in_matrix(matrix, 8))   # expect 13
kth_smallest_in_matrix.py
import heapq
 
def kth_smallest_in_matrix(matrix, k):
    n = len(matrix)
    # Seed the heap with the first element of every row.
    heap = [(matrix[row][0], row, 0) for row in range(n)]
    heapq.heapify(heap)
 
    val = None
    for _ in range(k):
        val, row, col = heapq.heappop(heap)
        if col + 1 < n:
            heapq.heappush(heap, (matrix[row][col + 1], row, col + 1))
    return val
 
 
matrix = [
    [1, 5, 9],
    [10, 11, 13],
    [12, 13, 15],
]
print(kth_smallest_in_matrix(matrix, 8))   # expect 13

The kk-th pop off the heap is, by construction, the kk-th smallest value across the whole matrix — you never sort all 99 elements to get there.

Time and space complexity

OperationComplexity
Seeding the heap with kk headsO(k)O(k)
Each pop + pushO(logk)O(\log k)
Total for NN elements across kk listsO(Nlogk)O(N \log k)
Space (heap holds at most kk entries)O(k)O(k)

Compare that to sorting all NN elements directly: O(NlogN)O(N \log N). When kNk \ll N — a handful of long lists, or a matrix with far more entries than rows — the k-way merge is meaningfully cheaper.

When to use it

  • You have kk already-sorted sequences (lists, arrays, or matrix rows) and need them combined into one sorted output, or need only the kk-th overall smallest/largest value without materializing the full merge.
  • kk is small relative to the total element count NN — the heap’s O(logk)O(\log k) cost per step is what makes this cheaper than one big sort.
  • The “list” doesn’t have to be a Python list — linked lists, matrix rows, or even ranges from kk different iterators all work the same way, as long as each one is individually sorted and you can ask for “the next element after this one.”

Practice — real LeetCode problems

Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.

LC 23 — Merge k Sorted Lists · Hard

Problem. Merge kk sorted linked lists into one sorted list and return its head.

Constraints. 0 <= k <= 10^40 <= k <= 10^4, 0 <= len(each list) <= 5000 <= len(each list) <= 500, total nodes up to 10^410^4.

Examples. [[1,4,5],[1,3,4],[2,6]][[1,4,5],[1,3,4],[2,6]] gives [1,1,2,3,4,4,5,6][1,1,2,3,4,4,5,6] · [][] gives [][] · [[]][[]] gives [][]

Editorial

The heap holds at most one node per list — the smallest unconsumed value from each. Popping the global minimum and pushing that node’s successor keeps the invariant.

Time O(Nlogk)O(N \log k) for NN total nodes across kk lists. Space O(k)O(k).

Two other approaches worth naming:

  • Pairwise merging. Merge lists in rounds, halving the count each time: O(Nlogk)O(N \log k) too, and it avoids the heap entirely. Many people find it easier to reason about.
  • Concatenate and sort. O(NlogN)O(N \log N) — worse, and it ignores the sortedness you were given.

Follow-ups: “Do it without a heap?” — pairwise merging, using LC 21 as the subroutine. “Merge kk sorted arrays?” — same heap, indices instead of nodes. “Why O(Nlogk)O(N \log k) and not O(NlogN)O(N \log N)?” — the heap never holds more than kk entries.

LC 378 — Kth Smallest Element in a Sorted Matrix · Medium

Problem. Given an n x nn x n matrix whose rows and columns are each sorted ascending, return the kkth smallest element (in overall sorted order, counting duplicates).

Constraints. 1 <= n <= 3001 <= n <= 300, 1 <= k <= n^21 <= k <= n^2.

Examples. matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8 gives 1313 · matrix = [[-5]], k = 1matrix = [[-5]], k = 1 gives -5-5

Editorial

The matrix is nn sorted lists, so this is a k-way merge stopped after kk pops.

Time O(klogn)O(k \log n). Space O(n)O(n).

Seeding only min(n, k)min(n, k) rows is a genuine optimisation: the first element of row kk is at least as large as the first elements of all earlier rows, so it cannot be among the kk smallest.

[[1,2],[1,3]][[1,2],[1,3]] with k = 2k = 2 gives 11 — duplicates count separately, so the answer is the second 11, not 22. That is a common misreading.

The stronger solution is binary search on the value range: pick a candidate value, count how many matrix entries are at most it (via a staircase walk from the bottom-left, O(n)O(n)), and binary search until the count reaches kk. That is O(nlog(maxmin))O(n \log(\max - \min)), which beats O(klogn)O(k \log n) when kk approaches n2n^2, and it uses O(1)O(1) space. Worth naming as the follow-up answer — it composes the staircase walk with binary search on the answer.

Follow-ups: “Beat O(klogn)O(k \log n)?” — the value-range binary search above. “Why only min(n, k)min(n, k) rows?” — the bounding argument. “Kth largest?” — mirror it, or ask for the (n2k+1)(n^2 - k + 1)th smallest.

LC 373 — Find K Pairs with Smallest Sums · Medium

Problem. Given two sorted arrays and an integer kk, return the kk pairs (u, v)(u, v) with uu from nums1nums1 and vv from nums2nums2 that have the smallest sums.

Constraints. 1 <= len(nums1), len(nums2) <= 10^51 <= len(nums1), len(nums2) <= 10^5, both sorted ascending, 1 <= k <= 10^41 <= k <= 10^4.

Examples. nums1 = [1,7,11], nums2 = [2,4,6], k = 3nums1 = [1,7,11], nums2 = [2,4,6], k = 3 gives [[1,2],[1,4],[1,6]][[1,2],[1,4],[1,6]] · nums1 = [1,1,2], nums2 = [1,2,3], k = 2nums1 = [1,1,2], nums2 = [1,2,3], k = 2 gives [[1,1],[1,1]][[1,1],[1,1]]

Editorial

There can be 101010^{10} pairs, so they must be generated lazily. The structure is a k-way merge over conceptual rows: row ii is the sorted sequence nums1[i] + nums2[0], nums1[i] + nums2[1], …nums1[i] + nums2[0], nums1[i] + nums2[1], ….

Time O(klogk)O(k \log k). Space O(k)O(k).

The generation rule is what makes it correct: seed one entry per nums1nums1 index, and when popping (i, j)(i, j) push only (i, j+1)(i, j+1). Because each row is entered once at j = 0j = 0 and advanced only along jj, every pair is reachable by exactly one path — so no duplicates and nothing missed. Pushing both (i+1, j)(i+1, j) and (i, j+1)(i, j+1) is the common variant and it needs a visitedvisited set, because pairs become reachable two ways.

([1,1,2], [1,2,3], 2)([1,1,2], [1,2,3], 2) giving [[1,1],[1,1]][[1,1],[1,1]] shows duplicate values are distinct pairs — both nums1[0]nums1[0] and nums1[1]nums1[1] pair with nums2[0]nums2[0].

Seeding only min(len(nums1), k)min(len(nums1), k) rows matters at the stated constraints: with len(nums1) = 10^5len(nums1) = 10^5 and k = 10^4k = 10^4, seeding everything would be ten times the necessary work.

Follow-ups: “Why not push both directions?” — it works but needs deduplication; the one-direction rule avoids it. “Kth smallest sum rather than the first k?” — pop kk times and return the last. “Three arrays?” — fold pairwise, or extend the heap entry to three indices.

LeetCode problem set

#ProblemDifficultyThe twist
23Merge k Sorted ListsHardThe exact template, applied to linked lists instead of Python lists
378Kth Smallest Element in a Sorted MatrixMediumTreat each row as one of the kk sorted sequences
632Smallest Range Covering Elements from K ListsHardExtend the pattern: track the current max alongside the heap’s min to shrink the covering range as you advance
373Find K Pairs with Smallest SumsMediumThe “lists” are implicit (all pairs (nums1[i], nums2[j])(nums1[i], nums2[j])), generated and pruned lazily through the heap instead of built up front

Recap

  • Merging kk sorted sequences two at a time costs up to O(Nk)O(N \cdot k) — a single heap holding one candidate per sequence does it in O(Nlogk)O(N \log k) instead.
  • The heap tuple is (value, list_idx, elem_idx)(value, list_idx, elem_idx) — the indices both identify where to advance next and break ties Python can compare safely.
  • Every element is pushed and popped from the heap exactly once over the whole run.
  • The same idea covers linked lists, plain arrays, matrix rows, and even lazily generated pairs — anything you can ask “what’s your next smallest element?”

Next: Binary Search on Answer — turning binary search from an array lookup into a tool for optimization problems.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did