Hard#238 min readLinked ListsDivide and ConquerPriority QueueLeetCode

Merge k Sorted Lists

Pairwise merge

Divide & conquer or min-heap. Your first ‘hard’ that’s just a clean composition of basics.

Published Apr 15, 2026

Problem

You are given an array of k sorted linked lists. Merge them into one sorted linked list and return its head.

Input
lists = [[1,4,5],[1,3,4],[2,6]]
Output
[1,1,2,3,4,4,5,6]
Input
lists = []
Output
[]

Intuition

Merge k Sorted Lists is not a new problem. It is the merge step from #21 repeated on a larger schedule.

The key question is how to schedule the merges. There are two standard answers:

  • keep one current node from each list in a min-heap
  • repeatedly merge pairs of lists until only one remains

The second one is cleaner in a linked-list implementation because it reuses the exact same merge_two_lists primitive you already know.

Interactive concept

K-Way Merge Frontier

A min-heap keeps only the smallest visible node from each list.

Expose list heads

Each sorted list contributes one candidate. Anything behind a head cannot be smaller than that head.

heap[1A, 1B, 2C]
List A
1 -> 4 -> 5
List B
1 -> 3 -> 4
List C
2 -> 6

Approach

Pairwise Merge

TimeO(n log k)
SpaceO(1)
Recommended

Merge lists in rounds. In each round, merge list i with list i + step, where step doubles after every pass. This is the iterative version of divide and conquer:

  1. step = 1: merge adjacent pairs
  2. step = 2: merge the results of the previous round
  3. repeat until one list remains

Each node participates in O(log k) merges, so the total work is O(n log k).

impl Solution {
    pub fn merge_k_lists(mut lists: Vec<Option<Box<ListNode>>>) -> Option<Box<ListNode>> {
        fn merge_two(
            mut list1: Option<Box<ListNode>>,
            mut list2: Option<Box<ListNode>>,
        ) -> Option<Box<ListNode>> {
            let mut dummy = Box::new(ListNode { val: 0, next: None });
            let mut tail = &mut dummy;

            while list1.is_some() && list2.is_some() {
                let take_left = list1.as_ref().unwrap().val <= list2.as_ref().unwrap().val;
                let mut node= if take_left {
                    let mut node= list1.take().unwrap();
                    list1= node.next.take();
                    node
                } else {
                    let mut node= list2.take().unwrap();
                    list2= node.next.take();
                    node
                };

                tail.next= Some(node);
                tail= tail.next.as_mut().unwrap();
            }

            tail.next= if list1.is_some() { list1 } else { list2 };
            dummy.next
        }

        if lists.is_empty() {
            return None;
        }

        let mut step= 1;
        while step < lists.len() {
            let mut i= 0;
            while i + step < lists.len() {
                let left= lists[i].take();
                let right= lists[i + step].take();
                lists[i]= merge_two(left, right);
                i = step * 2;
            }
            step = 2;
        }

        lists.into_iter().next().unwrap_or(None)
    }
}
Rust · runnable

Trace

Pairwise rounds for lists = [[1,4,5], [1,3,4], [2,6]]:

Merge schedule#23

Merge k Sorted Lists walkthrough

[[1,4,5], [1,3,4], [2,6]]

Step 1 of 30%

Merge schedule state

round 1
1→4→5
0
1→3→4
1
2→6
2

The hard problem is scheduling repeated two-list merges.

Start with independent sorted streams

Decision

Pair lists 0 and 1; carry list 2 forward.

Update

Round 1 will reduce three heads to two heads.

Why it works

Balanced merging keeps the merge tree shallow.

Invariant

Each round merges disjoint sorted lists, so every node participates in at most one merge per round.

Finish

Pairwise reduction leaves one sorted list: [1,1,2,3,4,4,5,6].

Sequentially merging into one accumulator can degrade to O(kN).Do not allocate new nodes when pointer relinking is enough.

The same merge_two_lists primitive does all the work. The schedule is the only thing that changes.

Edge cases

  • No lists — return null.
  • One non-null list — return it unchanged.
  • Many empty lists — skip over null inputs naturally.
  • Strongly uneven lengths — pairwise merging still touches each node only O(log k) times.

Takeaway

Merge k Sorted Lists is a composition problem, not a novel one. Once merge_two_lists is solid, the hard part is deciding how to schedule repeated merges. Pairwise reduction is the cleanest path when you want a linked-list solution that stays simple in both Rust and TypeScript.

More in Linked Lists