Q45- code23- 合并 K 个升序链表
实现思路
1 方法1:最小堆/优先队列
- 创建最小堆,并依次把每个链表的头节点入堆
- 创建dummy 和 cur
- 依次remove堆顶头节点minNode,并把它的next入堆,堆内部会自动处理它的正确位置 + cur连接每次的minNode
- 知道 最小堆变成空,说明所有节点都被处理完成
2 dfs分治递归
- 把 合并N个有序链表,拆分为 合并左右2部分有序链表
- 左右2部分,又可以继续左右拆分,直到是只有1个链表...
- 然后把 左右2个有序链表合并起来,直到所有链表有序
3 方法3- 分治-循环写法
S1 step含义:每组火车车厢 需要合并的车厢节数
S2 i += step * 2 原因:
- step:1 (0, 1), (2, 3), (4,5) ...
- stpe:2 (0[1] , 2[3]), (4[5], 6[7])...
- step:4 (0[1,2,3], 4[5,6,7]), (8[9,10,11], 12[13,14,15]) ...
- 根本原因是 车厢是两两合并的 (a, b), (c,d)
- 由于 a和b都占有step个车厢,所以想要到达c,就需要是 i + step * 2
S3 i < len - step 原因:
- 为了确保当前位置 i对应的车厢a, 后面还有一个可以配对的车厢b
- 因为 b的索引是 i + step,必须保证它是没有越界的,即 i + step < len
- 转换一下就是 i < len - step
参考文档
代码实现
1 方法1: 最小堆/优先队列
- 时间复杂度:O(Llogm); m 为 lists 的长度,L 为所有链表的长度之和
- 空间复杂度:O(m); 堆中至多有 m 个元素
class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
// 构建最小堆
const minHeap = new MinHeap<ListNode>((a, b) => a.val < b.val);
for (const head of lists) {
// 易错点1:要保证head不为null,才能加入到堆中
if (head) {
minHeap.add(head);
}
}
// 构建虚拟头节点
let dummy = new ListNode(), cur = dummy;
// 思维难点:循环直到堆为空:依次弹出堆顶元素,并加入next,到堆内自动排序
while (!minHeap.isEmpty()) {
const minHead = minHeap.remove();
if (minHead && minHead.next) {
minHeap.add(minHead.next);
}
cur.next = minHead;
cur = cur.next;
}
return dummy.next;
}
class MinHeap<T> {
private heap: T[] = [];
private compare: (a: T, b: T) => boolean;
constructor(compare: (a: T, b: T) => boolean) {
this.compare = compare;
}
private getParentIdx(i: number) {
if (i <= 0) {
throw new Error("index违法");
}
return ~~((i - 1) / 2);
}
private getLeftIdx(i: number) {
if (i < 0) {
throw new Error("index 非法");
}
return 2 * i + 1;
}
private getRightIdx(i: number) {
if (i < 0) {
throw new Error("index 非法");
}
return 2 * i + 2;
}
private swap(i: number, j: number) {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
}
private siftUp(i: number) {
while (i > 0) {
const parentIdx = this.getParentIdx(i);
// 定义:compare(a, b) 返回 true 表示: a < b
// 如果当前节点比父节点小,才会上浮;如果父节点已经是较小的,则停止上浮
const parentIsSmall = this.compare(this.heap[parentIdx], this.heap[i]);
if (parentIsSmall) break;
this.swap(i, parentIdx);
i = parentIdx;
}
}
private siftDown(i: number) {
while (1) {
// 定义:compare(a, b) 返回 true 表示: a < b
// 如果当前节点比子节点大,才会下沉
let swapIdx = i;
const ldx = this.getLeftIdx(i);
const rdx = this.getRightIdx(i);
if (
ldx < this.getSize() &&
this.compare(this.heap[ldx], this.heap[swapIdx])
)
swapIdx = ldx;
if (
rdx < this.getSize() &&
this.compare(this.heap[rdx], this.heap[swapIdx])
)
swapIdx = rdx;
if (swapIdx === i) break;
this.swap(i, swapIdx);
i = swapIdx;
}
}
public getSize() {
return this.heap.length;
}
public isEmpty() {
return this.heap.length === 0;
}
public peek() {
if (this.isEmpty()) {
throw new Error("堆为空");
}
return this.heap[0];
}
public add(node: T) {
this.heap.push(node);
this.siftUp(this.heap.length - 1);
}
public remove() {
if (this.isEmpty()) {
throw new Error("堆为空");
}
const ret = this.peek();
this.swap(0, this.getSize() - 1);
this.heap.pop();
this.siftDown(0);
return ret;
}
}
2 方法2- dfs分治递归
-
时间复杂度:O(Llogm); m 为 lists 的长度,L 为所有链表的长度之和
- 每个节点参与链表合并的次数为 O(logm) 次,
- 一共有 L 个节点,所以总的时间复杂度为 O(Llogm)
-
空间复杂度:O(m); 递归深度为 O(logm),需要 O(logm) 的栈空间
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
if (!lists.length) return null;
return dfs(lists, 0, lists.length - 1);
}
function dfs(lists: Array<ListNode | null>, l: number, r: number): ListNode | null {
if (l === r) return lists[l];
const mid = l + ((r - l) >> 1);
const linkL = dfs(lists, l, mid);
const linkR = dfs(lists, mid + 1, r);
return merge2Link(linkL, linkR);
}
function merge2Link(link1: ListNode | null, link2: ListNode | null) {
let dummy = new ListNode(), cur = dummy;
while (link1 && link2) {
if (link1.val < link2.val) {
cur.next = link1;
link1 = link1.next;
} else {
cur.next = link2;
link2 = link2.next;
}
cur = cur.next;
}
if (link1 || link2) cur.next = link1 || link2;
return dummy.next;
}
3 方法3- 分治-循环写法
-
时间复杂度:O(Llogm); m 为 lists 的长度,L 为所有链表的长度之和
- 外层关于step 的循环是 O(logm) 次
- 内层相当于把每个链表节点都遍历了一遍,是 O(L) 的
-
空间复杂度:O(logm); 需要 O(logm) 的栈空间
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
if (!lists || !lists.length) return null;
const len = lists.length;
// 每轮 依次合并的车厢节数(a,b), (c,d) 分别是 1/2/4/8......
for (let step = 1; step < len; step *= 2) {
// 在每轮里,依次从第0节车厢开始,按step长度进行(a, b), (c,d) 合并
// 注意点1:需要 b的idx是合法的
// 注意点2:注意 (a,b)的长度都是step, 所以c的idx是 i+= step * 2
for (let i = 0; i < len - step; i += step * 2) {
lists[i] = merge2List(lists[i], lists[i + step]);
}
}
return lists[0];
}
// 合并2个有序链表,返回合并后的链表
function merge2List(l1: ListNode | null, l2: ListNode | null): ListNode | null {
if (!l1 || !l2) return l1 || l2;
if (l1.val < l2.val) {
l1.next = merge2List(l1.next, l2);
return l1;
} else {
l2.next = merge2List(l2.next, l1);
return l2;
}
}