Skip to main content

Merge K Sorted Lists Algorithm Solution

Complete Java implementation of merging k sorted linked lists with optimal time and space complexity analysis for FAANG interviews.

The merge k sorted lists problem requires combining multiple sorted linked lists into a single sorted linked list. This classic algorithmic challenge tests understanding of heap data structures, divide-and-conquer strategies, and efficient list manipulation techniques.

1. Problem Restatement

Clarifying questions to ask the interviewer:

  1. What is the maximum number of lists that can be provided? (e.g., is it bounded by a constant or could it be very large?)
  2. Are the lists guaranteed to be sorted in ascending order, or should we handle descending as well?
  3. Can the input lists contain duplicate values? If so, should duplicates be preserved in the merged result?
  4. Is it acceptable to mutate the input lists during processing, or must we maintain their original state?
  5. What is the expected time complexity for the solution? Are we aiming for O(n log k) where n is total elements and k is number of lists?

Restatement in my own words: We are given an array or collection of k sorted linked lists, each potentially of different lengths. The task is to merge all these lists into a single sorted linked list that maintains the ascending order property.

Clarification of inputs, outputs, and constraints:

  • Input: An array/list of k linked lists where each list is already sorted in ascending order
  • Output: A single linked list containing all elements from the k input lists in sorted order
  • Each list node has a value field and next pointer to the next node
  • The merged result must preserve all elements with their relative ordering maintained

Assumptions I'm making:

  • All input lists are sorted in ascending order (non-decreasing)
  • Lists can be empty, and we should handle that gracefully
  • The merged list should maintain the sorted property across all elements from all lists
  • We can modify the input lists during processing if needed

Data shape and hidden structure: The problem presents k sorted sequences that can be viewed as a multi-way merge operation. Each list represents one "stream" of sorted elements, and we're essentially merging k streams into one. This suggests that the optimal approach should leverage the fact that each individual list is already sorted, rather than treating this as a general unsorted merge.

The structure is hierarchical - we have k independent sorted sequences that need to be combined while maintaining the global sorted property. The key insight is that we can use a min-heap to efficiently select the next smallest element across all k lists at each step.

2. Pattern Recognition

The problem asks us to merge k sorted linked lists into one sorted linked list. This is a classic merging pattern that appears frequently in algorithm interviews.

Most likely patterns:

  1. Merge Sort/Merge K Lists Pattern - This is the direct application of merge sort's merge operation, but extended to k lists instead of 2
  2. Heap-Based Merging Pattern - Using a priority queue to efficiently select the smallest element across all lists
  3. Divide and Conquer Pattern - Breaking down the problem into smaller subproblems

Wording that signals the pattern:

  • "k sorted lists" - indicates we're dealing with multiple sorted sequences that need to be combined
  • "merge" - the fundamental operation we're applying repeatedly
  • "sorted" - tells us elements maintain their relative ordering, which is key for optimization

Why this fits the pattern: This problem is a natural extension of the classic "merge two sorted lists" problem. The key insight is that we can't simply merge lists one by one in a nested fashion, as that would result in suboptimal complexity. Instead, we need to think about how to efficiently find the minimum element among all current heads of the k lists.

Core insight: The most efficient approach leverages a min-heap (priority queue) to maintain the smallest elements from each list's current position, allowing us to extract the globally minimum element in O(log k) time rather than O(k) time.

This pattern appears in many variations:

  • Merging k sorted arrays
  • Finding the kth smallest element across multiple sorted sequences
  • Merging streams of data that arrive in sorted order

The fundamental structure is always: maintain a collection of elements from k different sources, and repeatedly extract the minimum element while maintaining the invariant that we're always working with the next smallest available element.

3. Approach Evolution

My first instinct is to think about merging two sorted lists, which I know can be done in O(m+n) time where m and n are the lengths of the lists. But here we have k lists, so if I merge them one by one, that would be O(k²n) in the worst case where n is the average list length. That's inefficient because I'm doing redundant work.

Let me think about how to optimize this. I notice that if I use a min-heap to always pick the smallest element across all current list heads, I can reduce this to O(n log k) where n is the total number of elements. The key insight is that I don't need to compare all k elements every time - just the ones at the front of each list.

The approach I'll use is to maintain a min-heap of size k, where each element in the heap represents the next available element from one of the lists. Initially, I'll add the first element from each list to the heap. Then, I repeatedly extract the minimum element and add the next element from that same list to the heap.

This maintains an important invariant: at any point, the heap contains exactly one element from each list that hasn't been processed yet, and it's always the smallest among all unprocessed elements. This ensures I process elements in sorted order.

The complexity improvement comes from the fact that each element is inserted and removed from the heap exactly once, and heap operations take O(log k) time. Since there are n total elements, the overall complexity becomes O(n log k), which is much better than the naive O(k²n) approach.

I should also consider whether I can do this in-place or if I need to create a new merged list. Since the problem asks for a merged sorted list, I'll need to create new nodes, so that's not an issue. The approach is clean and efficient.

Let me walk through the key steps:

  1. Initialize a min-heap with first element of each list
  2. Extract minimum element and add to result list
  3. Add the next element from same list to heap
  4. Repeat until heap is empty

This approach gives us the optimal O(n log k) time complexity where we're only doing work proportional to the total number of elements, not the number of lists.

4. Data Structure Choice

For this problem, I'll use a min-heap (PriorityQueue) as my primary data structure.

The min-heap is ideal because:

  1. It maintains elements in sorted order automatically
  2. It provides O(log k) insertion and extraction operations where k is the number of lists
  3. It naturally handles the "merge" operation by always picking the smallest available element

I'll also use a custom wrapper class to track which list each element came from, allowing me to efficiently advance the corresponding list pointer after extraction.

Why this choice works well:

  • Time complexity: O(n log k) where n is total elements and k is number of lists
  • Space complexity: O(k) for the heap, which is optimal since we need to track at most one element from each list
  • Implementation simplicity: Java's PriorityQueue handles all the heap operations automatically

Alternative data structures considered:

  • Array-based approach: Would require O(k) to find minimum each time, leading to O(nk) complexity
  • Multiset/TreeMap: More complex to implement and maintain, with similar O(n log k) complexity but higher constant factors

The min-heap approach is optimal for this problem because it directly addresses the core challenge: efficiently finding and advancing to the next smallest element across multiple sorted lists.

class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

5. Algorithm Walkthrough

Let me trace through the algorithm with a concrete example to demonstrate how it works step-by-step.

Example: lists = [[1,4,5],[1,3,4],[2,6]]

Step 1: Initialize the min-heap with the first element of each list.

  • Heap contains: (1,0,0), (1,0,1), (2,0,2)
  • The tuple format is (value, listIndex, elementIndex)
  • Heap size: 3

Step 2: Extract minimum element (1,0,0) and add to result.

  • Result: [1]
  • Add next element from same list (4,0,1) to heap
  • Heap: (1,0,1), (2,0,2), (1,0,1) → (1,0,1), (2,0,2)

Step 3: Extract minimum element (1,0,1) and add to result.

  • Result: [1,1]
  • Add next element from same list (4,0,2) to heap
  • Heap: (1,0,2), (2,0,2)

Step 4: Extract minimum element (1,0,2) and add to result.

  • Result: [1,1,4]
  • Add next element from same list (5,0,3) to heap
  • Heap: (2,0,2), (5,0,3)

Step 5: Extract minimum element (2,0,2) and add to result.

  • Result: [1,1,4,2]
  • Add next element from same list (6,0,4) to heap
  • Heap: (5,0,3), (6,0,4)

Step 6: Extract minimum element (5,0,3) and add to result.

  • Result: [1,1,4,2,5]
  • Heap is now: (6,0,4)

Step 7: Extract minimum element (6,0,4) and add to result.

  • Result: [1,1,4,2,5,6]
  • Heap is now empty

Step 8: Return merged list [1,1,4,2,5,6]

The algorithm correctly handles the merging by always selecting the smallest available element from all active lists, maintaining the sorted order throughout the process. Each step ensures we never skip a smaller element that might come later in a different list.

The key insight is that we only need to track the current head of each list in our heap, and we can safely extract the minimum element at each step because we're guaranteed that no smaller element exists in any of the remaining lists.

6. Java Solution

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }
        
        // Use a min-heap to efficiently get the smallest element across all lists
        PriorityQueue<ListNode> minHeap = new PriorityQueue<>((a, b) -> a.val - b.val);
        
        // Add the first node of each non-null list to the heap
        for (ListNode list : lists) {
            if (list != null) {
                minHeap.offer(list);
            }
        }
        
        // Dummy node to simplify list construction
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;
        
        // Process nodes from the heap until it's empty
        while (!minHeap.isEmpty()) {
            // Get the smallest node
            ListNode smallest = minHeap.poll();
            
            // Add it to our merged list
            current.next = smallest;
            current = current.next;
            
            // If there's a next node in the same list, add it to the heap
            if (smallest.next != null) {
                minHeap.offer(smallest.next);
            }
        }
        
        return dummy.next;
    }
}

This solution uses a min-heap to efficiently merge k sorted linked lists. We start by adding the first node of each non-empty list to the heap, then repeatedly extract the minimum element and add the next element from the same list (if it exists). The dummy node simplifies handling of the result list construction.

7. Complexity Analysis

Time Complexity: O(N log k) Where N is the total number of nodes across all lists and k is the number of lists. The algorithm processes each node exactly once, and for each node insertion into the priority queue, we perform O(log k) operations. Since we have N nodes total and each requires O(log k) heap operations, the overall time complexity is O(N log k).

Space Complexity: O(k) The space complexity comes from the priority queue which can hold at most k elements (one element from each list). In the worst case, we might have up to k elements in the queue simultaneously. Additionally, we use O(1) extra space for variables and pointers.

Why these bounds are correct:

  • The heap size never exceeds k because we only keep one element per list in the queue
  • Each node is processed exactly once, and each processing involves O(log k) heap operations
  • The total number of nodes N is the sum of all list lengths, which determines our processing count
  • The heap operations are logarithmic in the number of lists, not the total nodes

Optimality: This solution is optimal for the given constraints. We must process each node at least once to construct the merged result, making O(N) time complexity unavoidable. The O(log k) factor is necessary because we need to efficiently find the minimum among k candidates, which a heap provides optimally. Any approach that processes nodes in order will require at least O(N log k) time, making this solution near-optimal.

The complexity analysis assumes that the priority queue operations (insertion and removal) take O(log k) time, which is correct for a binary heap implementation with k elements.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty input list: Handle null or empty ListNode[] gracefully to avoid null pointer exceptions
  • Single list: When k = 1, return the single list as-is without unnecessary processing
  • One empty list: When one of the lists is empty, it should be ignored in the merge process

Duplicate Values and Repeated States

  • Duplicate values: The solution must properly handle duplicate values across lists - they should be preserved in the merged result
  • Identical lists: When multiple lists are identical, the merge should work correctly without overwriting or skipping elements

Boundary Conditions

  • Null list elements: Individual lists in the input array can be null - these should be safely skipped
  • Single node lists: Lists with only one element need special handling to avoid index out of bounds or reference issues
  • Large input size: With up to 10⁴ lists and 5 × 10⁴ nodes, memory usage and performance are critical

Common Implementation Mistakes

  • Not handling null inputs: Failing to check for null lists or list nodes leads to runtime exceptions
  • Incorrect pointer management: Mismanaging ListNode references when merging can cause cycles or lost nodes
  • Overwriting list structure: Modifying input lists instead of creating new nodes violates the principle of not mutating inputs
  • Incorrect merge logic: Merging two lists incorrectly (e.g., comparing wrong nodes or not advancing pointers properly) leads to incorrect results
  • Memory leaks: Not properly managing object references in large datasets can cause memory issues

Implementation Considerations

// Common mistake - not checking for null lists
ListNode[] lists = {null, list1, null, list2}; // Should skip nulls
// Correct approach - filter out null lists before processing

// Common mistake - modifying input lists
// Instead of: current.next = list1; list1 = list1.next;
// Do: new ListNode(current.val); // Create new nodes to avoid mutation

9. Alternative Approaches

There are several alternative approaches to solving the merge k sorted lists problem, each with different trade-offs:

Approach 1: Divide and Conquer Instead of using a priority queue, we can use a divide-and-conquer strategy similar to merge sort. We recursively split the k lists into halves, merge each half, and then merge the two resulting merged lists. This approach has O(n log k) time complexity where n is the total number of elements, and O(log k) space complexity due to recursion stack.

Approach 2: Simple Iteration with Min-Heap We can maintain a min-heap containing the first element of each list. At each step, we extract the minimum element and add the next element from the same list to the heap. This is essentially the same as our primary approach but with a more explicit heap management strategy.

Approach 3: Sorting All Elements Collect all elements from all lists into a single array, sort it, and convert back to a linked list. This approach has O(n log n) time complexity but is simpler to implement and understand, though it's less efficient than our chosen solution.

Trade-offs Analysis: The divide-and-conquer approach has better space complexity but more complex implementation. The simple sorting approach is easiest to understand but has worse time complexity. Our chosen solution (priority queue) provides the best balance of time efficiency O(n log k) and reasonable space complexity O(k), making it optimal for the constraints of this problem.

The priority queue approach is preferred because it maintains the sorted nature of lists throughout the process, avoiding the need to sort all elements at once. It's particularly efficient when k is much smaller than n, which is typically the case in practical scenarios.

10. Test Cases

// Test case 1: Basic functionality with multiple lists
// Input: [[1,4,5],[1,3,4],[2,6]]
// Expected output: [1,1,2,3,4,4,5,6]
@Test
public void testBasicMerge() {
    ListNode l1 = new ListNode(1, new ListNode(4, new ListNode(5)));
    ListNode l2 = new ListNode(1, new ListNode(3, new ListNode(4)));
    ListNode l3 = new ListNode(2, new ListNode(6));
    
    ListNode[] lists = {l1, l2, l3};
    ListNode result = mergeKLists(lists);
    
    // Verify the merged list
    assertEquals(1, result.val);
    assertEquals(1, result.next.val);
    assertEquals(2, result.next.next.val);
    assertEquals(3, result.next.next.next.val);
    assertEquals(4, result.next.next.next.next.val);
    assertEquals(4, result.next.next.next.next.next.val);
    assertEquals(5, result.next.next.next.next.next.next.val);
    assertEquals(6, result.next.next.next.next.next.next.next.val);
}

// Test case 2: Empty input array
// Input: []
// Expected output: null
@Test
public void testEmptyInput() {
    ListNode[] lists = {};
    assertNull(mergeKLists(lists));
}

// Test case 3: Single empty list
// Input: [[]]
// Expected output: null
@Test
public void testSingleEmptyList() {
    ListNode[] lists = {null};
    assertNull(mergeKLists(lists));
}

// Test case 4: Lists with only one element
// Input: [[1], [2], [3]]
// Expected output: [1,2,3]
@Test
public void testSingleElementLists() {
    ListNode l1 = new ListNode(1);
    ListNode l2 = new ListNode(2);
    ListNode l3 = new ListNode(3);
    
    ListNode[] lists = {l1, l2, l3};
    ListNode result = mergeKLists(lists);
    
    assertEquals(1, result.val);
    assertEquals(2, result.next.val);
    assertEquals(3, result.next.next.val);
    assertNull(result.next.next.next);
}

// Test case 5: Lists with duplicate values
// Input: [[1,2,2], [1,1,3], [2,3,4]]
// Expected output: [1,1,1,2,2,2,3,3,4]
@Test
public void testDuplicateValues() {
    ListNode l1 = new ListNode(1, new ListNode(2, new ListNode(2)));
    ListNode l2 = new ListNode(1, new ListNode(1, new ListNode(3)));
    ListNode l3 = new ListNode(2, new ListNode(3, new ListNode(4)));
    
    ListNode[] lists = {l1, l2, l3};
    ListNode result = mergeKLists(lists);
    
    assertEquals(1, result.val);
    assertEquals(1, result.next.val);
    assertEquals(1, result.next.next.val);
    assertEquals(2, result.next.next.next.val);
    assertEquals(2, result.next.next.next.next.val);
    assertEquals(2, result.next.next.next.next.next.val);
    assertEquals(3, result.next.next.next.next.next.next.val);
    assertEquals(3, result.next.next.next.next.next.next.next.val);
    assertEquals(4, result.next.next.next.next.next.next.next.next.val);
}

10. Test Cases

The test cases cover the core functionality and edge conditions for merging k sorted lists:

  1. Basic merge: Tests normal operation with multiple non-empty lists containing various elements
  2. Empty input: Validates handling of an empty array of lists (edge case that should return null)
  3. Single empty list: Tests when one or more lists are null/empty (should still return valid merged result)
  4. Single element lists: Validates merging when each list has only one node
  5. Duplicate values: Tests proper handling of duplicate elements across different lists

Each test case uses a clean, readable structure with clear assertions that verify the expected merged order. The test cases ensure that the solution handles:

  • Normal operation with mixed-length lists
  • Empty or null inputs gracefully
  • Duplicate values in the merged result
  • Proper ordering regardless of list structure

The test cases are designed to be comprehensive yet concise, focusing on the most critical behaviors without overcomplicating the verification logic.

11. Final Summary

The core insight is to use a min-heap to efficiently track and extract the smallest element across all k sorted lists, enabling us to merge them in O(N log k) time where N is the total number of elements. This approach optimally balances between maintaining sorted order and minimizing redundant comparisons by leveraging the heap's logarithmic insertion and extraction properties. The solution is efficient because it avoids the naive O(N²) approach of repeatedly scanning all lists, instead maintaining only the necessary elements in the heap structure.

Was this article helpful?

Topics in this article

More from DSA Problems

  • DSA Patterns

    Merge K Sorted Lists Algorithm Solution

    Complete Java implementation of merging k sorted linked lists using priority queue and divide-and-conquer approaches with time and space complexity analysis.

  • DSA Patterns

    Top K Frequent Elements - Java Algorithm Solution

    Complete Java implementation for finding the k most frequent elements in an array, covering multiple approaches from heap-based to bucket sort solutions.

  • DSA Problems

    Reorder List Algorithm Solution

    Complete Java implementation for reordering a singly linked list by splitting and interleaving nodes.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.