Q45- code23- 合并 K 个升序链表

66 阅读4分钟

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

参考文档

01- 直接参考文档

代码实现

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;
  }
}