【面试经典 150 | 分治】合并 K 个升序链表

写在前面

本专栏专注于分析与讲解【面试经典150】算法,两到三天更新一篇文章,欢迎催更……

专栏内容以分析题目为主,并附带一些对于本题涉及到的数据结构等内容进行回顾与总结,文章结构大致如下,部分内容会有增删:

  • Tag:介绍本题牵涉到的知识点、数据结构;
  • 题目来源:贴上题目的链接,方便大家查找题目并完成练习;
  • 题目解读:复述题目(确保自己真的理解题目意思),并强调一些题目重点信息;
  • 解题思路:介绍一些解题思路,每种解题思路包括思路讲解、实现代码以及复杂度分析;
  • 知识回忆:针对今天介绍的题目中的重点内容、数据结构进行回顾总结。

Tag

【链表】【分治】【优先队列】


题目来源

23. 合并 K 个升序链表


解题思路

在开始解题之前,需要先掌握 合并两个有序链表

方法一:顺序合并

所谓的顺序合并就是将链表数组中的两个链表依次合并。思路比较简单清晰,直接看答案。

代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* merge2Lists(ListNode* list1, ListNode* list2){
        ListNode* dummy = new ListNode(-1);
        ListNode* prev = dummy;

        while(list1 && list2){
            if(list1->val < list2->val){
                prev->next = list1;
                list1 = list1->next;
            }
            else{
                prev->next = list2;
                list2 = list2->next;
            }
            prev = prev->next;
        }
        prev->next = list1 ? list1 : list2;

        return dummy->next;
    }
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        ListNode* ans = nullptr;
        int i;
        for(i = 0; i < lists.size(); ++i){
            ans = merge2Lists(ans, lists[i]);
        }
        return ans;
    }
};

复杂度分析

时间复杂度:渐进的时间复杂度为 O ( k 2 n ) O(k^2n) O(k2n)分析

空间复杂度: O ( 1 ) O(1) O(1)

方法二:分治合并

用分治的方法进行合并,步骤如下:

  • k 个链表配对并将同一对中的链表合并,
  • 第一轮合并以后, k 个链表被合并成了 k 2 \frac{k}{2} 2k 个链表,然后是 k 4 \frac{k}{4} 4k 个链表, k 8 \frac{k}{8} 8k 个链表等等;
  • 重复这一过程,直到我们得到了最终的有序链表。

代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    // 递归合并两个有序链表
    ListNode* merge2Lists(ListNode* a, ListNode* b) {
        if (a == nullptr) {
            return b;
        }
        else if (b == nullptr) {
            return a;
        }
        else {
            if (a->val < b->val) {
                a->next = merge2Lists(a->next, b);
                return a;
            }
            else {
                b->next = merge2Lists(a, b->next);
                return b;
            }
        }
        return nullptr;         // 到不了这里
    }


    ListNode* merge(vector<ListNode*>& lists, int l, int r) {
        if (l == r) {
            return lists[l];
        }
        if (l > r) {
            return nullptr;
        } 

        int mid = l + ((r - l) >> 1);
        return merge2Lists(merge(lists, l, mid), merge(lists, mid+1, r));
    }

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return merge(lists, 0, lists.size() - 1);
    }
};

复杂度分析

时间复杂度:渐进的时间复杂度为 O ( k n × l o g k ) O(kn \times logk) O(kn×logk)分析

空间复杂度:递归会使用到 O ( log ⁡ k ) O(\log k) O(logk) 空间代价的栈空间。

方法三:使用优先队列合并

思路

维护一个优先队列(小顶堆),队列中存放的是 pair<int, ListNode*> 其中第一个元素表示的是节点的值,第二个元素表示该节点,优先队列是关于 节点的值 的优先队列。优先队列中也可以放置封装了节点的值和节点的结构体。小顶堆可以保证每次出队的都是队列中的最小元素,然后将出队的最小元素连接到链表中。

代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    struct Status {
        int val;
        ListNode *node;
        bool operator < (const Status& rhs) const {
            return val > rhs.val;
        }
    };
    
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        priority_queue<Status> que;
        for (auto node : lists) {
            if (node) {
                que.push({node->val, node});
            }
        }

        ListNode head, *tail = &head;           // 见笔记中代码理解

        while (!que.empty()) {
            auto f = que.top(); que.pop();
            tail->next = f.node;
            tail = tail->next;
            if (f.node->next) {
                que.push({f.node->next->val, f.node->next});
            }
        }
        return head.next;
    }
};

理解 ListNode head, *tail = &head;

head 初始化为一个空节点,最后 head.next 指向的是合并后链表的头结点。通过 tail 指针来帮助 head.next 指向合并后链表的头节点。

tail 是一个指针,初始化指向的是 head。在第一次为 tail 建立后继节点时,就将捆绑的 head 也指向了一个节点(就是合并链表后的头节点),接着通过 tail = tail->next 操作将 tail 指向了新的节点,也就切断了 tailhead 的联系。最后返回合并后的链表头节点为 head->next

复杂度分析

时间复杂度:考虑优先队列中的元素不超过 k 个,那么插入和删除的时间代价为 O ( l o g ⁡ k ) O(log⁡k) O(logk),这里最多有 kn 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为 O ( k n × log ⁡ k ) O(kn \times \log k) O(kn×logk)

空间复杂度:这里用了优先队列,优先队列中的元素不超过 k 个,故渐进空间复杂度为 O ( k ) O(k) O(k)


写在最后

如果您发现文章有任何错误或者对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家有更优的时间、空间复杂度的方法,欢迎评论区交流。

最后,感谢您的阅读,如果有所收获的话可以给我点一个 👍 哦。

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要实现合并K个升序链表,我们可以考虑使用分治法,将K个链表划分为两个子问题,分别合并这两个子问题,然后不断递归下去。 具体实现过程如下: 1. 将K个链表按照长度平均划分为两个子问题,每个子问题递归调用合并函数,直到只剩下一个链表或两个链表。 2. 合并两个链表的过程可以使用归并排序中的合并函数,将两个链表合并为一个升序链表。 3. 将合并后的链表返回,然后递归回去继续合并两个子问题的结果。 Java代码实现如下: ``` public ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) { return null; } return mergeKLists(lists, 0, lists.length - 1); } private ListNode mergeKLists(ListNode[] lists, int left, int right) { if (left == right) { return lists[left]; } int mid = (left + right) / 2; ListNode l1 = mergeKLists(lists, left, mid); ListNode l2 = mergeKLists(lists, mid + 1, right); return mergeTwoLists(l1, l2); } private ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) { return l2; } if (l2 == null) { return l1; } if (l1.val < l2.val) { l1.next = mergeTwoLists(l1.next, l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } } ``` 其中,`mergeKLists` 函数是递归调用的入口函数,它接收一个 `ListNode` 数组作为参数,表示要合并的K个链表。在函数中,我们首先判断链表数组是否为空或长度为0,如果是,则返回 `null`。否则,我们调用 `mergeKLists` 函数,将链表数组划分为两个子问题,然后递归调用 `mergeKLists` 函数,继续划分子问题,直到只剩下一个链表或两个链表。 在 `mergeKLists` 函数中,我们使用归并排序的思想,将两个链表合并为一个升序链表。具体实现是在 `mergeTwoLists` 函数中,它接收两个链表作为参数,递归调用自身,将两个链表合并为一个升序链表
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wang_nn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值