Given k sorted linked lists, merge them into one sorted list. The key insight is that you can reduce k lists to one in O(log k) rounds by repeatedly merging pairs — the same divide-and-conquer that powers merge sort.
You receive an array of k sorted linked lists. Return one sorted linked list containing all nodes. Example: [[1,4,5], [1,3,4], [2,6]] → [1,1,2,3,4,4,5,6]. You see this in database merge phases, external sort, and anywhere you must unify pre-sorted streams.
null. Single list → return it directly; no merging needed.Insert the head of every list into a min-heap keyed by node.val. Pop the minimum, attach it to the result, push that node's next (if any). Also O(N log k) — same asymptotic complexity but different constant factors. The heap approach is easier to extend to a streaming setting; the pairwise approach needs all lists up front and is usually simpler to implement correctly in an interview.
3 sorted lists. We will repeatedly merge pairs (divide & conquer) until one list remains.1// Definition for singly-linked list.2class ListNode {3 val: number;4 next: ListNode | null;5 constructor(val = 0, next: ListNode | null = null) {6 this.val = val;7 this.next = next;8 }9}1011// --- Approach 1: Divide & Conquer (matches the visualizer) ---12▶function mergeKLists(lists: Array<ListNode | null>): ListNode | null {13▶ if (lists.length === 0) return null;1415 // Repeatedly halve the list of lists, merging pairs each round16▶ let current = lists.slice();17 while (current.length > 1) {18 const next: Array<ListNode | null> = [];19 for (let i = 0; i < current.length; i += 2) {20 if (i + 1 < current.length) {21 next.push(mergeTwoLists(current[i], current[i + 1]));22 } else {23 next.push(current[i]); // odd list — carry it over24 }25 }26 current = next;27 }28 return current[0];29}3031function mergeTwoLists(32 a: ListNode | null,33 b: ListNode | null,34): ListNode | null {35 const dummy = new ListNode(0);36 let tail = dummy;37 while (a !== null && b !== null) {38 if (a.val <= b.val) { tail.next = a; a = a.next; }39 else { tail.next = b; b = b.next; }40 tail = tail.next!;41 }42 tail.next = a ?? b;43 return dummy.next;44}4546// --- Approach 2: Min-Heap (O(N log k)) ---47// import { MinHeap } from 'some-heap-library'; // or implement manually48//49// function mergeKListsHeap(lists: Array<ListNode | null>): ListNode | null {50// const heap = new MinHeap<ListNode>((a, b) => a.val - b.val);51// for (const head of lists) if (head) heap.push(head);52// const dummy = new ListNode(0);53// let tail = dummy;54// while (!heap.isEmpty()) {55// const node = heap.pop()!;56// tail.next = node;57// tail = tail.next;58// if (node.next) heap.push(node.next);59// }60// return dummy.next;61// }
// Definition for singly-linked list.
class ListNode {
val: number;
next: ListNode | null;
constructor(val = 0, next: ListNode | null = null) {
this.val = val;
this.next = next;
}
}
// --- Approach 1: Divide & Conquer (matches the visualizer) ---
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
if (lists.length === 0) return null;
// Repeatedly halve the list of lists, merging pairs each round
let current = lists.slice();
while (current.length > 1) {
const next: Array<ListNode | null> = [];
for (let i = 0; i < current.length; i += 2) {
if (i + 1 < current.length) {
next.push(mergeTwoLists(current[i], current[i + 1]));
} else {
next.push(current[i]); // odd list — carry it over
}
}
current = next;
}
return current[0];
}
function mergeTwoLists(
a: ListNode | null,
b: ListNode | null,
): ListNode | null {
const dummy = new ListNode(0);
let tail = dummy;
while (a !== null && b !== null) {
if (a.val <= b.val) { tail.next = a; a = a.next; }
else { tail.next = b; b = b.next; }
tail = tail.next!;
}
tail.next = a ?? b;
return dummy.next;
}
// --- Approach 2: Min-Heap (O(N log k)) ---
// import { MinHeap } from 'some-heap-library'; // or implement manually
//
// function mergeKListsHeap(lists: Array<ListNode | null>): ListNode | null {
// const heap = new MinHeap<ListNode>((a, b) => a.val - b.val);
// for (const head of lists) if (head) heap.push(head);
// const dummy = new ListNode(0);
// let tail = dummy;
// while (!heap.isEmpty()) {
// const node = heap.pop()!;
// tail.next = node;
// tail = tail.next;
// if (node.next) heap.push(node.next);
// }
// return dummy.next;
// }null; a single list is already merged. These let the main loop assume ≥ 2 lists exist at the start of each iteration.current holds the working list of lists. Each iteration walks it in steps of 2, merging adjacent pairs into next. An odd-indexed tail element (i + 1 >= current.length) is carried forward without merging — it will find a partner in the next round.dummy head eliminates the special-case for the first node. tail.next = a ?? b appends whichever list still has nodes. O(m + n) time, O(1) extra space.mergeTwoLists inner loop looks like it might be O(N²) but each node is touched in exactly one merge per round, and there are log k rounds, giving O(N log k) total.// Definition for singly-linked list.
class ListNode {
val: number;
next: ListNode | null;
constructor(val = 0, next: ListNode | null = null) {
this.val = val;
this.next = next;
}
}
// --- Approach 1: Divide & Conquer (matches the visualizer) ---
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
if (lists.length === 0) return null;
// Repeatedly halve the list of lists, merging pairs each round
let current = lists.slice();
while (current.length > 1) {
const next: Array<ListNode | null> = [];
for (let i = 0; i < current.length; i += 2) {
if (i + 1 < current.length) {
next.push(mergeTwoLists(current[i], current[i + 1]));
} else {
next.push(current[i]); // odd list — carry it over
}
}
current = next;
}
return current[0];
}
function mergeTwoLists(
a: ListNode | null,
b: ListNode | null,
): ListNode | null {
const dummy = new ListNode(0);
let tail = dummy;
while (a !== null && b !== null) {
if (a.val <= b.val) { tail.next = a; a = a.next; }
else { tail.next = b; b = b.next; }
tail = tail.next!;
}
tail.next = a ?? b;
return dummy.next;
}
// --- Approach 2: Min-Heap (O(N log k)) ---
// import { MinHeap } from 'some-heap-library'; // or implement manually
//
// function mergeKListsHeap(lists: Array<ListNode | null>): ListNode | null {
// const heap = new MinHeap<ListNode>((a, b) => a.val - b.val);
// for (const head of lists) if (head) heap.push(head);
// const dummy = new ListNode(0);
// let tail = dummy;
// while (!heap.isEmpty()) {
// const node = heap.pop()!;
// tail.next = node;
// tail = tail.next;
// if (node.next) heap.push(node.next);
// }
// return dummy.next;
// }mergeTwoLists inner loop looks like it might be O(N²) but each node is touched in exactly one merge per round, and there are log k rounds, giving O(N log k) total.Matches divide & conquer asymptotically and is the natural fit when lists arrive as a stream — keep a heap of the current front of each list and always emit the global minimum.
// A small binary min-heap keyed by node.val.
class NodeHeap {
private data: ListNode[] = [];
get size(): number {
return this.data.length;
}
push(node: ListNode): void {
this.data.push(node);
let i = this.data.length - 1;
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.data[parent].val <= this.data[i].val) break;
[this.data[parent], this.data[i]] = [this.data[i], this.data[parent]];
i = parent;
}
}
pop(): ListNode {
const top = this.data[0];
const last = this.data.pop()!;
if (this.data.length > 0) {
this.data[0] = last;
let i = 0;
const n = this.data.length;
while (true) {
const left = 2 * i + 1;
const right = 2 * i + 2;
let smallest = i;
if (left < n && this.data[left].val < this.data[smallest].val) smallest = left;
if (right < n && this.data[right].val < this.data[smallest].val) smallest = right;
if (smallest === i) break;
[this.data[smallest], this.data[i]] = [this.data[i], this.data[smallest]];
i = smallest;
}
}
return top;
}
}
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
const heap = new NodeHeap();
for (const head of lists) {
if (head !== null) heap.push(head);
}
const dummy = new ListNode(0);
let tail = dummy;
while (heap.size > 0) {
const node = heap.pop();
tail.next = node;
tail = node;
if (node.next !== null) heap.push(node.next);
}
return dummy.next;
}The simplest correct approach: fold the lists one at a time into a single running result with the same two-list merge. Easy to write, but the accumulator is re-walked every fold.
function mergeTwoLists(
a: ListNode | null,
b: ListNode | null,
): ListNode | null {
const dummy = new ListNode(0);
let tail = dummy;
while (a !== null && b !== null) {
if (a.val <= b.val) { tail.next = a; a = a.next; }
else { tail.next = b; b = b.next; }
tail = tail.next!;
}
tail.next = a ?? b;
return dummy.next;
}
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
let result: ListNode | null = null;
for (const head of lists) {
result = mergeTwoLists(result, head);
}
return result;
}Ignore the fact that the inputs are sorted: dump every value into an array, sort it, then build a fresh list. Trivially correct but throws away the pre-sorted structure.
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
const values: number[] = [];
for (const head of lists) {
let node = head;
while (node !== null) {
values.push(node.val);
node = node.next;
}
}
values.sort((x, y) => x - y);
const dummy = new ListNode(0);
let tail = dummy;
for (const v of values) {
tail.next = new ListNode(v);
tail = tail.next;
}
return dummy.next;
}| merge k sorted lists / streams | pairwise divide & conquer or min-heap |
| O(N log k) over O(N·k) | pair-merge rounds (like merge sort) |
| streaming / online merge of k sources | min-heap of size k |
| dummy head node in linked list | sentinel for clean tail attachment |
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
if (lists.length === 0) return null;
let current = lists.slice();
while (current.length > 1) {
const next: Array<ListNode | null> = [];
for (let i = 0; i < current.length; i += 2) {
next.push(i + 1 < current.length
? mergeTwoLists(current[i], current[i + 1])
: current[i]);
}
current = next;
}
return current[0];
}mergeKLists([[1,4,5],[1,3,4],[2,6]]). What is the output?