【icyle】Leetcode-cn:23. 合并K个升序链表

题目

解答1:两个两个合并

思路

最简单直接的方法,不解释了。

代码
/*
 * @lc app=leetcode.cn id=23 lang=cpp
 *
 * [23] 合并K个升序链表
 */

// @lc code=start
/**
 * 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) {}
 * };
 */
#include <vector>
using namespace std;
class Solution
{
public:
    ListNode *mergeKLists(vector<ListNode *> &lists)
    {
        int listslength = lists.size();
        ListNode *reslist = nullptr;
        for (int i = 0; i < listslength; i++)
        {
            reslist = mergeTwoLists(reslist, lists[i]);
        }
        return reslist;
    }

    //这个方法可以照搬 21.合成两个有序链表 来使用
    ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
    {
        //一旦有哪个链表元素遍历完了,直接返回另外一个链表即可
        if (l1 == nullptr || l2 == nullptr)
        {
            return l1 ? l1 : l2;
        }
        //l1比l2小,所以下一个元素应该从 "l1->next" 和 "l2" 这两个结点开头的两个链表里选
        //意思很明显了,就是l1要顺位指向下一个结点
        //else同理
        else if (l1->val < l2->val)
        {
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        }
        else
        {
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
        //最后不需要再写return,上面两端if的判断说明:如果l1->val < l2->val,最后一定是执行return l1
        //否则,最后一定是执行return l2,均符合题目要求(小的结点当头头)
    }
};
// @lc code=end
时间复杂度和空间复杂度(官方答案,说的很好)
  • 时间复杂度: O(k2n) 。假设每个链表的最长长度均为 n。
    在第一次合并后,reslist的长度为 n
    第二次合并后,reslist的长度为2n
    第 i 次合并后,reslist的长度为 in。
    由此,可以推出:第 i 次合并的时间复杂度(第i-1个reslist和第i个链表合并):O(n+(i-1)*n)
    最后,合并k个链表意味着需要进行k-1次两两合并:O( ∑ i = 1 k − 1 i × n \sum_{i=1}^{k-1}i\times n i=1k1i×n) = O( k ( k − 1 ) 2 × n \frac{k(k-1)}{2}\times n 2k(k1)×n) = O(k2n)
    由于实际中每个链表最长长度均不超过n,所以可以归纳出以上的时间复杂度。
  • 空间复杂度:O(1)

解答2:分治法

思路

分治法是对解法1的优化,使用了归并排序的思想,思考一下是不是很像一颗倒置二叉树。

代码
/*
 * @lc app=leetcode.cn id=23 lang=cpp
 *
 * [23] 合并K个升序链表
 */

// @lc code=start
/**
 * 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) {}
 * };
 */

#include <vector>
using namespace std;
class Solution
{
public:
    ListNode *mergeKLists(vector<ListNode *> &lists)
    {
        int listslength = lists.size();
        return mergeDAC(lists, 0, listslength - 1);
    }
    ListNode *mergeDAC(vector<ListNode *> &lists, int left, int right)
    {
        if (left == right)
        {
            return lists[left];
        }
        if (left > right)
        {
            return nullptr;
        }
        //注意>>1和/2的区别
        int mid = (left + right) >> 1;
        return mergeTwoLists(mergeDAC(lists, left, mid), mergeDAC(lists, mid + 1, right));
    }
    //这个方法可以照搬21.合成两个有序链表来使用
    ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
    {
        //一旦有哪个链表元素遍历完了,直接返回另外一个链表即可
        if (l1 == nullptr || l2 == nullptr)
        {
            return l1 ? l1 : l2;
        }
        //l1比l2小,所以下一个元素应该从 "l1->next" 和 "l2" 这两个结点开头的两个链表里选
        //意思很明显了,就是l1要顺位指向下一个结点
        //else同理
        else if (l1->val < l2->val)
        {
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        }
        else
        {
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
        //最后不需要再写return,上面两端if的判断说明:如果l1->val < l2->val,最后一定是执行return l1
        //否则,最后一定是执行return l2,均符合题目要求(小的结点当头头)
    }
};
// @lc code=end
时间复杂度和空间复杂度(官方答案,说的很好)
  • 时间复杂度: O( k n log ⁡ k kn\log k knlogk) 。假设每个链表的最长长度均为 n。
    第一次合并 k 2 \frac{k}{2} 2k组链表,每组链表合并时间复杂度为 O(2n)
    第二次合并 k 4 \frac{k}{4} 4k组链表,每组链表合并时间复杂度为 O(4n)
    以此类推,总的时间复杂度:O(n+(i-1)*n)
    最后,合并k个链表意味着需要进行k-1次两两合并:O( ∑ i = 1 l o g k k 2 i × 2 i n \sum_{i=1}^{\\logk }\frac{k}{2^{i}}\times 2^{i}n i=1logk2ik×2in) = O( k n log ⁡ k kn\log k knlogk)
    由于实际中每个链表最长长度均不超过n,所以可以归纳出以上的时间复杂度。
  • 空间复杂度: O( log ⁡ k \log k logk) 。 递归次数产生的空间。
    注:为什么上面的时间复杂度和空间复杂度存在 log k?想一想二叉树高度和叶子结点的关系。

解答3:队列法

思路

假如某天免费抢购iPhone,来了16个人,商店分了4条队列,但是又要按照先来后到的顺序,那我们应该怎么办呢?

     | 1 5 9 13| 2 6 10 14| 3 7 11 15
     | 4 8 12 16

假定这16个人排成了以上的顺序。我们肯定要先选每列的第一个人,看看谁的更小,就轮到谁。可以很容易地看出来,第一个人可以先拿到iPhone,然后走人。

     | 5 9 13| 2 6 10 14| 3 7 11 15
     | 4 8 12 16

5号很开心,以为轮到自己了,但是店员告诉他,你前面还有三位顾客正在等待哦,不能插队。2号点了个赞,开开心心地拿到iPhone走了。

     | 5 9 13| 6 10 14| 3 7 11 15
     | 4 8 12 16

接着,3号和4号都拿到了iPhone,这回终于轮到5号了。

     | 5 9 13| 6 10 14| 7 11 15
     | 8 12 16

之后就是一样的道理,大家看懂了吗?

代码
/*
 * @lc app=leetcode.cn id=23 lang=cpp
 *
 * [23] 合并K个升序链表
 */

// @lc code=start
/**
 * 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) {}
 * };
 */
#include <vector>
#include <queue>
using namespace std;
//如果在自定义类内重载运算符,那么这个函数作为成员函数,有一个隐式的参数this指针,无法实现双目运算符重载
//一个双目运算符重载应该类外定义,作为全局重载使用

class Solution
{
public:
    //注意这个是队列的struct,而链表本身的struct仍然是listnode
    struct reset_heap_seq
    {
        int val;
        ListNode *queueNext;
        //重载<变为>,const reset_heap_seq &rhs指任意一个结点
        //函数加上const后缀的作用是表明函数本身不会修改类成员变量,同时使得该函数可以被 const 对象所调用
        bool operator<(const reset_heap_seq &node) const
        {
            return val > node.val;
        }
    };

    priority_queue<reset_heap_seq> firstQueue;

    ListNode *mergeKLists(vector<ListNode *> &lists)
    {
        //将目前list中非空的每个链表的第一个node push到优先队列中
        //自动调整为小根堆,即已经自动排序
        for (ListNode *node : lists)
        {
            if (node)
                firstQueue.push({node->val, node});
        }

        //定义返回的链表数组和头结点
        ListNode *resList = new ListNode();
        ListNode *head = resList;

        //如果队列非空,则pop第一个数(最小值)
        //注意分清楚queue的每一个元素是一个链表,而resList是一个链表指针
        //多想想就不难理解 first.queueNext->next->val 是什么意思
        while (!firstQueue.empty())
        {
            //定义first为队列top元素,避免编写过于复杂
            auto first = firstQueue.top();
            //弹出队列top元素
            firstQueue.pop();
            //接上队列元素下一个
            resList->next = first.queueNext;
            resList = resList->next;
            //查找下一个队列里的元素,如果非空,将当前元素所在链表的下一个元素push进队列
            if (first.queueNext->next)
                firstQueue.push({first.queueNext->next->val, first.queueNext->next});
        }
        return head->next;
    }
};
// @lc code=end
时间复杂度和空间复杂度(官方答案,说的很好)
  • 时间复杂度: O( k n log ⁡ k kn\log k knlogk)
  • 空间复杂度: O(k) 。 优先队列中的元素不超过 k 个,用于存放每一条链表当前的第一个元素。
    注:为什么上面的时间复杂度还是和 log k有关?想一想,优先队列基于什么?(大顶堆/小顶堆)

反思与总结

  1. 想一想>>1和/2的不同之处。
  2. 碰到最大最小问题,可以思考优先队列的方法是否合适。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值