lotus

贵有恒何必三更眠五更起 最无益只怕一日曝十日寒

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

1. 题目

 

https://leetcode.cn/problems/merge-k-sorted-lists/

 

考察点

 Leetcode 23 是一个关于合并 k 个有序链表的问题。它考察的是排序、分治和合并排序的知识。

你需要使用一个优先队列或者一个最小堆来维护 k 个链表的当前最小元素,

然后每次从中取出最小的一个,加入到结果链表中,再把它所在的链表的下一个元素放入优先队列或者最小堆中,直到所有的链表都为空为止。

这样可以保证时间复杂度为 O(nlogk),其中 n 是所有链表的总节点数,k 是链表的个数

2. 解法

有两种解法

  • 使用优先队列,也叫最小堆
  • 使用分治法

 

使用优先队列,也叫最小堆

思路

  • 我想要把k个有序的链表合并成一个有序的链表,那么我需要每次从k个链表中找出最小的节点,加入到结果链表中,直到所有链表都为空。
  • 为了方便地找出最小的节点,我想到了使用优先队列这种数据结构,它可以让我快速地取出最小的元素,并且可以动态地更新队列中的元素。
  • 我把每个链表的头节点放入优先队列中,然后每次从队列中取出最小的节点,加入到结果链表中,然后把该节点的下一个节点放入队列中,直到队列为空。
  • 这样,我就可以保证结果链表是有序的,并且时间复杂度和空间复杂度都比较低。

代码逻辑

代码的逻辑是这样的:

  • 首先,判断输入的链表数组是否为空,如果为空,直接返回null。
  • 然后,创建一个优先队列,这是一种特殊的数据结构,它可以按照一定的规则(比如节点的值)来排序队列中的元素,并且可以快速地取出最小(或最大)的元素。我们需要传入一个比较器(Comparator)来定义排序的规则,这里我们让节点的值越小,优先级越高。
  • 接着,遍历输入的链表数组,把每个链表的头节点(如果不为空)放入优先队列中。这样,队列中就有k个节点,分别是k个链表的第一个节点。
  • 然后,创建一个虚拟头节点和一个指针,用来构造结果链表。虚拟头节点是一个没有实际意义的节点,它只是为了方便操作结果链表,它的下一个节点才是真正的结果链表的头节点。指针是用来遍历结果链表的,它始终指向结果链表的最后一个节点。
  • 接着,当优先队列不为空时,重复以下操作:
    • 从优先队列中取出最小的节点,这个节点就是当前k个链表中最小的节点。把这个节点加入到结果链表中,也就是让指针的下一个节点指向这个节点,并且更新指针。
    • 如果这个节点有下一个节点,说明它所在的链表还没有遍历完,那么就把它的下一个节点放入优先队列中。这样,队列中又有k个节点,分别是k个链表中当前最小的节点。
  • 最后,当优先队列为空时,说明所有链表都遍历完了,结果链表也构造完了。返回虚拟头节点的下一个节点,就是结果链表的头节点。

具体实现

 

public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) return null;
    // 创建一个优先队列,按照节点的值从小到大排序
    PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length, new Comparator<ListNode>() {
        @Override
        public int compare(ListNode o1, ListNode o2) {
            return o1.val - o2.val;
        }
    });
    // 把每个链表的头节点入队
    for (ListNode node : lists) {
        if (node != null) queue.offer(node);
    }
    // 创建一个虚拟头节点和一个指针
    ListNode dummy = new ListNode(0);
    ListNode p = dummy;
    // 当队列不为空时,循环操作
    while (!queue.isEmpty()) {
        // 取出队列中的最小节点,加入到结果链表中
        ListNode node = queue.poll();
        p.next = node;
        p = p.next;
        // 如果该节点有下一个节点,把下一个节点入队
        if (node.next != null) queue.offer(node.next);
    }
    // 返回结果链表的头节点
    return dummy.next;
}

  

 

 

使用分治法

思路

另一种解法是使用分治法,也就是把k个链表分成两半,然后递归地合并每一半,最后再合并两个已经有序的链表。这样可以减少比较的次数,时间复杂度是O(nlogk),空间复杂度是O(logk),其中n是所有链表的节点总数,k是链表的个数。

代码逻辑

以上代码的逻辑是使用分治法来合并k个有序的链表。具体的步骤是:

  • 首先,判断输入的链表数组是否为空,如果为空,直接返回null。
  • 然后,调用一个辅助函数,传入链表数组和左右边界,表示要合并的链表范围。
  • 在辅助函数中,如果左右相等,说明只有一个链表,直接返回该链表;如果左边大于右边,说明没有链表,返回null。
  • 然后,计算中间位置,把链表数组分成两半,递归地调用辅助函数来合并每一半的链表,得到两个有序的链表。
  • 最后,调用另一个辅助函数,传入两个有序的链表,用一个虚拟头节点和一个指针来构造结果链表。每次比较两个链表的当前节点,取出较小的节点,加入到结果链表中,并且更新指针和链表。如果有一个链表为空,把另一个链表的剩余部分加入到结果链表中。返回虚拟头节点的下一个节点,就是结果链表的头节点。

具体实现

public ListNode mergeKLists(ListNode[] lists) {
    if (lists == null || lists.length == 0) return null;
    return merge(lists, 0, lists.length - 1);
}

// 分治法,把k个链表分成两半,递归地合并每一半,最后再合并两个有序的链表
private ListNode merge(ListNode[] lists, int left, int right) {
    // 如果左右相等,说明只有一个链表,直接返回
    if (left == right) return lists[left];
    // 如果左边大于右边,说明没有链表,返回null
    if (left > right) return null;
    // 计算中间位置
    int mid = left + (right - left) / 2;
    // 递归地合并左半部分的链表
    ListNode l1 = merge(lists, left, mid);
    // 递归地合并右半部分的链表
    ListNode l2 = merge(lists, mid + 1, right);
    // 合并两个有序的链表
    return mergeTwoLists(l1, l2);
}

// 合并两个有序的链表
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    // 创建一个虚拟头节点和一个指针
    ListNode dummy = new ListNode(0);
    ListNode p = dummy;
    // 当两个链表都不为空时,循环操作
    while (l1 != null && l2 != null) {
        // 比较两个链表的当前节点,取出较小的节点,加入到结果链表中
        if (l1.val < l2.val) {
            p.next = l1;
            l1 = l1.next;
        } else {
            p.next = l2;
            l2 = l2.next;
        }
        p = p.next;
    }
    // 如果有一个链表为空,把另一个链表的剩余部分加入到结果链表中
    if (l1 != null) p.next = l1;
    if (l2 != null) p.next = l2;
    // 返回结果链表的头节点
    return dummy.next;
}

 

 

两种解法对比

这个问题没有一个确定的答案,不同的解法有不同的优缺点。在这个情况下,一个人可能会说:

  • 如果k很大,那么优先队列的解法可能更好,因为它可以减少空间复杂度,只需要存储k个节点,而不是logk层递归调用的栈空间。
  • 如果k很小,那么分治法的解法可能更好,因为它可以减少时间复杂度,只需要比较nlogk次,而不是nlogk*k次。
  • 如果链表的长度不均匀,那么分治法的解法可能更好,因为它可以平衡地合并链表,而不是每次都从最长的链表中取出节点。
  • 如果链表的长度比较均匀,那么优先队列的解法可能更好,因为它可以避免多余的比较,只需要比较队列中的k个节点。

所以,最好的解法取决于具体的情况和需求。你可以根据你的偏好和判断来选择合适的解法

 

3. 总结

posted on 2023-04-18 16:36  白露~  阅读(36)  评论(0编辑  收藏  举报