Combine Sorted Linked Lists

  • Given k linked lists, each sorted ascending, combine them in one sorted linked list

The point:

  • Use a min-heap
  • If the comparaison operator of the Node is redefined, this impacts the comparaisons in the min-heap (good) but also the rest of the application (may be bad)

Complexity :

Time Space
O(n + log(k)) O(k)
  • Time : O(n + log(k)). O(k log(k)) to create le heap (k nodes inserted one by one). Then for all n nodes we push and pop in O(log(k)). O(k log(k)) + n O(log(k)) = O(n log(k))
  • Space : O(k) because the heap stores up to one node of each k lists

About Rust :

  • BinaryHeap is a max-heap by default. It sorts elements so that the largest is at the top.
  • Here we want to extract the smallest element first, which corresponds to a min-heap.
  • So, for BinaryHeap to function as a min-heap, the order of comparison must be reversed.

  • YES : tested on the Rust Playground

First version

  • Behaves like the code in the book
  • ATTENTION comparaisons between nodes are reversed
use std::cmp::Ordering;
use std::collections::BinaryHeap;

type Link = Option<Box<Node>>; // type alias. Use Option and Box to allow an optional pointer to the next node 

#[derive (Debug)]
struct Node {
    val: i32,
    next: Link, // use the alias here for clarity
}

impl Node {
    fn new(val: i32, next: Link) -> Self {
        Self { val, next }
    }
}

// Implement comparison traits for Node so it can be used in a BinaryHeap
impl Eq for Node {}

impl PartialEq for Node {
    fn eq(&self, other: &Self) -> bool {
        self.val == other.val
    }
}

impl Ord for Node {
    fn cmp(&self, other: &Self) -> Ordering {
        // Reverse order for min-heap behavior
        other.val.cmp(&self.val)
    }
}

// Ord implies PartialOrd, so we must implement PartialOrd if we implement Ord
// Implement PartialOrd to allow comparison using <, >, etc.
impl PartialOrd for Node {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

fn combined_sorted_linked_lists(mut lists: Vec<Link>) -> Link {
    let mut heap = BinaryHeap::new();

    // Initialize heap with head of each list
    for list in lists.iter_mut() {
        if let Some(node) = list.take() {
            heap.push(node);
        }
    }

    // Dummy node to start building the result
    let mut dummy = Box::new(Node::new(-1, None));
    let mut tail = &mut dummy;

    // While the heap is not empty, extract the smallest node
    while let Some(mut node) = heap.pop() {
        // Append node to result list
        tail.next = Some(Box::new(Node::new(node.val, None)));
        tail = tail.next.as_mut().unwrap();

        // Push next node into heap if it exists
        if let Some(next_node) = node.next.take() {
            heap.push(next_node);
        }
    }

    dummy.next
}

fn main(){   // no main() if this code runs in a Jupyter cell 

    fn create_list(vals : &[i32])->Link{
        let mut head = None;
        // for v in vals.into_iter().rev() {    // with .into_iter() vals would be lost
        for v in vals.iter().rev() {            // .iter() returns &T
                head = Some(Box::new(Node::new(*v, head)));
            }
        head
    }

    let lists = vec![
        create_list(&[1, 6]),
        create_list(&[1, 4, 6]),
        create_list(&[3, 7])
    ];

    let head_combined = combined_sorted_linked_lists(lists);

    let mut current = head_combined.as_ref();
    while let Some(node) = current {
        print!("{}->", node.val);
        current = node.next.as_ref();
    }
    println!("EOL"); // End of List 1->1->3->4->6->6->7->EOL
    
    
    // Understand the comparaison between nodes is reversed
    let n1 = Node::new(1, None);
    let n2 = Node::new(2, None);

    println!("\nn1 = {:?}, n2 = {:?}", n1, n2); // n1 = Node { val: 1, next: None }, n2 = Node { val: 2, next: None }
    
    // Because of reversed comparison, n1 > n2
    println!("n1 < n2: {}", n1 < n2);           // false
    println!("n1 > n2: {}", n1 > n2);           // true

} // end of local scope OR end of main()       

Version with an HeapNode

  • We may not want to apply the reversed comparaison to the Node because the need is only within the heap.
  • In a larger application if we compare Node we may want to do the comparaison the right way, not the reversed way
  • We can create a HeapNode with a specific reversed compare which will be used only in the heap
  • Preferred solution?
use std::cmp::Ordering;
use std::collections::BinaryHeap;

type Link = Option<Box<Node>>; // type alias. Use Option and Box to allow an optional pointer to the next node 

struct Node {
    val: i32,
    next: Link, // use the alias here for clarity
}

impl Node {
    fn new(val: i32, next: Link) -> Self {
        Self { val, next }
    }
}

// HeapNode wraps a node and inverts the order for use in a min-heap
// #[derive(Eq, PartialEq)] // BinaryHeap => Eq, PartialEq, Ord, PartialOrd must be implemented
                            // Here, we CANNOT leverage Eq and PartialEq which compare all fields (link included)
struct HeapNode {
    val: i32,
    node: Link, // Using the alias (Option<Box<Node>>)
}

impl Eq for HeapNode {}

// Eq implies PartialEq, so we must implement PartialEq if we implement Eq
impl PartialEq for HeapNode {
    fn eq(&self, other: &Self) -> bool {
        self.val == other.val
    }
}

impl Ord for HeapNode {
    fn cmp(&self, other: &Self) -> Ordering {
        // Reverse order for min-heap behavior
        other.val.cmp(&self.val)
    }
}

// Ord implies PartialOrd, so we must implement PartialOrd if we implement Ord
// Implement PartialOrd to allow comparison using <, >, etc.
impl PartialOrd for HeapNode {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        // Reverse order for min-heap behavior
        Some(other.val.cmp(&self.val))
    }
}

fn combined_sorted_linked_lists(mut lists: Vec<Link>) -> Link {
    let mut heap = BinaryHeap::new();

    // Initialize heap with head of each list
    for list in lists.iter_mut() {
        if let Some(node) = list.take() {
            heap.push(HeapNode {
                val: node.val,
                node: Some(node),
            });
        }
    }

    // Dummy node to start building the result
    let mut dummy = Box::new(Node::new(-1, None));
    let mut tail = &mut dummy;

    // While the heap is not empty, extract the smallest node
    while let Some(mut heap_node) = heap.pop() {
        if let Some(mut node) = heap_node.node.take() {
            // Append node to result list
            tail.next = Some(Box::new(Node::new(node.val, None)));
            tail = tail.next.as_mut().unwrap();

            // Push next node into heap if it exists
            if let Some(next_node) = node.next.take() {
                heap.push(HeapNode {
                    val: next_node.val,
                    node: Some(next_node),
                });
            }
        }
    }
    dummy.next
}

fn main(){     // no main() if this code runs in a Jupyter cell 

    fn create_list(vals : &[i32])->Link{
        let mut head = None;
        // for v in vals.into_iter().rev() {    // with .into_iter() vals would be lost
        for v in vals.iter().rev() {            // .iter() returns &T
                head = Some(Box::new(Node::new(*v, head)));
            }
        head
    }

    let lists = vec![
        create_list(&[1, 6]),
        create_list(&[1, 4, 6]),
        create_list(&[3, 7])
    ];

    let head_combined = combined_sorted_linked_lists(lists);

    let mut current = head_combined.as_ref();
    while let Some(node) = current {
        print!("{}->", node.val);
        current = node.next.as_ref();
    }
    print!("EOL"); // End of List   1->1->3->4->6->6->7->EOL
} // end of local scope OR end of main()       

Back to top

Published on: Jun 22 2025 at 09:00 AM | Last updated: Jun 22 2025 at 09:00 AM

Copyright © 1964-2025 - 40tude